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@114: import static org.tmatesoft.hg.internal.RequiresFile.*; tikhomirov@74: tikhomirov@202: import java.io.File; tikhomirov@202: import java.io.FileOutputStream; 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@295: import org.tmatesoft.hg.repo.HgInternals; 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@74: private int requiresFlags = 0; tikhomirov@114: private List filterFactories; tikhomirov@411: private final SessionContext sessionContext; tikhomirov@388: private final boolean isCaseSensitiveFileSystem; tikhomirov@388: private final boolean shallCacheRevlogsInRepo; tikhomirov@114: tikhomirov@388: public Internals(SessionContext ctx) { tikhomirov@411: this.sessionContext = ctx; tikhomirov@388: isCaseSensitiveFileSystem = !runningOnWindows(); tikhomirov@388: Object p = ctx.getProperty(CFG_PROPERTY_REVLOG_STREAM_CACHE, true); tikhomirov@388: shallCacheRevlogsInRepo = p instanceof Boolean ? ((Boolean) p).booleanValue() : Boolean.parseBoolean(String.valueOf(p)); tikhomirov@114: } tikhomirov@295: tikhomirov@295: public void parseRequires(HgRepository hgRepo, File requiresFile) { tikhomirov@295: try { tikhomirov@295: new RequiresFile().parse(this, requiresFile); tikhomirov@295: } catch (IOException ex) { tikhomirov@295: // FIXME not quite sure error reading requires file shall be silently logged only. tikhomirov@295: HgInternals.getContext(hgRepo).getLog().error(getClass(), ex, null); tikhomirov@295: } tikhomirov@295: } tikhomirov@74: tikhomirov@83: public/*for tests, otherwise pkg*/ void setStorageConfig(int version, int flags) { tikhomirov@74: requiresFlags = flags; tikhomirov@74: } tikhomirov@388: 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@74: // XXX perhaps, should keep both fields right here, not in the HgRepository tikhomirov@74: public PathRewrite buildDataFilesHelper() { tikhomirov@412: // Note, tests in TestStorePath depend on the encoding not being cached tikhomirov@412: Charset cs = getFileEncoding(); tikhomirov@412: // StoragePathHelper needs fine-grained control over char encoding, hence doesn't use EncodingHelper tikhomirov@411: return new StoragePathHelper((requiresFlags & STORE) != 0, (requiresFlags & FNCACHE) != 0, (requiresFlags & DOTENCODE) != 0, cs); tikhomirov@74: } tikhomirov@74: tikhomirov@74: public PathRewrite buildRepositoryFilesHelper() { tikhomirov@74: if ((requiresFlags & STORE) != 0) { tikhomirov@74: return new PathRewrite() { tikhomirov@292: public CharSequence rewrite(CharSequence path) { tikhomirov@74: return "store/" + path; tikhomirov@74: } tikhomirov@74: }; tikhomirov@74: } else { tikhomirov@292: return new PathRewrite.Empty(); tikhomirov@74: } tikhomirov@74: } tikhomirov@114: tikhomirov@331: public List getFilters(HgRepository hgRepo) { tikhomirov@114: if (filterFactories == null) { tikhomirov@114: filterFactories = new ArrayList(); tikhomirov@331: ExtensionsSection cfg = hgRepo.getConfiguration().getExtensions(); tikhomirov@331: if (cfg.isEnabled("eol")) { tikhomirov@114: NewlineFilter.Factory ff = new NewlineFilter.Factory(); tikhomirov@331: ff.initialize(hgRepo); tikhomirov@114: filterFactories.add(ff); tikhomirov@114: } tikhomirov@331: if (cfg.isEnabled("keyword")) { tikhomirov@114: KeywordFilter.Factory ff = new KeywordFilter.Factory(); tikhomirov@331: ff.initialize(hgRepo); tikhomirov@114: filterFactories.add(ff); tikhomirov@114: } tikhomirov@114: } tikhomirov@114: return filterFactories; tikhomirov@114: } tikhomirov@202: tikhomirov@202: public void initEmptyRepository(File hgDir) throws IOException { tikhomirov@202: hgDir.mkdir(); tikhomirov@202: FileOutputStream requiresFile = new FileOutputStream(new File(hgDir, "requires")); tikhomirov@202: StringBuilder sb = new StringBuilder(40); tikhomirov@202: sb.append("revlogv1\n"); tikhomirov@202: if ((requiresFlags & STORE) != 0) { tikhomirov@202: sb.append("store\n"); tikhomirov@202: } tikhomirov@202: if ((requiresFlags & FNCACHE) != 0) { tikhomirov@202: sb.append("fncache\n"); tikhomirov@202: } tikhomirov@202: if ((requiresFlags & DOTENCODE) != 0) { tikhomirov@202: sb.append("dotencode\n"); tikhomirov@202: } tikhomirov@202: requiresFile.write(sb.toString().getBytes()); tikhomirov@202: requiresFile.close(); tikhomirov@202: new File(hgDir, "store").mkdir(); // with that, hg verify says ok. tikhomirov@202: } tikhomirov@388: tikhomirov@388: public boolean isCaseSensitiveFileSystem() { tikhomirov@388: return isCaseSensitiveFileSystem; tikhomirov@388: } tikhomirov@412: tikhomirov@412: public EncodingHelper buildFileNameEncodingHelper() { tikhomirov@415: return new EncodingHelper(getFileEncoding(), sessionContext); tikhomirov@412: } tikhomirov@412: tikhomirov@412: private Charset getFileEncoding() { tikhomirov@412: Object altEncoding = sessionContext.getProperty(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@412: sessionContext.getLog().error(Internals.class, 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@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@382: String p = (String) ctx.getProperty(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@331: public ConfigFile readConfiguration(HgRepository hgRepo, File repoRoot) throws IOException { tikhomirov@331: ConfigFile configFile = new ConfigFile(); tikhomirov@382: File hgInstallRoot = findHgInstallRoot(HgInternals.getContext(hgRepo)); // 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@331: configFile.addLocation(new File(repoRoot, "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: }