replaced <code></code> with more concise {@code}
[idea/community.git] / plugins / hg4idea / src / org / zmlx / hg4idea / util / HgUtil.java
1 // Copyright 2010 Victor Iacoban
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software distributed under
10 // the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
11 // either express or implied. See the License for the specific language governing permissions and
12 // limitations under the License.
13 package org.zmlx.hg4idea.util;
14
15 import com.intellij.dvcs.DvcsUtil;
16 import com.intellij.openapi.Disposable;
17 import com.intellij.openapi.application.ApplicationManager;
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.util.BackgroundTaskUtil;
22 import com.intellij.openapi.project.Project;
23 import com.intellij.openapi.ui.Messages;
24 import com.intellij.openapi.util.Couple;
25 import com.intellij.openapi.util.ShutDownTracker;
26 import com.intellij.openapi.util.io.FileUtil;
27 import com.intellij.openapi.util.text.StringUtil;
28 import com.intellij.openapi.vcs.FilePath;
29 import com.intellij.openapi.vcs.FileStatus;
30 import com.intellij.openapi.vcs.VcsException;
31 import com.intellij.openapi.vcs.changes.Change;
32 import com.intellij.openapi.vcs.changes.ChangeListManager;
33 import com.intellij.openapi.vcs.changes.ContentRevision;
34 import com.intellij.openapi.vcs.changes.VcsDirtyScopeManager;
35 import com.intellij.openapi.vcs.history.FileHistoryPanelImpl;
36 import com.intellij.openapi.vcs.history.VcsFileRevisionEx;
37 import com.intellij.openapi.vcs.vfs.AbstractVcsVirtualFile;
38 import com.intellij.openapi.vcs.vfs.VcsVirtualFile;
39 import com.intellij.openapi.vfs.CharsetToolkit;
40 import com.intellij.openapi.vfs.LocalFileSystem;
41 import com.intellij.openapi.vfs.VfsUtil;
42 import com.intellij.openapi.vfs.VirtualFile;
43 import com.intellij.ui.GuiUtils;
44 import com.intellij.util.ArrayUtil;
45 import com.intellij.util.containers.ContainerUtil;
46 import com.intellij.vcsUtil.VcsUtil;
47 import org.jetbrains.annotations.CalledInAwt;
48 import org.jetbrains.annotations.NotNull;
49 import org.jetbrains.annotations.Nullable;
50 import org.zmlx.hg4idea.*;
51 import org.zmlx.hg4idea.command.HgCatCommand;
52 import org.zmlx.hg4idea.command.HgRemoveCommand;
53 import org.zmlx.hg4idea.command.HgStatusCommand;
54 import org.zmlx.hg4idea.command.HgWorkingCopyRevisionsCommand;
55 import org.zmlx.hg4idea.execution.HgCommandResult;
56 import org.zmlx.hg4idea.execution.ShellCommand;
57 import org.zmlx.hg4idea.execution.ShellCommandException;
58 import org.zmlx.hg4idea.log.HgHistoryUtil;
59 import org.zmlx.hg4idea.provider.HgChangeProvider;
60 import org.zmlx.hg4idea.repo.HgRepository;
61 import org.zmlx.hg4idea.repo.HgRepositoryManager;
62
63 import java.io.*;
64 import java.lang.reflect.InvocationTargetException;
65 import java.util.*;
66 import java.util.regex.Matcher;
67 import java.util.regex.Pattern;
68
69 /**
70  * HgUtil is a collection of static utility methods for Mercurial.
71  */
72 public abstract class HgUtil {
73
74   public static final Pattern URL_WITH_PASSWORD = Pattern.compile("(?:.+)://(?:.+)(:.+)@(?:.+)");      //http(s)://username:password@url
75   public static final int MANY_FILES = 100;
76   private static final Logger LOG = Logger.getInstance(HgUtil.class);
77   public static final String DOT_HG = ".hg";
78   public static final String TIP_REFERENCE = "tip";
79   public static final String HEAD_REFERENCE = "HEAD";
80
81   public static File copyResourceToTempFile(String basename, String extension) throws IOException {
82     final InputStream in = HgUtil.class.getClassLoader().getResourceAsStream("python/" + basename + extension);
83
84     final File tempFile = FileUtil.createTempFile(basename, extension);
85     final byte[] buffer = new byte[4096];
86
87     OutputStream out = null;
88     try {
89       out = new FileOutputStream(tempFile, false);
90       int bytesRead;
91       while ((bytesRead = in.read(buffer)) != -1)
92         out.write(buffer, 0, bytesRead);
93     } finally {
94       try {
95         out.close();
96       }
97       catch (IOException e) {
98         // ignore
99       }
100     }
101     try {
102       in.close();
103     }
104     catch (IOException e) {
105       // ignore
106     }
107     tempFile.deleteOnExit();
108     return tempFile;
109   }
110
111   public static void markDirectoryDirty(final Project project, final VirtualFile file)
112     throws InvocationTargetException, InterruptedException {
113     VfsUtil.markDirtyAndRefresh(true, true, false, file);
114     VcsDirtyScopeManager.getInstance(project).dirDirtyRecursively(file);
115   }
116
117   public static void markFileDirty(final Project project, final VirtualFile file) throws InvocationTargetException, InterruptedException {
118     ApplicationManager.getApplication().runReadAction(() -> VcsDirtyScopeManager.getInstance(project).fileDirty(file));
119     runWriteActionAndWait(() -> file.refresh(true, false));
120   }
121
122   /**
123    * Runs the given task as a write action in the event dispatching thread and waits for its completion.
124    */
125   public static void runWriteActionAndWait(@NotNull final Runnable runnable) throws InvocationTargetException, InterruptedException {
126     GuiUtils.runOrInvokeAndWait(() -> ApplicationManager.getApplication().runWriteAction(runnable));
127   }
128
129   /**
130    * Schedules the given task to be run as a write action in the event dispatching thread.
131    */
132   public static void runWriteActionLater(@NotNull final Runnable runnable) {
133     ApplicationManager.getApplication().invokeLater(() -> ApplicationManager.getApplication().runWriteAction(runnable));
134   }
135
136   /**
137    * Returns a temporary python file that will be deleted on exit.
138    *
139    * Also all compiled version of the python file will be deleted.
140    *
141    * @param base The basename of the file to copy
142    * @return The temporary copy the specified python file, with all the necessary hooks installed
143    * to make sure it is completely removed at shutdown
144    */
145   @Nullable
146   public static File getTemporaryPythonFile(String base) {
147     try {
148       final File file = copyResourceToTempFile(base, ".py");
149       final String fileName = file.getName();
150       ShutDownTracker.getInstance().registerShutdownTask(() -> {
151         File[] files = file.getParentFile().listFiles((dir, name) -> name.startsWith(fileName));
152         if (files != null) {
153           for (File file1 : files) {
154             file1.delete();
155           }
156         }
157       });
158       return file;
159     } catch (IOException e) {
160       return null;
161     }
162   }
163
164   /**
165    * Calls 'hg remove' to remove given files from the VCS.
166    * @param project
167    * @param files files to be removed from the VCS.
168    */
169   public static void removeFilesFromVcs(Project project, List<FilePath> files) {
170     final HgRemoveCommand command = new HgRemoveCommand(project);
171     for (FilePath filePath : files) {
172       final VirtualFile vcsRoot = VcsUtil.getVcsRootFor(project, filePath);
173       if (vcsRoot == null) {
174         continue;
175       }
176       command.executeInCurrentThread(new HgFile(vcsRoot, filePath));
177     }
178   }
179
180
181   /**
182    * Finds the nearest parent directory which is an hg root.
183    * @param dir Directory which parent will be checked.
184    * @return Directory which is the nearest hg root being a parent of this directory,
185    * or {@code null} if this directory is not under hg.
186    * @see com.intellij.openapi.vcs.AbstractVcs#isVersionedDirectory(VirtualFile)
187    */
188   @Nullable
189   public static VirtualFile getNearestHgRoot(VirtualFile dir) {
190     VirtualFile currentDir = dir;
191     while (currentDir != null) {
192       if (isHgRoot(currentDir)) {
193         return currentDir;
194       }
195       currentDir = currentDir.getParent();
196     }
197     return null;
198   }
199
200   /**
201    * Checks if the given directory is an hg root.
202    */
203   public static boolean isHgRoot(@Nullable VirtualFile dir) {
204     return dir != null && dir.findChild(DOT_HG) != null;
205   }
206
207   /**
208    * Gets the Mercurial root for the given file path or null if non exists:
209    * the root should not only be in directory mappings, but also the .hg repository folder should exist.
210    *
211    * @see #getHgRootOrThrow(Project, FilePath)
212    */
213   @Nullable
214   public static VirtualFile getHgRootOrNull(Project project, FilePath filePath) {
215     if (project == null) {
216       return getNearestHgRoot(VcsUtil.getVirtualFile(filePath.getPath()));
217     }
218     return getNearestHgRoot(VcsUtil.getVcsRootFor(project, filePath));
219   }
220
221   /**
222    * Get hg roots for paths
223    *
224    * @param filePaths the context paths
225    * @return a set of hg roots
226    */
227   @NotNull
228   public static Set<VirtualFile> hgRoots(@NotNull Project project, @NotNull Collection<FilePath> filePaths) {
229     HashSet<VirtualFile> roots = new HashSet<>();
230     for (FilePath path : filePaths) {
231       ContainerUtil.addIfNotNull(roots, getHgRootOrNull(project, path));
232     }
233     return roots;
234   }
235
236   /**
237    * Gets the Mercurial root for the given file path or null if non exists:
238    * the root should not only be in directory mappings, but also the .hg repository folder should exist.
239    * @see #getHgRootOrThrow(Project, FilePath)
240    * @see #getHgRootOrNull(Project, FilePath)
241    */
242   @Nullable
243   public static VirtualFile getHgRootOrNull(Project project, @NotNull VirtualFile file) {
244     return getHgRootOrNull(project, VcsUtil.getFilePath(file.getPath()));
245   }
246
247   /**
248    * Gets the Mercurial root for the given file path or throws a VcsException if non exists:
249    * the root should not only be in directory mappings, but also the .hg repository folder should exist.
250    * @see #getHgRootOrNull(Project, FilePath)
251    */
252   @NotNull
253   public static VirtualFile getHgRootOrThrow(Project project, FilePath filePath) throws VcsException {
254     final VirtualFile vf = getHgRootOrNull(project, filePath);
255     if (vf == null) {
256       throw new VcsException(HgVcsMessages.message("hg4idea.exception.file.not.under.hg", filePath.getPresentableUrl()));
257     }
258     return vf;
259   }
260
261   @NotNull
262   public static VirtualFile getHgRootOrThrow(Project project, VirtualFile file) throws VcsException {
263     return getHgRootOrThrow(project, VcsUtil.getFilePath(file.getPath()));
264   }
265
266   /**
267    * Shows a message dialog to enter the name of new branch.
268    *
269    * @return name of new branch or {@code null} if user has cancelled the dialog.
270    */
271   @Nullable
272   public static String getNewBranchNameFromUser(@NotNull HgRepository repository,
273                                                 @NotNull String dialogTitle) {
274     return Messages.showInputDialog(repository.getProject(), "Enter the name of new branch:", dialogTitle, Messages.getQuestionIcon(), "",
275                                     new HgBranchReferenceValidator(repository));
276   }
277
278   /**
279    * Checks is a merge operation is in progress on the given repository.
280    * Actually gets the number of parents of the current revision. If there are 2 parents, then a merge is going on. Otherwise there is
281    * only one parent.
282    * @param project    project to work on.
283    * @param repository repository which is checked on merge.
284    * @return True if merge operation is in progress, false if there is no merge operation.
285    */
286   public static boolean isMergeInProgress(@NotNull Project project, VirtualFile repository) {
287     return new HgWorkingCopyRevisionsCommand(project).parents(repository).size() > 1;
288   }
289   /**
290    * Groups the given files by their Mercurial repositories and returns the map of relative paths to files for each repository.
291    * @param hgFiles files to be grouped.
292    * @return key is repository, values is the non-empty list of relative paths to files, which belong to this repository.
293    */
294   @NotNull
295   public static Map<VirtualFile, List<String>> getRelativePathsByRepository(Collection<HgFile> hgFiles) {
296     final Map<VirtualFile, List<String>> map = new HashMap<>();
297     if (hgFiles == null) {
298       return map;
299     }
300     for(HgFile file : hgFiles) {
301       final VirtualFile repo = file.getRepo();
302       List<String> files = map.get(repo);
303       if (files == null) {
304         files = new ArrayList<>();
305         map.put(repo, files);
306       }
307       files.add(file.getRelativePath());
308     }
309     return map;
310   }
311
312   @NotNull
313   public static HgFile getFileNameInTargetRevision(Project project, HgRevisionNumber vcsRevisionNumber, HgFile localHgFile) {
314     //get file name in target revision if it was moved/renamed
315     // if file was moved but not committed then hg status would return nothing, so it's better to point working dir as '.' revision
316     HgStatusCommand statCommand = new HgStatusCommand.Builder(false).copySource(true).baseRevision(vcsRevisionNumber).
317       targetRevision(HgRevisionNumber.getInstance("", ".")).build(project);
318
319     Set<HgChange> changes = statCommand.executeInCurrentThread(localHgFile.getRepo(), Collections.singletonList(localHgFile.toFilePath()));
320
321     for (HgChange change : changes) {
322       if (change.afterFile().equals(localHgFile)) {
323         return change.beforeFile();
324       }
325     }
326     return localHgFile;
327   }
328
329   @NotNull
330   public static FilePath getOriginalFileName(@NotNull FilePath filePath, ChangeListManager changeListManager) {
331     Change change = changeListManager.getChange(filePath);
332     if (change == null) {
333       return filePath;
334     }
335
336     FileStatus status = change.getFileStatus();
337     if (status == HgChangeProvider.COPIED ||
338         status == HgChangeProvider.RENAMED) {
339       ContentRevision beforeRevision = change.getBeforeRevision();
340       assert beforeRevision != null : "If a file's status is copied or renamed, there must be an previous version";
341       return beforeRevision.getFile();
342     }
343     else {
344       return filePath;
345     }
346   }
347
348   @NotNull
349   public static Map<VirtualFile, Collection<VirtualFile>> sortByHgRoots(@NotNull Project project, @NotNull Collection<VirtualFile> files) {
350     Map<VirtualFile, Collection<VirtualFile>> sorted = new HashMap<>();
351     HgRepositoryManager repositoryManager = getRepositoryManager(project);
352     for (VirtualFile file : files) {
353       HgRepository repo = repositoryManager.getRepositoryForFile(file);
354       if (repo == null) {
355         continue;
356       }
357       Collection<VirtualFile> filesForRoot = sorted.get(repo.getRoot());
358       if (filesForRoot == null) {
359         filesForRoot = new HashSet<>();
360         sorted.put(repo.getRoot(), filesForRoot);
361       }
362       filesForRoot.add(file);
363     }
364     return sorted;
365   }
366
367   @NotNull
368   public static Map<VirtualFile, Collection<FilePath>> groupFilePathsByHgRoots(@NotNull Project project,
369                                                                                @NotNull Collection<FilePath> files) {
370     Map<VirtualFile, Collection<FilePath>> sorted = new HashMap<>();
371     if (project.isDisposed()) return sorted;
372     HgRepositoryManager repositoryManager = getRepositoryManager(project);
373     for (FilePath file : files) {
374       HgRepository repo = repositoryManager.getRepositoryForFile(file);
375       if (repo == null) {
376         continue;
377       }
378       Collection<FilePath> filesForRoot = sorted.get(repo.getRoot());
379       if (filesForRoot == null) {
380         filesForRoot = new HashSet<>();
381         sorted.put(repo.getRoot(), filesForRoot);
382       }
383       filesForRoot.add(file);
384     }
385     return sorted;
386   }
387
388   @NotNull
389   public static ProgressIndicator executeOnPooledThread(@NotNull Runnable runnable, @NotNull Disposable parentDisposable) {
390     return BackgroundTaskUtil.executeOnPooledThread(runnable, parentDisposable);
391   }
392
393   /**
394    * Convert {@link VcsVirtualFile} to the {@link LocalFileSystem local} Virtual File.
395    *
396    * TODO
397    * It is a workaround for the following problem: VcsVirtualFiles returned from the {@link FileHistoryPanelImpl} contain the current path
398    * of the file, not the path that was in certain revision. This has to be fixed by making {@link HgFileRevision} implement
399    * {@link VcsFileRevisionEx}.
400    */
401   @Nullable
402   public static VirtualFile convertToLocalVirtualFile(@Nullable VirtualFile file) {
403     if (!(file instanceof AbstractVcsVirtualFile)) {
404       return file;
405     }
406     LocalFileSystem lfs = LocalFileSystem.getInstance();
407     VirtualFile resultFile = lfs.findFileByPath(file.getPath());
408     if (resultFile == null) {
409       resultFile = lfs.refreshAndFindFileByPath(file.getPath());
410     }
411     return resultFile;
412   }
413
414   @NotNull
415   public static List<Change> getDiff(@NotNull final Project project,
416                                      @NotNull final VirtualFile root,
417                                      @NotNull final FilePath path,
418                                      @Nullable final HgRevisionNumber revNum1,
419                                      @Nullable final HgRevisionNumber revNum2) {
420     HgStatusCommand statusCommand;
421     if (revNum1 != null) {
422       //rev2==null means "compare with local version"
423       statusCommand = new HgStatusCommand.Builder(true).ignored(false).unknown(false).copySource(!path.isDirectory()).baseRevision(revNum1)
424         .targetRevision(revNum2).build(project);
425     }
426     else {
427       LOG.assertTrue(revNum2 != null, "revision1 and revision2 can't both be null. Path: " + path); //rev1 and rev2 can't be null both//
428       //get initial changes//
429       statusCommand =
430         new HgStatusCommand.Builder(true).ignored(false).unknown(false).copySource(false).baseRevision(revNum2)
431           .build(project);
432     }
433
434     Collection<HgChange> hgChanges = statusCommand.executeInCurrentThread(root, Collections.singleton(path));
435     List<Change> changes = new ArrayList<>();
436     //convert output changes to standard Change class
437     for (HgChange hgChange : hgChanges) {
438       FileStatus status = convertHgDiffStatus(hgChange.getStatus());
439       if (status != FileStatus.UNKNOWN) {
440         changes.add(HgHistoryUtil.createChange(project, root, hgChange.beforeFile().getRelativePath(), revNum1,
441                                                hgChange.afterFile().getRelativePath(), revNum2, status));
442       }
443     }
444     return changes;
445   }
446
447   @NotNull
448   public static FileStatus convertHgDiffStatus(@NotNull HgFileStatusEnum hgstatus) {
449     if (hgstatus.equals(HgFileStatusEnum.ADDED)) {
450       return FileStatus.ADDED;
451     }
452     else if (hgstatus.equals(HgFileStatusEnum.DELETED)) {
453       return FileStatus.DELETED;
454     }
455     else if (hgstatus.equals(HgFileStatusEnum.MODIFIED)) {
456       return FileStatus.MODIFIED;
457     }
458     else if (hgstatus.equals(HgFileStatusEnum.COPY)) {
459       return HgChangeProvider.COPIED;
460     }
461     else if (hgstatus.equals(HgFileStatusEnum.UNVERSIONED)) {
462       return FileStatus.UNKNOWN;
463     }
464     else if (hgstatus.equals(HgFileStatusEnum.IGNORED)) {
465       return FileStatus.IGNORED;
466     }
467     else {
468       return FileStatus.UNKNOWN;
469     }
470   }
471
472   @NotNull
473   public static byte[] loadContent(@NotNull Project project, @Nullable HgRevisionNumber revisionNumber, @NotNull HgFile fileToCat) {
474     HgCommandResult result = new HgCatCommand(project).execute(fileToCat, revisionNumber, fileToCat.toFilePath().getCharset());
475     return result != null && result.getExitValue() == 0 ? result.getBytesOutput() : ArrayUtil.EMPTY_BYTE_ARRAY;
476   }
477
478   public static String removePasswordIfNeeded(@NotNull String path) {
479     Matcher matcher = URL_WITH_PASSWORD.matcher(path);
480     if (matcher.matches()) {
481       return path.substring(0, matcher.start(1)) + path.substring(matcher.end(1), path.length());
482     }
483     return path;
484   }
485
486   @NotNull
487   public static String getDisplayableBranchOrBookmarkText(@NotNull HgRepository repository) {
488     HgRepository.State state = repository.getState();
489     String branchText = "";
490     if (state != HgRepository.State.NORMAL) {
491       branchText += state.toString() + " ";
492     }
493     return branchText + repository.getCurrentBranchName();
494   }
495
496   @NotNull
497   public static HgRepositoryManager getRepositoryManager(@NotNull Project project) {
498     return ServiceManager.getService(project, HgRepositoryManager.class);
499   }
500
501   @Nullable
502   @CalledInAwt
503   public static HgRepository getCurrentRepository(@NotNull Project project) {
504     if (project.isDisposed()) return null;
505     return DvcsUtil.guessRepositoryForFile(project, getRepositoryManager(project),
506                                            DvcsUtil.getSelectedFile(project),
507                                            HgProjectSettings.getInstance(project).getRecentRootPath());
508   }
509
510   @Nullable
511   public static HgRepository getRepositoryForFile(@NotNull Project project, @Nullable VirtualFile file) {
512     if (file == null || project.isDisposed()) return null;
513
514     HgRepositoryManager repositoryManager = getRepositoryManager(project);
515     VirtualFile root = getHgRootOrNull(project, file);
516     return repositoryManager.getRepositoryForRoot(root);
517   }
518
519   @Nullable
520   public static String getRepositoryDefaultPath(@NotNull Project project, @NotNull VirtualFile root) {
521     HgRepository hgRepository = getRepositoryManager(project).getRepositoryForRoot(root);
522     assert hgRepository != null : "Repository can't be null for root " + root.getName();
523     return hgRepository.getRepositoryConfig().getDefaultPath();
524   }
525
526   @Nullable
527   public static String getRepositoryDefaultPushPath(@NotNull Project project, @NotNull VirtualFile root) {
528     HgRepository hgRepository = getRepositoryManager(project).getRepositoryForRoot(root);
529     assert hgRepository != null : "Repository can't be null for root " + root.getName();
530     return hgRepository.getRepositoryConfig().getDefaultPushPath();
531   }
532
533   @Nullable
534   public static String getRepositoryDefaultPushPath(@NotNull HgRepository repository) {
535     return repository.getRepositoryConfig().getDefaultPushPath();
536   }
537
538   @Nullable
539   public static String getConfig(@NotNull Project project,
540                                  @NotNull VirtualFile root,
541                                  @NotNull String section,
542                                  @Nullable String configName) {
543     HgRepository hgRepository = getRepositoryManager(project).getRepositoryForRoot(root);
544     assert hgRepository != null : "Repository can't be null for root " + root.getName();
545     return hgRepository.getRepositoryConfig().getNamedConfig(section, configName);
546   }
547
548   @NotNull
549   public static Collection<String> getRepositoryPaths(@NotNull Project project,
550                                                       @NotNull VirtualFile root) {
551     HgRepository hgRepository = getRepositoryManager(project).getRepositoryForRoot(root);
552     assert hgRepository != null : "Repository can't be null for root " + root.getName();
553     return hgRepository.getRepositoryConfig().getPaths();
554   }
555
556   public static boolean isExecutableValid(@Nullable String executable) {
557     try {
558       if (StringUtil.isEmptyOrSpaces(executable)) {
559         return false;
560       }
561       HgCommandResult result = getVersionOutput(executable);
562       return result.getExitValue() == 0 && !result.getRawOutput().isEmpty();
563     }
564     catch (Throwable e) {
565       LOG.info("Error during hg executable validation: ", e);
566       return false;
567     }
568   }
569
570   @NotNull
571   public static HgCommandResult getVersionOutput(@NotNull String executable) throws ShellCommandException, InterruptedException {
572     String hgExecutable = executable.trim();
573     List<String> cmdArgs = new ArrayList<>();
574     cmdArgs.add(hgExecutable);
575     cmdArgs.add("version");
576     cmdArgs.add("-q");
577     ShellCommand shellCommand = new ShellCommand(cmdArgs, null, CharsetToolkit.getDefaultSystemCharset());
578     return shellCommand.execute(false, false);
579   }
580
581   public static List<String> getNamesWithoutHashes(Collection<HgNameWithHashInfo> namesWithHashes) {
582     //return names without duplication (actually for several heads in one branch)
583     List<String> names = new ArrayList<>();
584     for (HgNameWithHashInfo hash : namesWithHashes) {
585       if (!names.contains(hash.getName())) {
586         names.add(hash.getName());
587       }
588     }
589     return names;
590   }
591
592   public static List<String> getSortedNamesWithoutHashes(Collection<HgNameWithHashInfo> namesWithHashes) {
593     List<String> names = getNamesWithoutHashes(namesWithHashes);
594     Collections.sort(names);
595     return names;
596   }
597
598   @NotNull
599   public static Couple<String> parseUserNameAndEmail(@NotNull String authorString) {
600     //special characters should be retained for properly filtering by username. For Mercurial "a.b" username is not equal to "a b"
601     // Vasya Pupkin <vasya.pupkin@jetbrains.com> -> Vasya Pupkin , vasya.pupkin@jetbrains.com
602     int startEmailIndex = authorString.indexOf('<');
603     int startDomainIndex = authorString.indexOf('@');
604     int endEmailIndex = authorString.indexOf('>');
605     String userName;
606     String email;
607     if (0 < startEmailIndex && startEmailIndex < startDomainIndex && startDomainIndex < endEmailIndex) {
608       email = authorString.substring(startEmailIndex + 1, endEmailIndex);
609       userName = authorString.substring(0, startEmailIndex).trim();
610     }
611     // vasya.pupkin@email.com || <vasya.pupkin@email.com>
612     else if (!authorString.contains(" ") && startDomainIndex > 0) { //simple e-mail check. john@localhost
613       userName = "";
614       if (startEmailIndex >= 0 && startDomainIndex > startEmailIndex && startDomainIndex < endEmailIndex) {
615         email = authorString.substring(startEmailIndex + 1, endEmailIndex).trim();
616       } else {
617         email = authorString;
618       }
619     }
620
621     else {
622       userName = authorString.trim();
623       email = "";
624     }
625     return Couple.of(userName, email);
626   }
627
628   @NotNull
629   public static List<String> getTargetNames(@NotNull HgRepository repository) {
630     return ContainerUtil.<String>sorted(ContainerUtil.map(repository.getRepositoryConfig().getPaths(), s -> removePasswordIfNeeded(s)));
631   }
632 }