git: fix i18n warnings
[idea/community.git] / plugins / git4idea / src / git4idea / GitUtil.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package git4idea;
3
4 import com.intellij.dvcs.DvcsUtil;
5 import com.intellij.dvcs.repo.Repository;
6 import com.intellij.dvcs.repo.VcsRepositoryManager;
7 import com.intellij.openapi.diagnostic.Logger;
8 import com.intellij.openapi.progress.ProgressIndicator;
9 import com.intellij.openapi.progress.Task;
10 import com.intellij.openapi.project.Project;
11 import com.intellij.openapi.ui.DialogBuilder;
12 import com.intellij.openapi.ui.ex.MultiLineLabel;
13 import com.intellij.openapi.util.Computable;
14 import com.intellij.openapi.util.NlsContexts;
15 import com.intellij.openapi.util.NlsSafe;
16 import com.intellij.openapi.util.io.FileAttributes;
17 import com.intellij.openapi.util.io.FileSystemUtil;
18 import com.intellij.openapi.util.io.FileUtil;
19 import com.intellij.openapi.util.registry.Registry;
20 import com.intellij.openapi.util.text.StringUtil;
21 import com.intellij.openapi.vcs.*;
22 import com.intellij.openapi.vcs.changes.Change;
23 import com.intellij.openapi.vcs.changes.ChangeListManager;
24 import com.intellij.openapi.vcs.changes.ChangeListManagerEx;
25 import com.intellij.openapi.vcs.changes.ChangesUtil;
26 import com.intellij.openapi.vcs.update.RefreshVFsSynchronously;
27 import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
28 import com.intellij.openapi.vfs.CharsetToolkit;
29 import com.intellij.openapi.vfs.LocalFileSystem;
30 import com.intellij.openapi.vfs.VfsUtil;
31 import com.intellij.openapi.vfs.VirtualFile;
32 import com.intellij.util.Consumer;
33 import com.intellij.util.ObjectUtils;
34 import com.intellij.util.ThrowableRunnable;
35 import com.intellij.util.containers.ContainerUtil;
36 import com.intellij.util.containers.Convertor;
37 import com.intellij.util.containers.OpenTHashSet;
38 import com.intellij.util.ui.UIUtil;
39 import com.intellij.vcs.log.Hash;
40 import com.intellij.vcs.log.impl.HashImpl;
41 import com.intellij.vcsUtil.VcsFileUtil;
42 import com.intellij.vcsUtil.VcsImplUtil;
43 import com.intellij.vcsUtil.VcsUtil;
44 import git4idea.branch.GitBranchUtil;
45 import git4idea.changes.GitChangeUtils;
46 import git4idea.changes.GitCommittedChangeList;
47 import git4idea.commands.*;
48 import git4idea.config.GitConfigUtil;
49 import git4idea.i18n.GitBundle;
50 import git4idea.repo.GitBranchTrackInfo;
51 import git4idea.repo.GitRemote;
52 import git4idea.repo.GitRepository;
53 import git4idea.repo.GitRepositoryManager;
54 import git4idea.util.GitSimplePathsBrowser;
55 import git4idea.util.GitUIUtil;
56 import git4idea.util.StringScanner;
57 import org.jetbrains.annotations.*;
58
59 import java.io.File;
60 import java.io.IOException;
61 import java.nio.charset.StandardCharsets;
62 import java.util.*;
63
64 import static com.intellij.dvcs.DvcsUtil.getShortRepositoryName;
65 import static com.intellij.dvcs.DvcsUtil.joinShortNames;
66 import static com.intellij.openapi.vcs.changes.ChangesUtil.CASE_SENSITIVE_FILE_PATH_HASHING_STRATEGY;
67 import static com.intellij.util.ObjectUtils.chooseNotNull;
68
69 /**
70  * Git utility/helper methods
71  */
72 public final class GitUtil {
73
74   public static final String DOT_GIT = ".git";
75
76   /**
77    * This comment char overrides the standard '#' and any other potentially defined by user via {@code core.commentChar}.
78    */
79   public static final @NonNls String COMMENT_CHAR = "\u0001";
80
81   public static final @NonNls String ORIGIN_HEAD = "origin/HEAD";
82
83   public static final @NlsSafe String HEAD = "HEAD";
84   public static final @NonNls String CHERRY_PICK_HEAD = "CHERRY_PICK_HEAD";
85   public static final @NonNls String MERGE_HEAD = "MERGE_HEAD";
86   public static final @NonNls String REBASE_HEAD = "REBASE_HEAD";
87
88   private static final @NonNls String REPO_PATH_LINK_PREFIX = "gitdir:";
89   private final static Logger LOG = Logger.getInstance(GitUtil.class);
90   private static final @NonNls String HEAD_FILE = "HEAD";
91
92   /**
93    * A private constructor to suppress instance creation
94    */
95   private GitUtil() {
96     // do nothing
97   }
98
99   /**
100    * Returns the Git repository location for the given repository root directory, or null if nothing can be found.
101    * Able to find the real repository root of a submodule or of a working tree.
102    * <p/>
103    * More precisely: checks if there is {@code .git} directory or file directly under rootDir. <br/>
104    * If there is a directory, performs a quick check that it looks like a Git repository;<br/>
105    * if it is a file, follows the path written inside this file to find the actual repo dir.
106    */
107   @Nullable
108   public static VirtualFile findGitDir(@NotNull VirtualFile rootDir) {
109     VirtualFile dotGit = VfsUtil.refreshAndFindChild(rootDir, DOT_GIT);
110     if (dotGit == null) {
111       return null;
112     }
113     if (dotGit.isDirectory()) {
114       boolean headExists = VfsUtil.refreshAndFindChild(dotGit, HEAD_FILE) != null;
115       return headExists ? dotGit : null;
116     }
117
118     // 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
119     String content = readContent(dotGit);
120     if (content == null) return null;
121     String pathToDir = parsePathToRepository(content);
122     File file = findRealRepositoryDir(rootDir.getPath(), pathToDir);
123     if (file == null) return null;
124     return VcsUtil.getVirtualFileWithRefresh(file);
125   }
126
127   @Nullable
128   private static File findRealRepositoryDir(@NotNull @NonNls String rootPath, @NotNull @NonNls String path) {
129     if (!FileUtil.isAbsolute(path)) {
130       String canonicalPath = FileUtil.toCanonicalPath(FileUtil.join(rootPath, path), true);
131       if (canonicalPath == null) {
132         return null;
133       }
134       path = FileUtil.toSystemIndependentName(canonicalPath);
135     }
136     File file = new File(path);
137     return file.isDirectory() ? file : null;
138   }
139
140   @NotNull
141   private static String parsePathToRepository(@NotNull @NonNls String content) {
142     content = content.trim();
143     return content.startsWith(REPO_PATH_LINK_PREFIX) ? content.substring(REPO_PATH_LINK_PREFIX.length()).trim() : content;
144   }
145
146   @Nullable
147   private static String readContent(@NotNull VirtualFile dotGit) {
148     String content;
149     try {
150       content = readFile(dotGit);
151     }
152     catch (IOException e) {
153       LOG.error("Couldn't read the content of " + dotGit, e);
154       return null;
155     }
156     return content;
157   }
158
159   /**
160    * Makes 3 attempts to get the contents of the file. If all 3 fail with an IOException, rethrows the exception.
161    */
162   @NotNull
163   private static String readFile(@NotNull VirtualFile file) throws IOException {
164     final int ATTEMPTS = 3;
165     int attempt = 1;
166     while (true) {
167       try {
168         return new String(file.contentsToByteArray(), StandardCharsets.UTF_8);
169       }
170       catch (IOException e) {
171         LOG.info(String.format("IOException while reading %s (attempt #%s)", file, attempt));
172         if (attempt++ >= ATTEMPTS) {
173           throw e;
174         }
175       }
176     }
177   }
178
179   /**
180    * @throws VcsException if non git files are passed
181    */
182   @NotNull
183   @CalledInBackground
184   public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(@NotNull Project project,
185                                                                        @NotNull Collection<? extends VirtualFile> virtualFiles)
186     throws VcsException {
187     return sortFilesByGitRoot(project, virtualFiles, false);
188   }
189
190   @NotNull
191   @CalledInBackground
192   public static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRootIgnoringMissing(@NotNull Project project,
193                                                                                       @NotNull Collection<? extends VirtualFile> filePaths) {
194     try {
195       return sortFilesByGitRoot(project, filePaths, true);
196     }
197     catch (VcsException e) {
198       LOG.error(new IllegalArgumentException(e));
199       return Collections.emptyMap();
200     }
201   }
202
203   /**
204    * @throws VcsException if non git files are passed
205    */
206   @NotNull
207   @CalledInBackground
208   public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(@NotNull Project project,
209                                                                         @NotNull Collection<? extends FilePath> filePaths)
210     throws VcsException {
211     return sortFilePathsByGitRoot(project, filePaths, false);
212   }
213
214   @NotNull
215   @CalledInBackground
216   public static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRootIgnoringMissing(@NotNull Project project,
217                                                                                        @NotNull Collection<? extends FilePath> filePaths) {
218     try {
219       return sortFilePathsByGitRoot(project, filePaths, true);
220     }
221     catch (VcsException e) {
222       LOG.error(new IllegalArgumentException(e));
223       return Collections.emptyMap();
224     }
225   }
226
227   @NotNull
228   @CalledInBackground
229   private static Map<VirtualFile, List<VirtualFile>> sortFilesByGitRoot(@NotNull Project project,
230                                                                         @NotNull Collection<? extends VirtualFile> virtualFiles,
231                                                                         boolean ignoreNonGit)
232     throws VcsException {
233     Map<GitRepository, List<VirtualFile>> map = sortFilesByRepository(project, virtualFiles, ignoreNonGit);
234
235     Map<VirtualFile, List<VirtualFile>> result = new HashMap<>();
236     map.forEach((repo, files) -> result.put(repo.getRoot(), files));
237     return result;
238   }
239
240   /**
241    * @throws VcsException if non git files are passed
242    */
243   @NotNull
244   @CalledInBackground
245   public static Map<GitRepository, List<VirtualFile>> sortFilesByRepository(@NotNull Project project,
246                                                                             @NotNull Collection<? extends VirtualFile> filePaths)
247     throws VcsException {
248     return sortFilesByRepository(project, filePaths, false);
249   }
250
251   @NotNull
252   @CalledInBackground
253   public static Map<GitRepository, List<VirtualFile>> sortFilesByRepositoryIgnoringMissing(@NotNull Project project,
254                                                                                            @NotNull Collection<? extends VirtualFile> virtualFiles) {
255     try {
256       return sortFilesByRepository(project, virtualFiles, true);
257     }
258     catch (VcsException e) {
259       LOG.error(new IllegalArgumentException(e));
260       return Collections.emptyMap();
261     }
262   }
263
264   @NotNull
265   @CalledInBackground
266   private static Map<GitRepository, List<VirtualFile>> sortFilesByRepository(@NotNull Project project,
267                                                                              @NotNull Collection<? extends VirtualFile> virtualFiles,
268                                                                              boolean ignoreNonGit)
269     throws VcsException {
270     GitRepositoryManager manager = GitRepositoryManager.getInstance(project);
271
272     Map<GitRepository, List<VirtualFile>> result = new HashMap<>();
273     for (VirtualFile file : virtualFiles) {
274       // directory is reported only when it is a submodule or a mistakenly non-ignored nested root
275       // => it should be treated in the context of super-root
276       VirtualFile actualFile = file.isDirectory() ? file.getParent() : file;
277
278       GitRepository repository = manager.getRepositoryForFile(actualFile);
279       if (repository == null) {
280         if (ignoreNonGit) continue;
281         throw new GitRepositoryNotFoundException(file);
282       }
283
284       List<VirtualFile> files = result.computeIfAbsent(repository, key -> new ArrayList<>());
285       files.add(file);
286     }
287     return result;
288   }
289
290   @NotNull
291   private static Map<VirtualFile, List<FilePath>> sortFilePathsByGitRoot(@NotNull Project project,
292                                                                          @NotNull Collection<? extends FilePath> filePaths,
293                                                                          boolean ignoreNonGit)
294     throws VcsException {
295     ProjectLevelVcsManager manager = ProjectLevelVcsManager.getInstance(project);
296     GitVcs gitVcs = GitVcs.getInstance(project);
297     Map<VirtualFile, List<FilePath>> result = new HashMap<>();
298     for (FilePath path : filePaths) {
299       VcsRoot vcsRoot = manager.getVcsRootObjectFor(path);
300       AbstractVcs vcs = vcsRoot != null ? vcsRoot.getVcs() : null;
301       if (vcs == null || !vcs.equals(gitVcs)) {
302         if (ignoreNonGit) continue;
303         throw new GitRepositoryNotFoundException(path);
304       }
305
306       List<FilePath> paths = result.computeIfAbsent(vcsRoot.getPath(), key -> new ArrayList<>());
307       paths.add(path);
308     }
309     return result;
310   }
311
312   /**
313    * Parse UNIX timestamp as it is returned by the git
314    *
315    * @param value a value to parse
316    * @return timestamp as {@link Date} object
317    */
318   public static Date parseTimestamp(@NonNls String value) {
319     final long parsed;
320     parsed = Long.parseLong(value.trim());
321     return new Date(parsed * 1000);
322   }
323
324   /**
325    * Parse UNIX timestamp returned from Git and handle {@link NumberFormatException} if one happens: return new {@link Date} and
326    * log the error properly.
327    * In some cases git output gets corrupted and this method is intended to catch the reason, why.
328    *
329    * @param value     Value to parse.
330    * @param handler   Git handler that was called to received the output.
331    * @param gitOutput Git output.
332    * @return Parsed Date or {@code new Date} in the case of error.
333    */
334   public static Date parseTimestampWithNFEReport(@NonNls String value, GitHandler handler, String gitOutput) {
335     try {
336       return parseTimestamp(value);
337     }
338     catch (NumberFormatException e) {
339       LOG.error("annotate(). NFE. Handler: " + handler + ". Output: " + gitOutput, e);
340       return new Date();
341     }
342   }
343
344   /**
345    * Return a git root for the file path (the parent directory with ".git" subdirectory)
346    *
347    * @param filePath a file path
348    * @return git root for the file
349    * @throws IllegalArgumentException if the file is not under git
350    * @throws VcsException             if the file is not under git
351    *
352    * @deprecated because uses the java.io.File.
353    * @use GitRepositoryManager#getRepositoryForFile().
354    */
355   @Deprecated
356   public static VirtualFile getGitRoot(@NotNull FilePath filePath) throws VcsException {
357     VirtualFile root = getGitRootOrNull(filePath);
358     if (root != null) {
359       return root;
360     }
361     throw new VcsException("The file " + filePath + " is not under git.");
362   }
363
364   /**
365    * Return a git root for the file path (the parent directory with ".git" subdirectory)
366    *
367    * @param filePath a file path
368    * @return git root for the file or null if the file is not under git
369    *
370    * @deprecated because uses the java.io.File.
371    * @use GitRepositoryManager#getRepositoryForFile().
372    */
373   @Deprecated
374   @Nullable
375   public static VirtualFile getGitRootOrNull(@NotNull final FilePath filePath) {
376     File root = filePath.getIOFile();
377     while (root != null) {
378       if (isGitRoot(root)) {
379         return LocalFileSystem.getInstance().findFileByIoFile(root);
380       }
381       root = root.getParentFile();
382     }
383     return null;
384   }
385
386   public static boolean isGitRoot(@NotNull File folder) {
387     return isGitRoot(folder.getPath());
388   }
389
390   /**
391    * Return a git root for the file (the parent directory with ".git" subdirectory)
392    *
393    * @param file the file to check
394    * @return git root for the file
395    * @throws VcsException if the file is not under git
396    *
397    * @deprecated because uses the java.io.File.
398    * @use GitRepositoryManager#getRepositoryForFile().
399    */
400   @Deprecated
401   public static VirtualFile getGitRoot(@NotNull final VirtualFile file) throws VcsException {
402     final VirtualFile root = gitRootOrNull(file);
403     if (root != null) {
404       return root;
405     }
406     else {
407       throw new VcsException("The file " + file.getPath() + " is not under git.");
408     }
409   }
410
411   /**
412    * Return a git root for the file (the parent directory with ".git" subdirectory)
413    *
414    * @param file the file to check
415    * @return git root for the file or null if the file is not not under Git
416    *
417    * @deprecated because uses the java.io.File.
418    * @use GitRepositoryManager#getRepositoryForFile().
419    */
420   @Deprecated
421   @Nullable
422   public static VirtualFile gitRootOrNull(final VirtualFile file) {
423     return getGitRootOrNull(VcsUtil.getFilePath(file.getPath()));
424   }
425
426   /**
427    * Check if the virtual file under git
428    *
429    * @param vFile a virtual file
430    * @return true if the file is under git
431    */
432   public static boolean isUnderGit(final VirtualFile vFile) {
433     return gitRootOrNull(vFile) != null;
434   }
435
436
437   /**
438    * Return committer name based on author name and committer name
439    *
440    * @param authorName    the name of author
441    * @param committerName the name of committer
442    * @return just a name if they are equal, or name that includes both author and committer
443    */
444   @NlsSafe
445   public static String adjustAuthorName(@NlsSafe String authorName, @NlsSafe String committerName) {
446     if (!authorName.equals(committerName)) {
447       //noinspection HardCodedStringLiteral
448       committerName = authorName + ", via " + committerName;
449     }
450     return committerName;
451   }
452
453   /**
454    * Check if the file path is under git
455    *
456    * @param path the path
457    * @return true if the file path is under git
458    */
459   public static boolean isUnderGit(final FilePath path) {
460     return getGitRootOrNull(path) != null;
461   }
462
463   @NotNull
464   @CalledInBackground
465   public static Set<GitRepository> getRepositoriesForFiles(@NotNull Project project, @NotNull Collection<? extends VirtualFile> files)
466     throws VcsException {
467     Set<GitRepository> result = new HashSet<>();
468     for (VirtualFile file : files) {
469       result.add(getRepositoryForFile(project, file));
470     }
471     return result;
472   }
473
474   /**
475    * Get git time (UNIX time) basing on the date object
476    *
477    * @param time the time to convert
478    * @return the time in git format
479    */
480   @NonNls
481   public static String gitTime(Date time) {
482     long t = time.getTime() / 1000;
483     return Long.toString(t);
484   }
485
486   /**
487    * Format revision number from long to 16-digit abbreviated revision
488    *
489    * @param rev the abbreviated revision number as long
490    * @return the revision string
491    */
492   @NonNls
493   public static String formatLongRev(long rev) {
494     return String.format("%015x%x", (rev >>> 4), rev & 0xF);
495   }
496
497   public static void getLocalCommittedChanges(final Project project,
498                                               final VirtualFile root,
499                                               final Consumer<? super GitHandler> parametersSpecifier,
500                                               final Consumer<? super GitCommittedChangeList> consumer, boolean skipDiffsForMerge) throws VcsException {
501     GitLineHandler h = new GitLineHandler(project, root, GitCommand.LOG);
502     h.setSilent(true);
503     h.addParameters("--pretty=format:%x04%x01" + GitChangeUtils.COMMITTED_CHANGELIST_FORMAT, "--name-status");
504     parametersSpecifier.consume(h);
505
506     String output = Git.getInstance().runCommand(h).getOutputOrThrow();
507     LOG.debug("getLocalCommittedChanges output: '" + output + "'");
508     StringScanner s = new StringScanner(output);
509     final StringBuilder sb = new StringBuilder();
510     boolean firstStep = true;
511     while (s.hasMoreData()) {
512       final String line = s.line();
513       final boolean lineIsAStart = line.startsWith("\u0004\u0001");
514       if ((!firstStep) && lineIsAStart) {
515         final StringScanner innerScanner = new StringScanner(sb.toString());
516         sb.setLength(0);
517         consumer.consume(GitChangeUtils.parseChangeList(project, root, innerScanner, skipDiffsForMerge, h, false, false));
518       }
519       sb.append(lineIsAStart ? line.substring(2) : line).append('\n');
520       firstStep = false;
521     }
522     if (sb.length() > 0) {
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     if (s.hasMoreData()) {
528       throw new IllegalStateException("More input is available: " + s.line());
529     }
530   }
531
532   public static List<GitCommittedChangeList> getLocalCommittedChanges(final Project project,
533                                                                    final VirtualFile root,
534                                                                    final Consumer<? super GitHandler> parametersSpecifier)
535     throws VcsException {
536     final List<GitCommittedChangeList> rc = new ArrayList<>();
537
538     getLocalCommittedChanges(project, root, parametersSpecifier, committedChangeList -> rc.add(committedChangeList), false);
539
540     return rc;
541   }
542
543   /**
544    * @throws VcsException if the path is invalid
545    * @see VcsFileUtil#unescapeGitPath(String, String)
546    */
547   @NotNull
548   public static String unescapePath(@NotNull @NonNls String path) throws VcsException {
549     try {
550       return VcsFileUtil.unescapeGitPath(path, GitConfigUtil.getFileNameEncoding());
551     }
552     catch (IllegalStateException e) {
553       throw new VcsException(e);
554     }
555   }
556
557   public static boolean justOneGitRepository(Project project) {
558     if (project.isDisposed()) {
559       return true;
560     }
561     GitRepositoryManager manager = getRepositoryManager(project);
562     return !manager.moreThanOneRoot();
563   }
564
565
566   @Nullable
567   public static GitRemote findRemoteByName(@NotNull GitRepository repository, @NotNull @NonNls String name) {
568     return findRemoteByName(repository.getRemotes(), name);
569   }
570
571   @Nullable
572   public static GitRemote findRemoteByName(Collection<GitRemote> remotes, @NotNull @NonNls String name) {
573     return ContainerUtil.find(remotes, remote -> remote.getName().equals(name));
574   }
575
576   @Nullable
577   public static GitRemoteBranch findRemoteBranch(@NotNull GitRepository repository,
578                                                  @NotNull final GitRemote remote,
579                                                  @NotNull @NonNls String nameAtRemote) {
580     return ContainerUtil.find(repository.getBranches().getRemoteBranches(), remoteBranch -> {
581       return remoteBranch.getRemote().equals(remote) &&
582              remoteBranch.getNameForRemoteOperations().equals(GitBranchUtil.stripRefsPrefix(nameAtRemote));
583     });
584   }
585
586   @NotNull
587   public static GitRemoteBranch findOrCreateRemoteBranch(@NotNull GitRepository repository,
588                                                          @NotNull GitRemote remote,
589                                                          @NotNull @NonNls String branchName) {
590     GitRemoteBranch remoteBranch = findRemoteBranch(repository, remote, branchName);
591     return ObjectUtils.notNull(remoteBranch, new GitStandardRemoteBranch(remote, branchName));
592   }
593
594   @NotNull
595   public static Collection<VirtualFile> getRootsFromRepositories(@NotNull Collection<? extends GitRepository> repositories) {
596     return ContainerUtil.map(repositories, Repository::getRoot);
597   }
598
599   @NotNull
600   @CalledInBackground
601   public static Collection<GitRepository> getRepositoriesFromRoots(@NotNull GitRepositoryManager repositoryManager,
602                                                                    @NotNull Collection<? extends VirtualFile> roots) {
603     Collection<GitRepository> repositories = new ArrayList<>(roots.size());
604     for (VirtualFile root : roots) {
605       GitRepository repo = repositoryManager.getRepositoryForRoot(root);
606       if (repo == null) {
607         LOG.error("Repository not found for root " + root);
608       }
609       else {
610         repositories.add(repo);
611       }
612     }
613     return repositories;
614   }
615
616   /**
617    * Returns absolute paths which have changed remotely comparing to the current branch, i.e. performs
618    * {@code git diff --name-only master..origin/master}
619    * <p/>
620    * Paths are absolute, Git-formatted (i.e. with forward slashes).
621    */
622   @NotNull
623   public static Collection<String> getPathsDiffBetweenRefs(@NotNull Git git, @NotNull GitRepository repository,
624                                                            @NotNull @NonNls String beforeRef, @NotNull @NonNls String afterRef)
625     throws VcsException {
626     List<String> parameters = Arrays.asList("--name-only", "--pretty=format:");
627     String range = beforeRef + ".." + afterRef;
628     GitCommandResult result = git.diff(repository, parameters, range);
629     if (!result.success()) {
630       LOG.info(String.format("Couldn't get diff in range [%s] for repository [%s]", range, repository.toLogString()));
631       return Collections.emptyList();
632     }
633
634     final Collection<String> remoteChanges = new HashSet<>();
635     for (StringScanner s = new StringScanner(result.getOutputAsJoinedString()); s.hasMoreData(); ) {
636       final String relative = s.line();
637       if (StringUtil.isEmptyOrSpaces(relative)) {
638         continue;
639       }
640       final String path = repository.getRoot().getPath() + "/" + unescapePath(relative);
641       remoteChanges.add(path);
642     }
643     return remoteChanges;
644   }
645
646   @NotNull
647   public static GitRepositoryManager getRepositoryManager(@NotNull Project project) {
648     return GitRepositoryManager.getInstance(project);
649   }
650
651   @NotNull
652   @CalledInBackground
653   public static GitRepository getRepositoryForFile(@NotNull Project project, @NotNull VirtualFile file) throws VcsException {
654     GitRepository repository = GitRepositoryManager.getInstance(project).getRepositoryForFile(file);
655     if (repository == null) throw new GitRepositoryNotFoundException(file);
656     return repository;
657   }
658
659   @NotNull
660   @CalledInBackground
661   public static GitRepository getRepositoryForFile(@NotNull Project project, @NotNull FilePath file) throws VcsException {
662     GitRepository repository = GitRepositoryManager.getInstance(project).getRepositoryForFile(file);
663     if (repository == null) throw new GitRepositoryNotFoundException(file);
664     return repository;
665   }
666
667   @NotNull
668   @CalledInBackground
669   public static GitRepository getRepositoryForRoot(@NotNull Project project, @NotNull VirtualFile root) throws VcsException {
670     GitRepository repository = GitRepositoryManager.getInstance(project).getRepositoryForRoot(root);
671     if (repository == null) throw new GitRepositoryNotFoundException(root);
672     return repository;
673   }
674
675   @Nullable
676   public static GitRepository getRepositoryForRootOrLogError(@NotNull Project project, @NotNull VirtualFile root) {
677     GitRepository repository = GitRepositoryManager.getInstance(project).getRepositoryForRoot(root);
678     if (repository == null) LOG.error(new GitRepositoryNotFoundException(root));
679     return repository;
680   }
681
682   @NotNull
683   public static VirtualFile getRootForFile(@NotNull Project project, @NotNull FilePath filePath) throws VcsException {
684     VcsRoot root = ProjectLevelVcsManager.getInstance(project).getVcsRootObjectFor(filePath);
685     if (isGitVcsRoot(root)) return root.getPath();
686
687     Repository repository = VcsRepositoryManager.getInstance(project).getExternalRepositoryForFile(filePath);
688     if (repository instanceof GitRepository) return repository.getRoot();
689     throw new GitRepositoryNotFoundException(filePath);
690   }
691
692   @NotNull
693   public static VirtualFile getRootForFile(@NotNull Project project, @NotNull VirtualFile file) throws VcsException {
694     VcsRoot root = ProjectLevelVcsManager.getInstance(project).getVcsRootObjectFor(file);
695     if (isGitVcsRoot(root)) return root.getPath();
696
697     Repository repository = VcsRepositoryManager.getInstance(project).getExternalRepositoryForFile(file);
698     if (repository instanceof GitRepository) return repository.getRoot();
699     throw new GitRepositoryNotFoundException(file);
700   }
701
702   private static boolean isGitVcsRoot(@Nullable VcsRoot root) {
703     if (root == null) return false;
704     AbstractVcs vcs = root.getVcs();
705     if (vcs == null) return false;
706     return GitVcs.getKey().equals(vcs.getKeyInstanceMethod());
707   }
708
709   /**
710    * Show changes made in the specified revision.
711    *
712    * @param project    the project
713    * @param revision   the revision number
714    * @param file       the file affected by the revision
715    * @param local      pass true to let the diff be editable, i.e. making the revision "at the right" be a local (current) revision.
716    *                   pass false to let both sides of the diff be non-editable.
717    * @param revertable pass true to let "Revert" action be active.
718    */
719   public static void showSubmittedFiles(final Project project, @NonNls String revision, final VirtualFile file,
720                                         final boolean local, final boolean revertable) {
721     new Task.Backgroundable(project, GitBundle.message("changes.retrieving", revision)) {
722       @Override
723       public void run(@NotNull ProgressIndicator indicator) {
724         indicator.setIndeterminate(true);
725         try {
726           VirtualFile vcsRoot = getRootForFile(project, file);
727           final CommittedChangeList changeList = GitChangeUtils.getRevisionChanges(project, vcsRoot, revision, true, local, revertable);
728           UIUtil.invokeLaterIfNeeded(
729             () -> AbstractVcsHelper.getInstance(project)
730               .showChangesListBrowser(changeList, GitBundle.message("paths.affected.title", revision)));
731         }
732         catch (final VcsException e) {
733           UIUtil.invokeLaterIfNeeded(() -> GitUIUtil.showOperationError(project, e, "git show"));
734         }
735       }
736     }.queue();
737   }
738
739
740   /**
741    * Returns the tracking information (remote and the name of the remote branch), or null if we are not on a branch.
742    */
743   @Nullable
744   public static GitBranchTrackInfo getTrackInfoForCurrentBranch(@NotNull GitRepository repository) {
745     GitLocalBranch currentBranch = repository.getCurrentBranch();
746     if (currentBranch == null) {
747       return null;
748     }
749     return GitBranchUtil.getTrackInfoForBranch(repository, currentBranch);
750   }
751
752   /**
753    * git diff --name-only [--cached]
754    * @return true if there is anything in the unstaged/staging area, false if the unstaged/staging area is empty.
755    * @param staged if true checks the staging area, if false checks unstaged files.
756    * @param project
757    * @param root
758    */
759   public static boolean hasLocalChanges(boolean staged, Project project, VirtualFile root) throws VcsException {
760     GitLineHandler diff = new GitLineHandler(project, root, GitCommand.DIFF);
761     diff.addParameters("--name-only");
762     diff.addParameters("--no-renames");
763     if (staged) {
764       diff.addParameters("--cached");
765     }
766     diff.setStdoutSuppressed(true);
767     diff.setStderrSuppressed(true);
768     diff.setSilent(true);
769     final String output = Git.getInstance().runCommand(diff).getOutputOrThrow();
770     return !output.trim().isEmpty();
771   }
772
773   @Nullable
774   public static VirtualFile findRefreshFileOrLog(@NotNull @NonNls String absolutePath) {
775     VirtualFile file = LocalFileSystem.getInstance().findFileByPath(absolutePath);
776     if (file == null) {
777       file = LocalFileSystem.getInstance().refreshAndFindFileByPath(absolutePath);
778     }
779     if (file == null) {
780       LOG.debug("VirtualFile not found for " + absolutePath);
781     }
782     return file;
783   }
784
785   @NotNull
786   public static String toAbsolute(@NotNull VirtualFile root, @NotNull @NonNls String relativePath) {
787     return StringUtil.trimEnd(root.getPath(), "/") + "/" + StringUtil.trimStart(relativePath, "/");
788   }
789
790   @NotNull
791   public static Collection<String> toAbsolute(@NotNull final VirtualFile root, @NotNull Collection<@NonNls String> relativePaths) {
792     return ContainerUtil.map(relativePaths, s -> toAbsolute(root, s));
793   }
794
795   /**
796    * Given the list of paths converts them to the list of {@link Change Changes} found in the {@link ChangeListManager},
797    * i.e. this works only for local changes. </br>
798    * Paths can be absolute or relative to the repository.
799    * If a path is not found in the local changes, it is ignored, but the fact is logged.
800    */
801   @NotNull
802   public static List<Change> findLocalChangesForPaths(@NotNull Project project, @NotNull VirtualFile root,
803                                                       @NotNull Collection<@NonNls String> affectedPaths, boolean relativePaths) {
804     ChangeListManagerEx changeListManager = (ChangeListManagerEx)ChangeListManager.getInstance(project);
805     List<Change> affectedChanges = new ArrayList<>();
806     for (String path : affectedPaths) {
807       String absolutePath = relativePaths ? toAbsolute(root, path) : path;
808       VirtualFile file = findRefreshFileOrLog(absolutePath);
809       if (file != null) {
810         Change change = changeListManager.getChange(file);
811         if (change != null) {
812           affectedChanges.add(change);
813         }
814         else {
815           String message = "Change is not found for " + file.getPath();
816           if (changeListManager.isInUpdate()) {
817             message += " because ChangeListManager is being updated.";
818           }
819           LOG.debug(message);
820         }
821       }
822     }
823     return affectedChanges;
824   }
825
826   public static void showPathsInDialog(@NotNull Project project,
827                                        @NotNull Collection<@NonNls String> absolutePaths,
828                                        @NotNull @NlsContexts.DialogTitle String title,
829                                        @Nullable @NlsContexts.DialogMessage String description) {
830     DialogBuilder builder = new DialogBuilder(project);
831     builder.setCenterPanel(new GitSimplePathsBrowser(project, absolutePaths));
832     if (description != null) {
833       builder.setNorthPanel(new MultiLineLabel(description));
834     }
835     builder.addOkAction();
836     builder.setTitle(title);
837     builder.show();
838   }
839
840   @NlsSafe
841   @NotNull
842   public static String cleanupErrorPrefixes(@NotNull @NlsSafe String msg) {
843     final @NonNls String[] PREFIXES = { "fatal:", "error:" };
844     msg = msg.trim();
845     for (String prefix : PREFIXES) {
846       if (msg.startsWith(prefix)) {
847         msg = msg.substring(prefix.length()).trim();
848       }
849     }
850     return msg;
851   }
852
853   @Nullable
854   public static GitRemote getDefaultRemote(@NotNull Collection<GitRemote> remotes) {
855     return ContainerUtil.find(remotes, r -> r.getName().equals(GitRemote.ORIGIN));
856   }
857
858   @Nullable
859   public static GitRemote getDefaultOrFirstRemote(@NotNull Collection<GitRemote> remotes) {
860     return chooseNotNull(getDefaultRemote(remotes), ContainerUtil.getFirstItem(remotes));
861   }
862
863   @NotNull
864   public static String joinToHtml(@NotNull Collection<? extends GitRepository> repositories) {
865     return StringUtil.join(repositories, repository -> repository.getPresentableUrl(), UIUtil.BR);
866   }
867
868   @Nls
869   @NotNull
870   public static String mention(@NotNull GitRepository repository) {
871     return getRepositoryManager(repository.getProject()).moreThanOneRoot() ? " in " + getShortRepositoryName(repository) : "";
872   }
873
874   @Nls
875   @NotNull
876   public static String mention(@NotNull Collection<? extends GitRepository> repositories) {
877     if (repositories.isEmpty()) return "";
878     return " in " + joinShortNames(repositories, -1);
879   }
880
881   public static void updateRepositories(@NotNull Collection<? extends GitRepository> repositories) {
882     for (GitRepository repository : repositories) {
883       repository.update();
884     }
885   }
886
887   public static boolean hasGitRepositories(@NotNull Project project) {
888     return !getRepositories(project).isEmpty();
889   }
890
891   @NotNull
892   public static Collection<GitRepository> getRepositories(@NotNull Project project) {
893     return getRepositoryManager(project).getRepositories();
894   }
895
896   @NotNull
897   public static Collection<GitRepository> getRepositoriesInState(@NotNull Project project, @NotNull Repository.State state) {
898     return ContainerUtil.filter(getRepositories(project), repository -> repository.getState() == state);
899   }
900
901   /**
902    * Checks if the given paths are equal only by case.
903    * It is expected that the paths are different at least by the case.
904    */
905   public static boolean isCaseOnlyChange(@NotNull @NonNls String oldPath, @NotNull @NonNls String newPath) {
906     if (oldPath.equalsIgnoreCase(newPath)) {
907       if (oldPath.equals(newPath)) {
908         LOG.info("Comparing perfectly equal paths: " + newPath);
909       }
910       return true;
911     }
912     return false;
913   }
914
915   @NonNls
916   @NotNull
917   public static String getLogStringGitDiffChanges(@NotNull @NonNls String root,
918                                                   @NotNull Collection<? extends GitChangeUtils.GitDiffChange> changes) {
919     return getLogString(root, changes, it -> it.getBeforePath(), it -> it.getAfterPath());
920   }
921
922   @NonNls
923   @NotNull
924   public static String getLogString(@NotNull @NonNls String root, @NotNull Collection<? extends Change> changes) {
925     return getLogString(root, changes, ChangesUtil::getBeforePath, ChangesUtil::getAfterPath);
926   }
927
928   @NonNls
929   @NotNull
930   public static <T> String getLogString(@NotNull @NonNls String root, @NotNull Collection<? extends T> changes,
931                                         @NotNull Convertor<? super T, ? extends FilePath> beforePathGetter,
932                                         @NotNull Convertor<? super T, ? extends FilePath> afterPathGetter) {
933     return StringUtil.join(changes, change -> {
934       FilePath after = afterPathGetter.convert(change);
935       FilePath before = beforePathGetter.convert(change);
936       if (before == null) {
937         return "A: " + getRelativePath(root, after);
938       }
939       else if (after == null) {
940         return "D: " + getRelativePath(root, before);
941       }
942       else if (CASE_SENSITIVE_FILE_PATH_HASHING_STRATEGY.equals(before, after)) {
943         return "M: " + getRelativePath(root, after);
944       }
945       else {
946         return "R: " + getRelativePath(root, before) + " -> " + getRelativePath(root, after);
947       }
948     }, ", ");
949   }
950
951   @Nullable
952   public static String getRelativePath(@NotNull String root, @NotNull FilePath after) {
953     return FileUtil.getRelativePath(root, after.getPath(), File.separatorChar);
954   }
955
956   /**
957    * <p>Finds the local changes which are "the same" as the given changes.</p>
958    * <p>The purpose of this method is to get actual local changes after some other changes were applied to the working tree
959    * (e.g. if they were cherry-picked from a commit). Working with the original non-local changes is limited, in particular,
960    * the difference between content revisions may be not the same as the local change.</p>
961    * <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
962    * commit, but there are no local changes made in file A. Such situations are ignored.</p>
963    */
964   @NotNull
965   public static Collection<Change> findCorrespondentLocalChanges(@NotNull ChangeListManager changeListManager,
966                                                                  @NotNull Collection<? extends Change> originalChanges) {
967     OpenTHashSet<Change> allChanges = new OpenTHashSet<>(changeListManager.getAllChanges());
968     return ContainerUtil.mapNotNull(originalChanges, allChanges::get);
969   }
970
971   /**
972    * A convenience method to refresh either a part of the VFS modified by the given changes, or the whole root recursively.
973    *
974    * @param changes The changes which files were modified by a Git operation.
975    *                If null, the whole root is refreshed. Otherwise, only the files touched by these changes.
976    */
977   public static void refreshVfs(@NotNull VirtualFile root, @Nullable Collection<? extends Change> changes) {
978     if (changes == null || Registry.is("git.refresh.vfs.total")) {
979       refreshVfsInRoot(root);
980     }
981     else {
982       RefreshVFsSynchronously.updateChanges(changes);
983     }
984   }
985
986   public static void refreshVfsInRoot(@NotNull VirtualFile root) {
987     RefreshVFsSynchronously.trace("refresh root " + root);
988     VfsUtil.markDirtyAndRefresh(false, true, false, root);
989   }
990
991   public static void updateAndRefreshChangedVfs(@NotNull GitRepository repository, @Nullable Hash startHash) {
992     repository.update();
993     refreshChangedVfs(repository, startHash);
994   }
995
996   public static void refreshChangedVfs(@NotNull GitRepository repository, @Nullable Hash startHash) {
997     Collection<Change> changes = null;
998     if (startHash != null) {
999       Hash currentHash = getHead(repository);
1000       if (currentHash != null) {
1001         RefreshVFsSynchronously.trace(String.format("changes: %s -> %s", startHash.asString(), currentHash.asString()));
1002         changes = GitChangeUtils.getDiff(repository, startHash.asString(), currentHash.asString(), false);
1003       }
1004     }
1005     refreshVfs(repository.getRoot(), changes);
1006   }
1007
1008   public static boolean isGitRoot(@NotNull @NonNls String rootDir) {
1009     String dotGit = rootDir + File.separatorChar + DOT_GIT;
1010     FileAttributes attributes = FileSystemUtil.getAttributes(dotGit);
1011     if (attributes == null) return false;
1012
1013     if (attributes.isDirectory()) {
1014       FileAttributes headExists = FileSystemUtil.getAttributes(dotGit + File.separatorChar + HEAD_FILE);
1015       return headExists != null && headExists.isFile();
1016     }
1017     if (!attributes.isFile()) return false;
1018
1019     String content = DvcsUtil.tryLoadFileOrReturn(new File(dotGit), null, CharsetToolkit.UTF8);
1020     if (content == null) return false;
1021     String pathToDir = parsePathToRepository(content);
1022     return findRealRepositoryDir(rootDir, pathToDir) != null;
1023   }
1024
1025   public static void generateGitignoreFileIfNeeded(@NotNull Project project, @NotNull VirtualFile ignoreFileRoot) {
1026     VcsImplUtil.generateIgnoreFileIfNeeded(project, GitVcs.getInstance(project), ignoreFileRoot);
1027   }
1028
1029   public static <T extends Throwable> void tryRunOrClose(@NotNull AutoCloseable closeable,
1030                                                          @NotNull ThrowableRunnable<T> runnable) throws T {
1031     try {
1032       runnable.run();
1033     }
1034     catch (Throwable e) {
1035       try {
1036         closeable.close();
1037       }
1038       catch (Throwable e2) {
1039         e.addSuppressed(e2);
1040       }
1041       throw e;
1042     }
1043   }
1044
1045   private static final class GitRepositoryNotFoundException extends VcsException {
1046
1047     private GitRepositoryNotFoundException(@NotNull VirtualFile file) {
1048       super(GitBundle.message("repository.not.found.error", file.getPresentableUrl()));
1049     }
1050
1051     private GitRepositoryNotFoundException(@NotNull FilePath filePath) {
1052       super(GitBundle.message("repository.not.found.error", filePath.getPresentableUrl()));
1053     }
1054   }
1055
1056   @NotNull
1057   public static <T extends GitHandler> T createHandlerWithPaths(@Nullable Collection<? extends FilePath> paths,
1058                                                                 @NotNull Computable<T> handlerBuilder) {
1059     T handler = handlerBuilder.compute();
1060     handler.endOptions();
1061     if (paths != null) {
1062       handler.addRelativePaths(paths);
1063       if (handler.isLargeCommandLine()) {
1064         handler = handlerBuilder.compute();
1065         handler.endOptions();
1066       }
1067     }
1068     return handler;
1069   }
1070
1071   @Nullable
1072   public static Hash getHead(@NotNull GitRepository repository) {
1073     GitCommandResult result = Git.getInstance().tip(repository, HEAD);
1074     if (!result.success()) {
1075       LOG.warn("Couldn't identify the HEAD for " + repository + ": " + result.getErrorOutputAsJoinedString());
1076       return null;
1077     }
1078     String head = result.getOutputAsJoinedString();
1079     return HashImpl.build(head);
1080   }
1081 }