tikhomirov@74: /*
tikhomirov@388: * Copyright (c) 2011-2012 TMate Software Ltd
tikhomirov@74: *
tikhomirov@74: * This program is free software; you can redistribute it and/or modify
tikhomirov@74: * it under the terms of the GNU General Public License as published by
tikhomirov@74: * the Free Software Foundation; version 2 of the License.
tikhomirov@74: *
tikhomirov@74: * This program is distributed in the hope that it will be useful,
tikhomirov@74: * but WITHOUT ANY WARRANTY; without even the implied warranty of
tikhomirov@74: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
tikhomirov@74: * GNU General Public License for more details.
tikhomirov@74: *
tikhomirov@74: * For information on how to redistribute this software under
tikhomirov@74: * the terms of a license other than GNU General Public License
tikhomirov@102: * contact TMate Software at support@hg4j.com
tikhomirov@74: */
tikhomirov@74: package org.tmatesoft.hg.internal;
tikhomirov@74:
tikhomirov@456: import static org.tmatesoft.hg.util.LogFacility.Severity.Error;
tikhomirov@74:
tikhomirov@202: import java.io.File;
tikhomirov@202: import java.io.IOException;
tikhomirov@411: import java.nio.charset.Charset;
tikhomirov@114: import java.util.ArrayList;
tikhomirov@379: import java.util.Arrays;
tikhomirov@379: import java.util.Collections;
tikhomirov@407: import java.util.Iterator;
tikhomirov@379: import java.util.LinkedHashSet;
tikhomirov@114: import java.util.List;
tikhomirov@379: import java.util.StringTokenizer;
tikhomirov@114:
tikhomirov@382: import org.tmatesoft.hg.core.SessionContext;
tikhomirov@493: import org.tmatesoft.hg.repo.HgDataFile;
tikhomirov@493: import org.tmatesoft.hg.repo.HgRuntimeException;
tikhomirov@331: import org.tmatesoft.hg.repo.HgRepoConfig.ExtensionsSection;
tikhomirov@114: import org.tmatesoft.hg.repo.HgRepository;
tikhomirov@74: import org.tmatesoft.hg.util.PathRewrite;
tikhomirov@74:
tikhomirov@74: /**
tikhomirov@74: * Fields/members that shall not be visible
tikhomirov@74: *
tikhomirov@74: * @author Artem Tikhomirov
tikhomirov@74: * @author TMate Software Ltd.
tikhomirov@74: */
tikhomirov@388: public final class Internals {
tikhomirov@74:
tikhomirov@388: /**
tikhomirov@388: * Allows to specify Mercurial installation directory to detect installation-wide configurations.
tikhomirov@388: * Without this property set, hg4j would attempt to deduce this value locating hg executable.
tikhomirov@388: */
tikhomirov@379: public static final String CFG_PROPERTY_HG_INSTALL_ROOT = "hg4j.hg.install_root";
tikhomirov@388:
tikhomirov@388: /**
tikhomirov@388: * Tells repository not to cache files/revlogs
tikhomirov@388: * XXX perhaps, need to respect this property not only for data files, but for manifest and changelog as well?
tikhomirov@388: * (@see HgRepository#getChangelog and #getManifest())
tikhomirov@388: */
tikhomirov@388: public static final String CFG_PROPERTY_REVLOG_STREAM_CACHE = "hg4j.repo.disable_revlog_cache";
tikhomirov@379:
tikhomirov@411: /**
tikhomirov@411: * Name of charset to use when translating Unicode filenames to Mercurial storage paths, string,
tikhomirov@411: * to resolve with {@link Charset#forName(String)}.
tikhomirov@411: * E.g. "cp1251"
or "Latin-1"
.
tikhomirov@411: *
tikhomirov@411: *
Mercurial uses system encoding when mangling storage paths. Default value
tikhomirov@411: * based on 'file.encoding' Java system property is usually fine here, however
tikhomirov@411: * in certain scenarios it may be desirable to force a different one, and this
tikhomirov@411: * property is exactly for this purpose.
tikhomirov@411: *
tikhomirov@411: *
E.g. Eclipse defaults to project encoding (Launch config, Common page) when launching an application,
tikhomirov@411: * and if your project happen to use anything but filesystem default (say, UTF8 on cp1251 system),
tikhomirov@411: * native storage paths won't match
tikhomirov@411: */
tikhomirov@412: public static final String CFG_PROPERTY_FS_FILENAME_ENCODING = "hg.fs.filename.encoding";
tikhomirov@411:
tikhomirov@114: private List filterFactories;
tikhomirov@490: private final HgRepository repo;
tikhomirov@490: private final File repoDir;
tikhomirov@388: private final boolean isCaseSensitiveFileSystem;
tikhomirov@388: private final boolean shallCacheRevlogsInRepo;
tikhomirov@490: private final DataAccessProvider dataAccess;
tikhomirov@493:
tikhomirov@493: @SuppressWarnings("unused")
tikhomirov@493: private final int requiresFlags;
tikhomirov@114:
tikhomirov@493: private final PathRewrite dataPathHelper; // access to file storage area (usually under .hg/store/data/), with filenames mangled
tikhomirov@493: private final PathRewrite repoPathHelper; // access to system files (under .hg/store if requires has 'store' flag)
tikhomirov@493:
tikhomirov@493: public Internals(HgRepository hgRepo, File hgDir) throws HgRuntimeException {
tikhomirov@490: repo = hgRepo;
tikhomirov@490: repoDir = hgDir;
tikhomirov@388: isCaseSensitiveFileSystem = !runningOnWindows();
tikhomirov@490: SessionContext ctx = repo.getSessionContext();
tikhomirov@456: shallCacheRevlogsInRepo = new PropertyMarshal(ctx).getBoolean(CFG_PROPERTY_REVLOG_STREAM_CACHE, true);
tikhomirov@490: dataAccess = new DataAccessProvider(ctx);
tikhomirov@493: RepoInitializer repoInit = new RepoInitializer().initRequiresFromFile(repoDir);
tikhomirov@493: requiresFlags = repoInit.getRequires();
tikhomirov@493: dataPathHelper = repoInit.buildDataFilesHelper(getContext());
tikhomirov@493: repoPathHelper = repoInit.buildStoreFilesHelper();
tikhomirov@114: }
tikhomirov@295:
tikhomirov@490: public boolean isInvalid() {
tikhomirov@490: return !repoDir.exists() || !repoDir.isDirectory();
tikhomirov@490: }
tikhomirov@490:
tikhomirov@493: /**
tikhomirov@493: * Access files under ".hg/".
tikhomirov@493: * File not necessarily exists, this method is merely a factory for Files at specific, configuration-dependent location.
tikhomirov@493: *
tikhomirov@493: * @param name shall be normalized path
tikhomirov@493: */
tikhomirov@490: public File getFileFromRepoDir(String name) {
tikhomirov@490: return new File(repoDir, name);
tikhomirov@490: }
tikhomirov@493:
tikhomirov@493: /**
tikhomirov@493: * Access files under ".hg/store/" or ".hg/" depending on use of 'store' in requires.
tikhomirov@493: * File not necessarily exists, this method is merely a factory for Files at specific, configuration-dependent location.
tikhomirov@493: *
tikhomirov@493: * @param name shall be normalized path
tikhomirov@493: */
tikhomirov@493: public File getFileFromStoreDir(String name) {
tikhomirov@493: CharSequence location = repoPathHelper.rewrite(name);
tikhomirov@493: return new File(repoDir, location.toString());
tikhomirov@493: }
tikhomirov@493:
tikhomirov@493: /**
tikhomirov@493: * Access files under ".hg/store/data", ".hg/store/dh/" or ".hg/data" according to settings in requires file.
tikhomirov@493: * File not necessarily exists, this method is merely a factory for Files at specific, configuration-dependent location.
tikhomirov@493: *
tikhomirov@493: * @param name shall be normalized path, without .i or .d suffixes
tikhomirov@493: */
tikhomirov@493: public File getFileFromDataDir(CharSequence path) {
tikhomirov@493: CharSequence storagePath = dataPathHelper.rewrite(path);
tikhomirov@493: return new File(repoDir, storagePath.toString());
tikhomirov@493: }
tikhomirov@490:
tikhomirov@490: public SessionContext getContext() {
tikhomirov@490: return repo.getSessionContext();
tikhomirov@490: }
tikhomirov@490:
tikhomirov@490: public HgRepository getRepo() {
tikhomirov@490: return repo;
tikhomirov@490: }
tikhomirov@490:
tikhomirov@490: public DataAccessProvider getDataAccess() {
tikhomirov@490: return dataAccess;
tikhomirov@490: }
tikhomirov@490:
tikhomirov@388: public PathRewrite buildNormalizePathRewrite() {
tikhomirov@388: if (runningOnWindows()) {
tikhomirov@409: return new WinToNixPathRewrite();
tikhomirov@388: } else {
tikhomirov@388: return new PathRewrite.Empty(); // or strip leading slash, perhaps?
tikhomirov@388: }
tikhomirov@388: }
tikhomirov@74:
tikhomirov@490: public List getFilters() {
tikhomirov@114: if (filterFactories == null) {
tikhomirov@114: filterFactories = new ArrayList();
tikhomirov@490: ExtensionsSection cfg = repo.getConfiguration().getExtensions();
tikhomirov@331: if (cfg.isEnabled("eol")) {
tikhomirov@114: NewlineFilter.Factory ff = new NewlineFilter.Factory();
tikhomirov@490: ff.initialize(repo);
tikhomirov@114: filterFactories.add(ff);
tikhomirov@114: }
tikhomirov@331: if (cfg.isEnabled("keyword")) {
tikhomirov@114: KeywordFilter.Factory ff = new KeywordFilter.Factory();
tikhomirov@490: ff.initialize(repo);
tikhomirov@114: filterFactories.add(ff);
tikhomirov@114: }
tikhomirov@114: }
tikhomirov@114: return filterFactories;
tikhomirov@114: }
tikhomirov@202:
tikhomirov@388: public boolean isCaseSensitiveFileSystem() {
tikhomirov@388: return isCaseSensitiveFileSystem;
tikhomirov@388: }
tikhomirov@412:
tikhomirov@412: public EncodingHelper buildFileNameEncodingHelper() {
tikhomirov@490: SessionContext ctx = repo.getSessionContext();
tikhomirov@490: return new EncodingHelper(getFileEncoding(ctx), ctx);
tikhomirov@412: }
tikhomirov@412:
tikhomirov@490: /*package-local*/ static Charset getFileEncoding(SessionContext ctx) {
tikhomirov@490: Object altEncoding = ctx.getConfigurationProperty(CFG_PROPERTY_FS_FILENAME_ENCODING, null);
tikhomirov@412: Charset cs;
tikhomirov@412: if (altEncoding == null) {
tikhomirov@412: cs = Charset.defaultCharset();
tikhomirov@412: } else {
tikhomirov@412: try {
tikhomirov@412: cs = Charset.forName(altEncoding.toString());
tikhomirov@412: } catch (IllegalArgumentException ex) {
tikhomirov@412: // both IllegalCharsetNameException and UnsupportedCharsetException are subclasses of IAE, too
tikhomirov@412: // not severe enough to throw an exception, imo. Just record the fact it's bad ad we ignore it
tikhomirov@490: ctx.getLog().dump(Internals.class, Error, ex, String.format("Bad configuration value for filename encoding %s", altEncoding));
tikhomirov@412: cs = Charset.defaultCharset();
tikhomirov@412: }
tikhomirov@412: }
tikhomirov@412: return cs;
tikhomirov@412: }
tikhomirov@493:
tikhomirov@493: /**
tikhomirov@493: * Access to mangled name of a file in repository storage, may come handy for debug.
tikhomirov@493: * @return mangled path of the repository file
tikhomirov@493: */
tikhomirov@493: public CharSequence getStoragePath(HgDataFile df) {
tikhomirov@493: return dataPathHelper.rewrite(df.getPath().toString());
tikhomirov@493: }
tikhomirov@493:
tikhomirov@202:
tikhomirov@292: public static boolean runningOnWindows() {
tikhomirov@292: return System.getProperty("os.name").indexOf("Windows") != -1;
tikhomirov@292: }
tikhomirov@379:
tikhomirov@379: /**
tikhomirov@419: * @param fsHint optional hint pointing to filesystem of interest (generally, it's possible to mount
tikhomirov@413: * filesystems with different capabilities and repository's capabilities would depend on which fs it resides)
tikhomirov@413: * @return true
if executable files deserve tailored handling
tikhomirov@413: */
tikhomirov@413: public static boolean checkSupportsExecutables(File fsHint) {
tikhomirov@413: // *.exe are not executables for Mercurial
tikhomirov@413: return !runningOnWindows();
tikhomirov@413: }
tikhomirov@413:
tikhomirov@413: /**
tikhomirov@419: * @param fsHint optional hint pointing to filesystem of interest (generally, it's possible to mount
tikhomirov@413: * filesystems with different capabilities and repository's capabilities would depend on which fs it resides)
tikhomirov@413: * @return true
if filesystem knows what symbolic links are
tikhomirov@413: */
tikhomirov@413: public static boolean checkSupportsSymlinks(File fsHint) {
tikhomirov@413: // Windows supports soft symbolic links starting from Vista
tikhomirov@413: // However, as of Mercurial 2.1.1, no support for this functionality
tikhomirov@413: // 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
tikhomirov@413: return !runningOnWindows();
tikhomirov@413: }
tikhomirov@413:
tikhomirov@413:
tikhomirov@413: /**
tikhomirov@379: * For Unix, returns installation root, which is the parent directory of the hg executable (or symlink) being run.
tikhomirov@379: * For Windows, it's Mercurial installation directory itself
tikhomirov@382: * @param ctx
tikhomirov@379: */
tikhomirov@382: private static File findHgInstallRoot(SessionContext ctx) {
tikhomirov@379: // let clients to override Hg install location
tikhomirov@456: String p = (String) ctx.getConfigurationProperty(CFG_PROPERTY_HG_INSTALL_ROOT, null);
tikhomirov@379: if (p != null) {
tikhomirov@379: return new File(p);
tikhomirov@379: }
tikhomirov@379: StringTokenizer st = new StringTokenizer(System.getenv("PATH"), System.getProperty("path.separator"), false);
tikhomirov@379: final boolean runsOnWin = runningOnWindows();
tikhomirov@379: while (st.hasMoreTokens()) {
tikhomirov@379: String pe = st.nextToken();
tikhomirov@379: File execCandidate = new File(pe, runsOnWin ? "hg.exe" : "hg");
tikhomirov@379: if (execCandidate.exists() && execCandidate.isFile()) {
tikhomirov@379: File execDir = execCandidate.getParentFile();
tikhomirov@379: // e.g. on Unix runs "/shared/tools/bin/hg", directory of interest is "/shared/tools/"
tikhomirov@379: return runsOnWin ? execDir : execDir.getParentFile();
tikhomirov@379: }
tikhomirov@379: }
tikhomirov@379: return null;
tikhomirov@379: }
tikhomirov@379:
tikhomirov@379: /**
tikhomirov@379: * @see http://www.selenic.com/mercurial/hgrc.5.html
tikhomirov@379: */
tikhomirov@490: public ConfigFile readConfiguration() throws IOException {
tikhomirov@490: SessionContext sessionCtx = repo.getSessionContext();
tikhomirov@483: ConfigFile configFile = new ConfigFile(sessionCtx);
tikhomirov@483: File hgInstallRoot = findHgInstallRoot(sessionCtx); // may be null
tikhomirov@379: //
tikhomirov@351: if (runningOnWindows()) {
tikhomirov@379: if (hgInstallRoot != null) {
tikhomirov@379: for (File f : getWindowsConfigFilesPerInstall(hgInstallRoot)) {
tikhomirov@379: configFile.addLocation(f);
tikhomirov@379: }
tikhomirov@379: }
tikhomirov@379: LinkedHashSet locations = new LinkedHashSet();
tikhomirov@379: locations.add(System.getenv("USERPROFILE"));
tikhomirov@379: locations.add(System.getenv("HOME"));
tikhomirov@379: locations.remove(null);
tikhomirov@379: for (String loc : locations) {
tikhomirov@379: File location = new File(loc);
tikhomirov@379: configFile.addLocation(new File(location, "Mercurial.ini"));
tikhomirov@379: configFile.addLocation(new File(location, ".hgrc"));
tikhomirov@379: }
tikhomirov@351: } else {
tikhomirov@379: if (hgInstallRoot != null) {
tikhomirov@379: File d = new File(hgInstallRoot, "etc/mercurial/hgrc.d/");
tikhomirov@379: if (d.isDirectory() && d.canRead()) {
tikhomirov@379: for (File f : listConfigFiles(d)) {
tikhomirov@351: configFile.addLocation(f);
tikhomirov@351: }
tikhomirov@351: }
tikhomirov@379: configFile.addLocation(new File(hgInstallRoot, "etc/mercurial/hgrc"));
tikhomirov@379: }
tikhomirov@379: // same, but with absolute paths
tikhomirov@379: File d = new File("/etc/mercurial/hgrc.d/");
tikhomirov@379: if (d.isDirectory() && d.canRead()) {
tikhomirov@379: for (File f : listConfigFiles(d)) {
tikhomirov@379: configFile.addLocation(f);
tikhomirov@379: }
tikhomirov@351: }
tikhomirov@351: configFile.addLocation(new File("/etc/mercurial/hgrc"));
tikhomirov@379: configFile.addLocation(new File(System.getenv("HOME"), ".hgrc"));
tikhomirov@351: }
tikhomirov@331: // last one, overrides anything else
tikhomirov@331: // /.hg/hgrc
tikhomirov@490: configFile.addLocation(getFileFromRepoDir("hgrc"));
tikhomirov@331: return configFile;
tikhomirov@331: }
tikhomirov@379:
tikhomirov@379: private static List getWindowsConfigFilesPerInstall(File hgInstallDir) {
tikhomirov@379: File f = new File(hgInstallDir, "Mercurial.ini");
tikhomirov@379: if (f.exists()) {
tikhomirov@379: return Collections.singletonList(f);
tikhomirov@379: }
tikhomirov@379: f = new File(hgInstallDir, "hgrc.d/");
tikhomirov@379: if (f.canRead() && f.isDirectory()) {
tikhomirov@379: return listConfigFiles(f);
tikhomirov@379: }
tikhomirov@418: // TODO post-1.0 query registry, e.g. with
tikhomirov@379: // Runtime.exec("reg query HKLM\Software\Mercurial")
tikhomirov@379: //
tikhomirov@379: f = new File("C:\\Mercurial\\Mercurial.ini");
tikhomirov@379: if (f.exists()) {
tikhomirov@379: return Collections.singletonList(f);
tikhomirov@379: }
tikhomirov@379: return Collections.emptyList();
tikhomirov@379: }
tikhomirov@379:
tikhomirov@379: private static List listConfigFiles(File dir) {
tikhomirov@379: assert dir.canRead();
tikhomirov@379: assert dir.isDirectory();
tikhomirov@379: final File[] allFiles = dir.listFiles();
tikhomirov@379: // File is Comparable, lexicographically by default
tikhomirov@379: Arrays.sort(allFiles);
tikhomirov@379: ArrayList rv = new ArrayList(allFiles.length);
tikhomirov@379: for (File f : allFiles) {
tikhomirov@379: if (f.getName().endsWith(".rc")) {
tikhomirov@379: rv.add(f);
tikhomirov@379: }
tikhomirov@379: }
tikhomirov@379: return rv;
tikhomirov@379: }
tikhomirov@379:
tikhomirov@382: public static File getInstallationConfigurationFileToWrite(SessionContext ctx) {
tikhomirov@382: File hgInstallRoot = findHgInstallRoot(ctx); // may be null
tikhomirov@379: // choice of which hgrc to pick here is according to my own pure discretion
tikhomirov@379: if (hgInstallRoot != null) {
tikhomirov@379: // use this location only if it's writable
tikhomirov@379: File cfg = new File(hgInstallRoot, runningOnWindows() ? "Mercurial.ini" : "etc/mercurial/hgrc");
tikhomirov@379: if (cfg.canWrite() || cfg.getParentFile().canWrite()) {
tikhomirov@379: return cfg;
tikhomirov@379: }
tikhomirov@379: }
tikhomirov@379: // fallback
tikhomirov@379: if (runningOnWindows()) {
tikhomirov@379: if (hgInstallRoot == null) {
tikhomirov@379: return new File("C:\\Mercurial\\Mercurial.ini");
tikhomirov@379: } else {
tikhomirov@379: // yes, we tried this file already (above) and found it non-writable
tikhomirov@379: // let caller fail with can't write
tikhomirov@379: return new File(hgInstallRoot, "Mercurial.ini");
tikhomirov@379: }
tikhomirov@379: } else {
tikhomirov@379: return new File("/etc/mercurial/hgrc");
tikhomirov@379: }
tikhomirov@378: }
tikhomirov@378:
tikhomirov@382: public static File getUserConfigurationFileToWrite(SessionContext ctx) {
tikhomirov@379: LinkedHashSet locations = new LinkedHashSet();
tikhomirov@379: final boolean runsOnWindows = runningOnWindows();
tikhomirov@379: if (runsOnWindows) {
tikhomirov@379: locations.add(System.getenv("USERPROFILE"));
tikhomirov@378: }
tikhomirov@379: locations.add(System.getenv("HOME"));
tikhomirov@379: locations.remove(null);
tikhomirov@379: for (String loc : locations) {
tikhomirov@379: File location = new File(loc);
tikhomirov@379: File rv = new File(location, ".hgrc");
tikhomirov@379: if (rv.exists() && rv.canWrite()) {
tikhomirov@379: return rv;
tikhomirov@379: }
tikhomirov@379: if (runsOnWindows) {
tikhomirov@379: rv = new File(location, "Mercurial.ini");
tikhomirov@379: if (rv.exists() && rv.canWrite()) {
tikhomirov@379: return rv;
tikhomirov@379: }
tikhomirov@378: }
tikhomirov@378: }
tikhomirov@379: // fallback to default, let calling code fail with Exception if can't write
tikhomirov@379: return new File(System.getProperty("user.home"), ".hgrc");
tikhomirov@378: }
tikhomirov@388:
tikhomirov@388: public boolean shallCacheRevlogs() {
tikhomirov@388: return shallCacheRevlogsInRepo;
tikhomirov@388: }
tikhomirov@407:
tikhomirov@407: public static CharSequence join(Iterable col, CharSequence separator) {
tikhomirov@407: if (col == null) {
tikhomirov@407: return String.valueOf(col);
tikhomirov@407: }
tikhomirov@407: Iterator it = col.iterator();
tikhomirov@407: if (!it.hasNext()) {
tikhomirov@407: return "[]";
tikhomirov@407: }
tikhomirov@407: String v = String.valueOf(it.next());
tikhomirov@407: StringBuilder sb = new StringBuilder(v);
tikhomirov@407: while (it.hasNext()) {
tikhomirov@407: sb.append(separator);
tikhomirov@407: v = String.valueOf(it.next());
tikhomirov@407: sb.append(v);
tikhomirov@407: }
tikhomirov@407: return sb;
tikhomirov@407: }
tikhomirov@420:
tikhomirov@420: /**
tikhomirov@420: * keep an eye on all long to int downcasts to get a chance notice the lost of data
tikhomirov@420: * Use if there's even subtle chance there might be loss
tikhomirov@420: * (ok not to use if there's no way for l to be greater than int)
tikhomirov@420: */
tikhomirov@420: public static int ltoi(long l) {
tikhomirov@420: int i = (int) l;
tikhomirov@420: assert ((long) i) == l : "Loss of data!";
tikhomirov@420: return i;
tikhomirov@420: }
tikhomirov@74: }