// Copyright 2010 Victor Iacoban // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software distributed under // the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, // either express or implied. See the License for the specific language governing permissions and // limitations under the License. package org.zmlx.hg4idea.util; import com.intellij.dvcs.DvcsUtil; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.TextEditor; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; import com.intellij.openapi.util.Couple; import com.intellij.openapi.util.ShutDownTracker; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.*; import com.intellij.openapi.vcs.changes.*; import com.intellij.openapi.vcs.history.FileHistoryPanelImpl; import com.intellij.openapi.vcs.history.VcsFileRevisionEx; import com.intellij.openapi.vcs.vfs.AbstractVcsVirtualFile; import com.intellij.openapi.vcs.vfs.VcsVirtualFile; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.StatusBar; import com.intellij.openapi.wm.WindowManager; import com.intellij.openapi.wm.impl.status.StatusBarUtil; import com.intellij.ui.GuiUtils; import com.intellij.util.containers.ContainerUtil; import com.intellij.vcsUtil.VcsUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.zmlx.hg4idea.*; import org.zmlx.hg4idea.command.HgRemoveCommand; import org.zmlx.hg4idea.command.HgStatusCommand; import org.zmlx.hg4idea.command.HgWorkingCopyRevisionsCommand; import org.zmlx.hg4idea.execution.HgCommandResult; import org.zmlx.hg4idea.execution.ShellCommand; import org.zmlx.hg4idea.execution.ShellCommandException; import org.zmlx.hg4idea.provider.HgChangeProvider; import org.zmlx.hg4idea.repo.HgRepository; import org.zmlx.hg4idea.repo.HgRepositoryManager; import java.awt.*; import java.io.*; import java.lang.reflect.InvocationTargetException; import java.util.*; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * HgUtil is a collection of static utility methods for Mercurial. */ public abstract class HgUtil { public static final Pattern URL_WITH_PASSWORD = Pattern.compile("(?:.+)://(?:.+)(:.+)@(?:.+)"); //http(s)://username:password@url public static final int MANY_FILES = 100; private static final Logger LOG = Logger.getInstance(HgUtil.class); public static final String DOT_HG = ".hg"; public static final String TIP_REFERENCE = "tip"; public static final String HEAD_REFERENCE = "HEAD"; public static File copyResourceToTempFile(String basename, String extension) throws IOException { final InputStream in = HgUtil.class.getClassLoader().getResourceAsStream("python/" + basename + extension); final File tempFile = FileUtil.createTempFile(basename, extension); final byte[] buffer = new byte[4096]; OutputStream out = null; try { out = new FileOutputStream(tempFile, false); int bytesRead; while ((bytesRead = in.read(buffer)) != -1) out.write(buffer, 0, bytesRead); } finally { try { out.close(); } catch (IOException e) { // ignore } } try { in.close(); } catch (IOException e) { // ignore } tempFile.deleteOnExit(); return tempFile; } public static void markDirectoryDirty(final Project project, final VirtualFile file) throws InvocationTargetException, InterruptedException { ApplicationManager.getApplication().runReadAction(new Runnable() { public void run() { VcsDirtyScopeManager.getInstance(project).dirDirtyRecursively(file); } }); runWriteActionAndWait(new Runnable() { public void run() { file.refresh(true, true); } }); } public static void markFileDirty( final Project project, final VirtualFile file ) throws InvocationTargetException, InterruptedException { ApplicationManager.getApplication().runReadAction(new Runnable() { public void run() { VcsDirtyScopeManager.getInstance(project).fileDirty(file); } } ); runWriteActionAndWait(new Runnable() { public void run() { file.refresh(true, false); } }); } /** * Runs the given task as a write action in the event dispatching thread and waits for its completion. */ public static void runWriteActionAndWait(@NotNull final Runnable runnable) throws InvocationTargetException, InterruptedException { GuiUtils.runOrInvokeAndWait(new Runnable() { public void run() { ApplicationManager.getApplication().runWriteAction(runnable); } }); } /** * Schedules the given task to be run as a write action in the event dispatching thread. */ public static void runWriteActionLater(@NotNull final Runnable runnable) { ApplicationManager.getApplication().invokeLater(new Runnable() { public void run() { ApplicationManager.getApplication().runWriteAction(runnable); } }); } /** * Returns a temporary python file that will be deleted on exit. * * Also all compiled version of the python file will be deleted. * * @param base The basename of the file to copy * @return The temporary copy the specified python file, with all the necessary hooks installed * to make sure it is completely removed at shutdown */ @Nullable public static File getTemporaryPythonFile(String base) { try { final File file = copyResourceToTempFile(base, ".py"); final String fileName = file.getName(); ShutDownTracker.getInstance().registerShutdownTask(new Runnable() { public void run() { File[] files = file.getParentFile().listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { return name.startsWith(fileName); } }); if (files != null) { for (File file1 : files) { file1.delete(); } } } }); return file; } catch (IOException e) { return null; } } /** * Calls 'hg remove' to remove given files from the VCS. * @param project * @param files files to be removed from the VCS. */ public static void removeFilesFromVcs(Project project, List files) { final HgRemoveCommand command = new HgRemoveCommand(project); for (FilePath filePath : files) { final VirtualFile vcsRoot = VcsUtil.getVcsRootFor(project, filePath); if (vcsRoot == null) { continue; } command.execute(new HgFile(vcsRoot, filePath)); } } /** * Finds the nearest parent directory which is an hg root. * @param dir Directory which parent will be checked. * @return Directory which is the nearest hg root being a parent of this directory, * or null if this directory is not under hg. * @see com.intellij.openapi.vcs.AbstractVcs#isVersionedDirectory(com.intellij.openapi.vfs.VirtualFile) */ @Nullable public static VirtualFile getNearestHgRoot(VirtualFile dir) { VirtualFile currentDir = dir; while (currentDir != null) { if (isHgRoot(currentDir)) { return currentDir; } currentDir = currentDir.getParent(); } return null; } /** * Checks if the given directory is an hg root. */ public static boolean isHgRoot(VirtualFile dir) { return dir.findChild(DOT_HG) != null; } /** * Gets the Mercurial root for the given file path or null if non exists: * the root should not only be in directory mappings, but also the .hg repository folder should exist. * @see #getHgRootOrThrow(com.intellij.openapi.project.Project, com.intellij.openapi.vcs.FilePath) */ @Nullable public static VirtualFile getHgRootOrNull(Project project, FilePath filePath) { if (project == null) { return getNearestHgRoot(VcsUtil.getVirtualFile(filePath.getPath())); } return getNearestHgRoot(VcsUtil.getVcsRootFor(project, filePath)); } /** * Get hg roots for paths * * @param filePaths the context paths * @return a set of hg roots */ @NotNull public static Set hgRoots(@NotNull Project project, @NotNull Collection filePaths) { HashSet roots = new HashSet(); for (FilePath path : filePaths) { ContainerUtil.addIfNotNull(roots, getHgRootOrNull(project, path)); } return roots; } /** * Gets the Mercurial root for the given file path or null if non exists: * the root should not only be in directory mappings, but also the .hg repository folder should exist. * @see #getHgRootOrThrow(com.intellij.openapi.project.Project, com.intellij.openapi.vcs.FilePath) * @see #getHgRootOrNull(com.intellij.openapi.project.Project, com.intellij.openapi.vcs.FilePath) */ @Nullable public static VirtualFile getHgRootOrNull(Project project, @NotNull VirtualFile file) { return getHgRootOrNull(project, VcsUtil.getFilePath(file.getPath())); } /** * Gets the Mercurial root for the given file path or throws a VcsException if non exists: * the root should not only be in directory mappings, but also the .hg repository folder should exist. * @see #getHgRootOrNull(com.intellij.openapi.project.Project, com.intellij.openapi.vcs.FilePath) */ @NotNull public static VirtualFile getHgRootOrThrow(Project project, FilePath filePath) throws VcsException { final VirtualFile vf = getHgRootOrNull(project, filePath); if (vf == null) { throw new VcsException(HgVcsMessages.message("hg4idea.exception.file.not.under.hg", filePath.getPresentableUrl())); } return vf; } @NotNull public static VirtualFile getHgRootOrThrow(Project project, VirtualFile file) throws VcsException { return getHgRootOrThrow(project, VcsUtil.getFilePath(file.getPath())); } /** * Returns the currently selected file, based on which HgBranch components will identify the current repository root. */ @Nullable public static VirtualFile getSelectedFile(@NotNull Project project) { StatusBar statusBar = WindowManager.getInstance().getStatusBar(project); final FileEditor fileEditor = StatusBarUtil.getCurrentFileEditor(project, statusBar); VirtualFile result = null; if (fileEditor != null) { if (fileEditor instanceof TextEditor) { Document document = ((TextEditor)fileEditor).getEditor().getDocument(); result = FileDocumentManager.getInstance().getFile(document); } } if (result == null) { final FileEditorManager manager = FileEditorManager.getInstance(project); if (manager != null) { Editor editor = manager.getSelectedTextEditor(); if (editor != null) { result = FileDocumentManager.getInstance().getFile(editor.getDocument()); } } } return result; } @Nullable public static VirtualFile getRootForSelectedFile(@NotNull Project project) { VirtualFile selectedFile = getSelectedFile(project); if (selectedFile != null) { return getHgRootOrNull(project, selectedFile); } return null; } /** * Shows a message dialog to enter the name of new branch. * * @return name of new branch or {@code null} if user has cancelled the dialog. */ @Nullable public static String getNewBranchNameFromUser(@NotNull HgRepository repository, @NotNull String dialogTitle) { return Messages.showInputDialog(repository.getProject(), "Enter the name of new branch:", dialogTitle, Messages.getQuestionIcon(), "", HgReferenceValidator.newInstance(repository)); } /** * Checks is a merge operation is in progress on the given repository. * Actually gets the number of parents of the current revision. If there are 2 parents, then a merge is going on. Otherwise there is * only one parent. * @param project project to work on. * @param repository repository which is checked on merge. * @return True if merge operation is in progress, false if there is no merge operation. */ public static boolean isMergeInProgress(@NotNull Project project, VirtualFile repository) { return new HgWorkingCopyRevisionsCommand(project).parents(repository).size() > 1; } /** * Groups the given files by their Mercurial repositories and returns the map of relative paths to files for each repository. * @param hgFiles files to be grouped. * @return key is repository, values is the non-empty list of relative paths to files, which belong to this repository. */ @NotNull public static Map> getRelativePathsByRepository(Collection hgFiles) { final Map> map = new HashMap>(); if (hgFiles == null) { return map; } for(HgFile file : hgFiles) { final VirtualFile repo = file.getRepo(); List files = map.get(repo); if (files == null) { files = new ArrayList(); map.put(repo, files); } files.add(file.getRelativePath()); } return map; } @NotNull public static HgFile getFileNameInTargetRevision(Project project, HgRevisionNumber vcsRevisionNumber, HgFile localHgFile) { //get file name in target revision if it was moved/renamed HgStatusCommand statCommand = new HgStatusCommand.Builder(false).copySource(true).baseRevision(vcsRevisionNumber).build(project); Set changes = statCommand.execute(localHgFile.getRepo(), Collections.singletonList(localHgFile.toFilePath())); for (HgChange change : changes) { if (change.afterFile().equals(localHgFile)) { return change.beforeFile(); } } return localHgFile; } @NotNull public static FilePath getOriginalFileName(@NotNull FilePath filePath, ChangeListManager changeListManager) { Change change = changeListManager.getChange(filePath); if (change == null) { return filePath; } FileStatus status = change.getFileStatus(); if (status == HgChangeProvider.COPIED || status == HgChangeProvider.RENAMED) { ContentRevision beforeRevision = change.getBeforeRevision(); assert beforeRevision != null : "If a file's status is copied or renamed, there must be an previous version"; return beforeRevision.getFile(); } else { return filePath; } } /** * Returns all HG roots in the project. */ public static @NotNull List getHgRepositories(@NotNull Project project) { final List repos = new LinkedList(); for (VcsRoot root : ProjectLevelVcsManager.getInstance(project).getAllVcsRoots()) { if (HgVcs.VCS_NAME.equals(root.getVcs().getName())) { repos.add(root.getPath()); } } return repos; } @NotNull public static Map> sortByHgRoots(@NotNull Project project, @NotNull Collection files) { Map> sorted = new HashMap>(); HgRepositoryManager repositoryManager = getRepositoryManager(project); for (VirtualFile file : files) { HgRepository repo = repositoryManager.getRepositoryForFile(file); if (repo == null) { continue; } Collection filesForRoot = sorted.get(repo.getRoot()); if (filesForRoot == null) { filesForRoot = new HashSet(); sorted.put(repo.getRoot(), filesForRoot); } filesForRoot.add(file); } return sorted; } @NotNull public static Map> groupFilePathsByHgRoots(@NotNull Project project, @NotNull Collection files) { Map> sorted = new HashMap>(); HgRepositoryManager repositoryManager = getRepositoryManager(project); for (FilePath file : files) { HgRepository repo = repositoryManager.getRepositoryForFile(file); if (repo == null) { continue; } Collection filesForRoot = sorted.get(repo.getRoot()); if (filesForRoot == null) { filesForRoot = new HashSet(); sorted.put(repo.getRoot(), filesForRoot); } filesForRoot.add(file); } return sorted; } public static void executeOnPooledThreadIfNeeded(Runnable runnable) { if (EventQueue.isDispatchThread()) { ApplicationManager.getApplication().executeOnPooledThread(runnable); } else { runnable.run(); } } /** * Convert {@link VcsVirtualFile} to the {@link LocalFileSystem local} Virtual File. * * TODO * It is a workaround for the following problem: VcsVirtualFiles returned from the {@link FileHistoryPanelImpl} contain the current path * of the file, not the path that was in certain revision. This has to be fixed by making {@link HgFileRevision} implement * {@link VcsFileRevisionEx}. */ @Nullable public static VirtualFile convertToLocalVirtualFile(@Nullable VirtualFile file) { if (!(file instanceof AbstractVcsVirtualFile)) { return file; } LocalFileSystem lfs = LocalFileSystem.getInstance(); VirtualFile resultFile = lfs.findFileByPath(file.getPath()); if (resultFile == null) { resultFile = lfs.refreshAndFindFileByPath(file.getPath()); } return resultFile; } @NotNull public static List getDiff(@NotNull final Project project, @NotNull final VirtualFile root, @NotNull final FilePath path, @Nullable final HgFileRevision rev1, @Nullable final HgFileRevision rev2) { HgStatusCommand statusCommand; HgRevisionNumber revNumber1 = null; if (rev1 != null) { revNumber1 = rev1.getRevisionNumber(); //rev2==null means "compare with local version" statusCommand = new HgStatusCommand.Builder(true).ignored(false).unknown(false).copySource(false).baseRevision(revNumber1) .targetRevision(rev2 != null ? rev2.getRevisionNumber() : null).build(project); } else { LOG.assertTrue(rev2 != null, "revision1 and revision2 can't both be null. Path: " + path); //rev1 and rev2 can't be null both// //get initial changes// statusCommand = new HgStatusCommand.Builder(true).ignored(false).unknown(false).copySource(false).baseRevision(rev2.getRevisionNumber()) .build(project); } Collection hgChanges = statusCommand.execute(root, Collections.singleton(path)); List changes = new ArrayList(); //convert output changes to standart Change class for (HgChange hgChange : hgChanges) { FileStatus status = convertHgDiffStatus(hgChange.getStatus()); if (status != FileStatus.UNKNOWN) { changes.add(createChange(project, root, hgChange.beforeFile().getRelativePath(), revNumber1, hgChange.afterFile().getRelativePath(), rev2 != null ? rev2.getRevisionNumber() : null, status)); } } return changes; } @NotNull public static Change createChange(@NotNull final Project project, VirtualFile root, @NotNull String fileBefore, @Nullable HgRevisionNumber revisionBefore, @NotNull String fileAfter, @Nullable HgRevisionNumber revisionAfter, @NotNull FileStatus aStatus) { HgContentRevision beforeRevision = revisionBefore == null ? null : new HgContentRevision(project, new HgFile(root, new File(root.getPath(), fileBefore)), revisionBefore); if (revisionAfter == null) { ContentRevision currentRevision = CurrentContentRevision.create(new HgFile(root, new File(root.getPath(), fileBefore)).toFilePath()); return new Change(beforeRevision, currentRevision, aStatus); } HgContentRevision afterRevision = new HgContentRevision(project, new HgFile(root, new File(root.getPath(), fileAfter)), revisionAfter); return new Change(beforeRevision, afterRevision, aStatus); } @NotNull public static FileStatus convertHgDiffStatus(@NotNull HgFileStatusEnum hgstatus) { if (hgstatus.equals(HgFileStatusEnum.ADDED)) { return FileStatus.ADDED; } else if (hgstatus.equals(HgFileStatusEnum.DELETED)) { return FileStatus.DELETED; } else if (hgstatus.equals(HgFileStatusEnum.MODIFIED)) { return FileStatus.MODIFIED; } else if (hgstatus.equals(HgFileStatusEnum.COPY)) { return HgChangeProvider.COPIED; } else if (hgstatus.equals(HgFileStatusEnum.UNVERSIONED)) { return FileStatus.UNKNOWN; } else if (hgstatus.equals(HgFileStatusEnum.IGNORED)) { return FileStatus.IGNORED; } else { return FileStatus.UNKNOWN; } } public static String removePasswordIfNeeded(@NotNull String path) { Matcher matcher = URL_WITH_PASSWORD.matcher(path); if (matcher.matches()) { return path.substring(0, matcher.start(1)) + path.substring(matcher.end(1), path.length()); } return path; } @NotNull public static String getDisplayableBranchOrBookmarkText(@NotNull HgRepository repository) { HgRepository.State state = repository.getState(); String branchText = ""; if (state != HgRepository.State.NORMAL) { branchText += state.toString() + " "; } return branchText + getActiveBranchName(repository); } @NotNull public static String getActiveBranchName(@NotNull HgRepository repository) { String branchOrBookMarkName = repository.getCurrentBookmark(); if (StringUtil.isEmptyOrSpaces(branchOrBookMarkName)) { branchOrBookMarkName = repository.getCurrentBranch(); } return branchOrBookMarkName; } @NotNull public static HgRepositoryManager getRepositoryManager(@NotNull Project project) { return ServiceManager.getService(project, HgRepositoryManager.class); } @Nullable public static HgRepository getCurrentRepository(@NotNull Project project) { VirtualFile file = DvcsUtil.getSelectedFile(project); return getRepositoryForFile(project, file); } @Nullable public static HgRepository getRepositoryForFile(@NotNull Project project, @Nullable VirtualFile file) { if (file == null) { return null; } HgRepositoryManager repositoryManager = getRepositoryManager(project); VirtualFile root = getHgRootOrNull(project, file); return repositoryManager.getRepositoryForRoot(root); } @Nullable public static String getRepositoryDefaultPath(@NotNull Project project, @NotNull VirtualFile root) { HgRepository hgRepository = getRepositoryManager(project).getRepositoryForRoot(root); assert hgRepository != null : "Repository can't be null for root " + root.getName(); return hgRepository.getRepositoryConfig().getDefaultPath(); } @Nullable public static String getRepositoryDefaultPushPath(@NotNull Project project, @NotNull VirtualFile root) { HgRepository hgRepository = getRepositoryManager(project).getRepositoryForRoot(root); assert hgRepository != null : "Repository can't be null for root " + root.getName(); return hgRepository.getRepositoryConfig().getDefaultPushPath(); } @Nullable public static String getRepositoryDefaultPushPath(@NotNull HgRepository repository) { return repository.getRepositoryConfig().getDefaultPushPath(); } @Nullable public static String getConfig(@NotNull Project project, @NotNull VirtualFile root, @NotNull String section, @Nullable String configName) { HgRepository hgRepository = getRepositoryManager(project).getRepositoryForRoot(root); assert hgRepository != null : "Repository can't be null for root " + root.getName(); return hgRepository.getRepositoryConfig().getNamedConfig(section, configName); } @NotNull public static Collection getRepositoryPaths(@NotNull Project project, @NotNull VirtualFile root) { HgRepository hgRepository = getRepositoryManager(project).getRepositoryForRoot(root); assert hgRepository != null : "Repository can't be null for root " + root.getName(); return hgRepository.getRepositoryConfig().getPaths(); } public static boolean isExecutableValid(@Nullable String executable) { try { if (StringUtil.isEmptyOrSpaces(executable)) { return false; } HgCommandResult result = getVersionOutput(executable); return result.getExitValue() == 0 && !result.getRawOutput().isEmpty(); } catch (Throwable e) { LOG.info("Error during hg executable validation: ", e); return false; } } @NotNull public static HgCommandResult getVersionOutput(@NotNull String executable) throws ShellCommandException, InterruptedException { String hgExecutable = executable.trim(); List cmdArgs = new ArrayList(); cmdArgs.add(hgExecutable); cmdArgs.add("version"); cmdArgs.add("-q"); ShellCommand shellCommand = new ShellCommand(cmdArgs, null, CharsetToolkit.getDefaultSystemCharset()); return shellCommand.execute(false); } public static List getNamesWithoutHashes(Collection namesWithHashes) { //return names without duplication (actually for several heads in one branch) List names = new ArrayList(); for (HgNameWithHashInfo hash : namesWithHashes) { if (!names.contains(hash.getName())) { names.add(hash.getName()); } } Collections.sort(names); return names; } @NotNull public static Couple parseUserNameAndEmail(@NotNull String authorString) { //special characters should be retained for properly filtering by username. For Mercurial "a.b" username is not equal to "a b" // Vasya Pupkin -> Vasya Pupkin , vasya.pupkin@jetbrains.com int startEmailIndex = authorString.indexOf('<'); int startDomainIndex = authorString.indexOf('@'); int endEmailIndex = authorString.indexOf('>'); String userName; String email; if (0 < startEmailIndex && startEmailIndex < startDomainIndex && startDomainIndex < endEmailIndex) { email = authorString.substring(startEmailIndex + 1, endEmailIndex); userName = authorString.substring(0, startEmailIndex).trim(); } // vasya.pupkin@email.com --> vasya.pupkin, vasya.pupkin@email.com else if (!authorString.contains(" ") && startDomainIndex > 0) { //simple e-mail check. john@localhost userName = authorString.substring(0, startDomainIndex).trim(); email = authorString; } else { userName = authorString.trim(); email = ""; } return Couple.of(userName, email); } }