Mercurial > jhg
view src/org/tmatesoft/hg/internal/Internals.java @ 667:fba85bc1dfb8
Refactoring: move all encoding/decoding operations into single place, EncodingHelper
author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
---|---|
date | Thu, 11 Jul 2013 17:54:08 +0200 |
parents | 46b56864b483 |
children | b4242b7e7dfe |
line wrap: on
line source
/* * Copyright (c) 2011-2013 TMate Software Ltd * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * For information on how to redistribute this software under * the terms of a license other than GNU General Public License * contact TMate Software at support@hg4j.com */ package org.tmatesoft.hg.internal; import static org.tmatesoft.hg.util.LogFacility.Severity.Error; import java.io.File; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.StringTokenizer; import org.tmatesoft.hg.core.HgIOException; import org.tmatesoft.hg.core.SessionContext; import org.tmatesoft.hg.repo.HgDataFile; import org.tmatesoft.hg.repo.HgInternals; import org.tmatesoft.hg.repo.HgRepoConfig.ExtensionsSection; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.repo.HgRepositoryFiles; import org.tmatesoft.hg.repo.HgRepositoryLock; import org.tmatesoft.hg.repo.HgRuntimeException; import org.tmatesoft.hg.util.LogFacility; import org.tmatesoft.hg.util.Path; import org.tmatesoft.hg.util.PathRewrite; /** * Fields/members that shall not be visible * * @author Artem Tikhomirov * @author TMate Software Ltd. */ public final class Internals implements SessionContext.Source { /** * Allows to specify Mercurial installation directory to detect installation-wide configurations. * Without this property set, hg4j would attempt to deduce this value locating hg executable. */ public static final String CFG_PROPERTY_HG_INSTALL_ROOT = "hg4j.hg.install_root"; /** * Tells repository not to cache files/revlogs * XXX perhaps, need to respect this property not only for data files, but for manifest and changelog as well? * (@see HgRepository#getChangelog and #getManifest()) */ public static final String CFG_PROPERTY_REVLOG_STREAM_CACHE = "hg4j.repo.disable_revlog_cache"; /** * Name of charset to use when translating Unicode filenames to Mercurial storage paths, string, * to resolve with {@link Charset#forName(String)}. * E.g. <code>"cp1251"</code> or <code>"Latin-1"</code>. * * <p>Mercurial uses system encoding when mangling storage paths. Default value * based on 'file.encoding' Java system property is usually fine here, however * in certain scenarios it may be desirable to force a different one, and this * property is exactly for this purpose. * * <p>E.g. Eclipse defaults to project encoding (Launch config, Common page) when launching an application, * and if your project happen to use anything but filesystem default (say, UTF8 on cp1251 system), * native storage paths won't match */ public static final String CFG_PROPERTY_FS_FILENAME_ENCODING = "hg.fs.filename.encoding"; /** * Timeout, in seconds, to acquire filesystem {@link HgRepositoryLock lock}. * * Mercurial provides 'ui.timeout' in hgrc (defaults to 600 seconds) to specify how long * it shall try to acquire a lock for storage or working directory prior to fail. * * This configuration property allows to override timeout value from Mercurial's configuration * file and use Hg4J-specific value instead. * * Integer value, use negative for attempts to acquire lock until success, and zero to try once and fail immediately. */ public static final String CFG_PROPERTY_FS_LOCK_TIMEOUT = "hg4j.fs.lock.timeout"; /** * Alternative, more effective approach to build revision text from revlog patches - collect all the * patches one by one, starting at revision next to base, and apply against each other to get * one final patch, which in turned is applied to base revision. * <p> * Original approach is to apply each patch to a previous revision, so that with base revision * of 1M and three patches, each altering just a tiny fraction * of the origin, with latter approach we consume 1M (original) + 1M (first patch applied) + 1M (second * patch applied) + 1M (third patch applied). * <p> * Alternative approach, controlled with this option, first combines these there patches into one, * and only then applies it to base revision, eliminating 2 intermediate elements. * <p> * Since 1.2, default value for this option is <em>TRUE</em>, (was <code>false</code> in <b>Hg4J 1.1</b>) * * @since 1.1 */ public static final String CFG_PROPERTY_PATCH_MERGE = "hg4j.repo.merge_revlog_patches"; /** * Phases were introduced in Mercurial 2.1. Unless there's <code>phaseroots</code> file in the * repository's storage area, <b>Hg4J</b> pretends phases are not enabled and doesn't update * phase information on commit/push/pull. If, however, it's desired to keep phase information, * this option may be set to <code>true</code>, and <code>phaseroots</code> file gets updated * along with repository changes. * * <p>Default value: <code>false</code> * @since 1.2 */ public static final String CFG_PROPERTY_CREATE_PHASEROOTS = "hg4j.repo.create_phaseroots"; public static final int REVLOGV1_RECORD_SIZE = 64; private List<Filter.Factory> filterFactories; private final HgRepository repo; private final File repoDir; private final boolean isCaseSensitiveFileSystem; private final DataAccessProvider dataAccess; private final ImplAccess implAccess; private final int requiresFlags; private final PathRewrite dataPathHelper; // access to file storage area (usually under .hg/store/data/), with filenames mangled private final PathRewrite repoPathHelper; // access to system files (under .hg/store if requires has 'store' flag) private final boolean shallMergePatches; private final boolean shallWritePhaseroots; private final RevlogStreamFactory streamProvider; public Internals(HgRepository hgRepo, File hgDir, ImplAccess implementationAccess) throws HgRuntimeException { repo = hgRepo; repoDir = hgDir; implAccess = implementationAccess; isCaseSensitiveFileSystem = !runningOnWindows(); SessionContext ctx = repo.getSessionContext(); dataAccess = new DataAccessProvider(ctx); RepoInitializer repoInit = new RepoInitializer().initRequiresFromFile(repoDir); requiresFlags = repoInit.getRequires(); dataPathHelper = repoInit.buildDataFilesHelper(getSessionContext()); repoPathHelper = repoInit.buildStoreFilesHelper(); final PropertyMarshal pm = new PropertyMarshal(ctx); boolean shallCacheRevlogsInRepo = pm.getBoolean(CFG_PROPERTY_REVLOG_STREAM_CACHE, true); streamProvider = new RevlogStreamFactory(this, shallCacheRevlogsInRepo); shallMergePatches = pm.getBoolean(Internals.CFG_PROPERTY_PATCH_MERGE, true); shallWritePhaseroots = pm.getBoolean(Internals.CFG_PROPERTY_CREATE_PHASEROOTS, false); } public boolean isInvalid() { return !repoDir.exists() || !repoDir.isDirectory(); } public File getRepositoryFile(HgRepositoryFiles f) { switch (f.getHome()) { case Store : return getFileFromStoreDir(f.getName()); case Repo : return getFileFromRepoDir(f.getName()); default : return new File(repo.getWorkingDir(), f.getName()); } } /** * Access files under ".hg/". * File not necessarily exists, this method is merely a factory for {@link File files} at specific, configuration-dependent location. * * @param name shall be normalized path */ public File getFileFromRepoDir(String name) { return new File(repoDir, name); } /** * Access files under ".hg/store/" or ".hg/" depending on use of 'store' in requires. * File not necessarily exists, this method is merely a factory for Files at specific, configuration-dependent location. * * @param name shall be normalized path */ public File getFileFromStoreDir(String name) { CharSequence location = repoPathHelper.rewrite(name); return new File(repoDir, location.toString()); } /** * Access files under ".hg/store/data", ".hg/store/dh/" or ".hg/data" according to settings in requires file. * File not necessarily exists, this method is merely a factory for Files at specific, configuration-dependent location. * * @param name shall be normalized path, without .i or .d suffixes */ public File getFileFromDataDir(CharSequence path) { CharSequence storagePath = dataPathHelper.rewrite(path); return new File(repoDir, storagePath.toString()); } public SessionContext getSessionContext() { return repo.getSessionContext(); } public LogFacility getLog() { return getSessionContext().getLog(); } public HgRepository getRepo() { return repo; } public DataAccessProvider getDataAccess() { return dataAccess; } public PathRewrite buildNormalizePathRewrite() { if (runningOnWindows()) { return new WinToNixPathRewrite(); } else { return new PathRewrite.Empty(); // or strip leading slash, perhaps? } } public List<Filter.Factory> getFilters() { if (filterFactories == null) { filterFactories = new ArrayList<Filter.Factory>(); ExtensionsSection cfg = repo.getConfiguration().getExtensions(); if (cfg.isEnabled("eol")) { NewlineFilter.Factory ff = new NewlineFilter.Factory(); ff.initialize(repo); filterFactories.add(ff); } if (cfg.isEnabled("keyword")) { KeywordFilter.Factory ff = new KeywordFilter.Factory(); ff.initialize(repo); filterFactories.add(ff); } } return filterFactories; } public boolean isCaseSensitiveFileSystem() { return isCaseSensitiveFileSystem; } public boolean fncacheInUse() { return (getRequiresFlags() & RequiresFile.FNCACHE) != 0; } public EncodingHelper buildFileNameEncodingHelper() { return new EncodingHelper(getFileEncoding(getSessionContext()), repo); } public static EncodingHelper buildFileNameEncodingHelper(SessionContext.Source ctx) { return new EncodingHelper(getFileEncoding(ctx.getSessionContext()), ctx); } /*package-local*/ static Charset getFileEncoding(SessionContext ctx) { Object altEncoding = ctx.getConfigurationProperty(CFG_PROPERTY_FS_FILENAME_ENCODING, null); Charset cs; if (altEncoding == null) { cs = Charset.defaultCharset(); } else { try { cs = Charset.forName(altEncoding.toString()); } catch (IllegalArgumentException ex) { // both IllegalCharsetNameException and UnsupportedCharsetException are subclasses of IAE, too // not severe enough to throw an exception, imo. Just record the fact it's bad ad we ignore it ctx.getLog().dump(Internals.class, Error, ex, String.format("Bad configuration value for filename encoding %s", altEncoding)); cs = Charset.defaultCharset(); } } return cs; } /** * Access to mangled name of a file in repository storage, may come handy for debug. * @return mangled path of the repository file */ public CharSequence getStoragePath(HgDataFile df) { return dataPathHelper.rewrite(df.getPath().toString()); } public int getRequiresFlags() { return requiresFlags; } boolean shallMergePatches() { return shallMergePatches; } boolean shallCreatePhaseroots() { return shallWritePhaseroots; } RevlogChangeMonitor getRevlogTracker(File f) { // TODO decide whether to use one monitor per multiple files or // an instance per file; and let SessionContext pass alternative implementation) return new RevlogChangeMonitor(f); } public static boolean runningOnWindows() { return System.getProperty("os.name").indexOf("Windows") != -1; } /** * @param fsHint optional hint pointing to filesystem of interest (generally, it's possible to mount * filesystems with different capabilities and repository's capabilities would depend on which fs it resides) * @return <code>true</code> if executable files deserve tailored handling */ public static boolean checkSupportsExecutables(File fsHint) { // *.exe are not executables for Mercurial return !runningOnWindows(); } /** * @param fsHint optional hint pointing to filesystem of interest (generally, it's possible to mount * filesystems with different capabilities and repository's capabilities would depend on which fs it resides) * @return <code>true</code> if filesystem knows what symbolic links are */ public static boolean checkSupportsSymlinks(File fsHint) { // Windows supports soft symbolic links starting from Vista // However, as of Mercurial 2.1.1, no support for this functionality // XXX perhaps, makes sense to override with a property a) to speed up when no links are in use b) investigate how this runs windows return !runningOnWindows(); } /** * For Unix, returns installation root, which is the parent directory of the hg executable (or symlink) being run. * For Windows, it's Mercurial installation directory itself * @param ctx */ private static File findHgInstallRoot(SessionContext ctx) { // let clients to override Hg install location String p = (String) ctx.getConfigurationProperty(CFG_PROPERTY_HG_INSTALL_ROOT, null); if (p != null) { return new File(p); } StringTokenizer st = new StringTokenizer(System.getenv("PATH"), System.getProperty("path.separator"), false); final boolean runsOnWin = runningOnWindows(); while (st.hasMoreTokens()) { String pe = st.nextToken(); File execCandidate = new File(pe, runsOnWin ? "hg.exe" : "hg"); if (execCandidate.exists() && execCandidate.isFile()) { File execDir = execCandidate.getParentFile(); // e.g. on Unix runs "/shared/tools/bin/hg", directory of interest is "/shared/tools/" return runsOnWin ? execDir : execDir.getParentFile(); } } return null; } /** * User-specific configuration, from system-wide and user home locations, without any repository-specific data. * @see http://www.selenic.com/mercurial/hgrc.5.html */ public static ConfigFile readConfiguration(SessionContext sessionCtx) throws HgIOException { ConfigFile configFile = new ConfigFile(sessionCtx); File hgInstallRoot = findHgInstallRoot(sessionCtx); // may be null // if (runningOnWindows()) { if (hgInstallRoot != null) { for (File f : getWindowsConfigFilesPerInstall(hgInstallRoot)) { configFile.addLocation(f); } } LinkedHashSet<String> locations = new LinkedHashSet<String>(); locations.add(System.getenv("USERPROFILE")); locations.add(System.getenv("HOME")); locations.remove(null); for (String loc : locations) { File location = new File(loc); configFile.addLocation(new File(location, "Mercurial.ini")); configFile.addLocation(new File(location, ".hgrc")); } } else { if (hgInstallRoot != null) { File d = new File(hgInstallRoot, "etc/mercurial/hgrc.d/"); if (d.isDirectory() && d.canRead()) { for (File f : listConfigFiles(d)) { configFile.addLocation(f); } } configFile.addLocation(new File(hgInstallRoot, "etc/mercurial/hgrc")); } // same, but with absolute paths File d = new File("/etc/mercurial/hgrc.d/"); if (d.isDirectory() && d.canRead()) { for (File f : listConfigFiles(d)) { configFile.addLocation(f); } } configFile.addLocation(new File("/etc/mercurial/hgrc")); configFile.addLocation(new File(System.getenv("HOME"), ".hgrc")); } return configFile; } /** * Repository-specific configuration * @see http://www.selenic.com/mercurial/hgrc.5.html */ public ConfigFile readConfiguration() throws HgIOException { ConfigFile configFile = readConfiguration(repo.getSessionContext()); // last one, overrides anything else // <repo>/.hg/hgrc configFile.addLocation(getFileFromRepoDir("hgrc")); return configFile; } /*package-local*/ImplAccess getImplAccess() { return implAccess; } private static List<File> getWindowsConfigFilesPerInstall(File hgInstallDir) { File f = new File(hgInstallDir, "Mercurial.ini"); if (f.exists()) { return Collections.singletonList(f); } f = new File(hgInstallDir, "hgrc.d/"); if (f.canRead() && f.isDirectory()) { return listConfigFiles(f); } // TODO [post-1.1] query registry, e.g. with // Runtime.exec("reg query HKLM\Software\Mercurial") // f = new File("C:\\Mercurial\\Mercurial.ini"); if (f.exists()) { return Collections.singletonList(f); } return Collections.emptyList(); } private static List<File> listConfigFiles(File dir) { assert dir.canRead(); assert dir.isDirectory(); final File[] allFiles = dir.listFiles(); // File is Comparable, lexicographically by default Arrays.sort(allFiles); ArrayList<File> rv = new ArrayList<File>(allFiles.length); for (File f : allFiles) { if (f.getName().endsWith(".rc")) { rv.add(f); } } return rv; } public static File getInstallationConfigurationFileToWrite(SessionContext ctx) { File hgInstallRoot = findHgInstallRoot(ctx); // may be null // choice of which hgrc to pick here is according to my own pure discretion if (hgInstallRoot != null) { // use this location only if it's writable File cfg = new File(hgInstallRoot, runningOnWindows() ? "Mercurial.ini" : "etc/mercurial/hgrc"); if (cfg.canWrite() || cfg.getParentFile().canWrite()) { return cfg; } } // fallback if (runningOnWindows()) { if (hgInstallRoot == null) { return new File("C:\\Mercurial\\Mercurial.ini"); } else { // yes, we tried this file already (above) and found it non-writable // let caller fail with can't write return new File(hgInstallRoot, "Mercurial.ini"); } } else { return new File("/etc/mercurial/hgrc"); } } public static File getUserConfigurationFileToWrite(SessionContext ctx) { LinkedHashSet<String> locations = new LinkedHashSet<String>(); final boolean runsOnWindows = runningOnWindows(); if (runsOnWindows) { locations.add(System.getenv("USERPROFILE")); } locations.add(System.getenv("HOME")); locations.remove(null); for (String loc : locations) { File location = new File(loc); File rv = new File(location, ".hgrc"); if (rv.exists() && rv.canWrite()) { return rv; } if (runsOnWindows) { rv = new File(location, "Mercurial.ini"); if (rv.exists() && rv.canWrite()) { return rv; } } } // fallback to default, let calling code fail with Exception if can't write return new File(System.getProperty("user.home"), ".hgrc"); } public RevlogStream createManifestStream() { File manifestFile = getFileFromStoreDir("00manifest.i"); return streamProvider.create(manifestFile); } public RevlogStream createChangelogStream() { File chlogFile = getFileFromStoreDir("00changelog.i"); return streamProvider.create(chlogFile); } public RevlogStream resolveStoreFile(Path path) { return streamProvider.getStoreFile(path, false); } // marker method public static IllegalStateException notImplemented() { return new IllegalStateException("Not implemented"); } public static Internals getInstance(HgRepository repo) { return HgInternals.getImplementationRepo(repo); } public static <T> CharSequence join(Iterable<T> col, CharSequence separator) { if (col == null) { return String.valueOf(col); } Iterator<T> it = col.iterator(); if (!it.hasNext()) { return "[]"; } String v = String.valueOf(it.next()); StringBuilder sb = new StringBuilder(v); while (it.hasNext()) { sb.append(separator); v = String.valueOf(it.next()); sb.append(v); } return sb; } /** * keep an eye on all long to int downcasts to get a chance notice the lost of data * Use if there's even subtle chance there might be loss * (ok not to use if there's no way for l to be greater than int) */ public static int ltoi(long l) { int i = (int) l; assert ((long) i) == l : "Loss of data!"; return i; } // access implementation details (fields, methods) of oth.repo package public interface ImplAccess { public RevlogStream getStream(HgDataFile df); public RevlogStream getManifestStream(); public RevlogStream getChangelogStream(); } }