kitaev@213: /* kitaev@213: * Copyright (c) 2010-2011 TMate Software Ltd kitaev@213: * kitaev@213: * This program is free software; you can redistribute it and/or modify kitaev@213: * it under the terms of the GNU General Public License as published by kitaev@213: * the Free Software Foundation; version 2 of the License. kitaev@213: * kitaev@213: * This program is distributed in the hope that it will be useful, kitaev@213: * but WITHOUT ANY WARRANTY; without even the implied warranty of kitaev@213: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the kitaev@213: * GNU General Public License for more details. kitaev@213: * kitaev@213: * For information on how to redistribute this software under kitaev@213: * the terms of a license other than GNU General Public License kitaev@213: * contact TMate Software at support@hg4j.com kitaev@213: */ kitaev@213: package org.tmatesoft.hg.repo; kitaev@213: kitaev@213: import java.io.File; kitaev@213: import java.io.IOException; kitaev@213: import java.lang.ref.SoftReference; kitaev@213: import java.util.ArrayList; kitaev@213: import java.util.Collections; kitaev@213: import java.util.HashMap; kitaev@213: import java.util.List; kitaev@213: kitaev@213: import org.tmatesoft.hg.internal.ConfigFile; kitaev@213: import org.tmatesoft.hg.internal.DataAccessProvider; kitaev@213: import org.tmatesoft.hg.internal.Filter; kitaev@213: import org.tmatesoft.hg.internal.RelativePathRewrite; kitaev@213: import org.tmatesoft.hg.internal.RequiresFile; kitaev@213: import org.tmatesoft.hg.internal.RevlogStream; kitaev@213: import org.tmatesoft.hg.util.FileIterator; kitaev@213: import org.tmatesoft.hg.util.FileWalker; kitaev@213: import org.tmatesoft.hg.util.Path; kitaev@213: import org.tmatesoft.hg.util.PathRewrite; kitaev@213: kitaev@213: kitaev@213: kitaev@213: /** kitaev@213: * Shall be as state-less as possible, all the caching happens outside the repo, in commands/walkers kitaev@213: * kitaev@213: * @author Artem Tikhomirov kitaev@213: * @author TMate Software Ltd. kitaev@213: */ kitaev@213: public final class HgRepository { kitaev@213: kitaev@213: // if new constants added, consider fixing HgInternals#wrongLocalRevision kitaev@213: public static final int TIP = -3; kitaev@213: public static final int BAD_REVISION = Integer.MIN_VALUE; kitaev@213: public static final int WORKING_COPY = -2; kitaev@213: kitaev@213: // temp aux marker method kitaev@213: public static IllegalStateException notImplemented() { kitaev@213: return new IllegalStateException("Not implemented"); kitaev@213: } kitaev@213: kitaev@213: private final File repoDir; // .hg folder kitaev@213: private final String repoLocation; kitaev@213: private final DataAccessProvider dataAccess; kitaev@213: private final PathRewrite normalizePath; kitaev@213: private final PathRewrite dataPathHelper; kitaev@213: private final PathRewrite repoPathHelper; kitaev@213: kitaev@213: private HgChangelog changelog; kitaev@213: private HgManifest manifest; kitaev@213: private HgTags tags; kitaev@213: // XXX perhaps, shall enable caching explicitly kitaev@213: private final HashMap> streamsCache = new HashMap>(); kitaev@213: kitaev@213: private final org.tmatesoft.hg.internal.Internals impl = new org.tmatesoft.hg.internal.Internals(); kitaev@213: private HgIgnore ignore; kitaev@213: private ConfigFile configFile; kitaev@213: kitaev@213: HgRepository(String repositoryPath) { kitaev@213: repoDir = null; kitaev@213: repoLocation = repositoryPath; kitaev@213: dataAccess = null; kitaev@213: dataPathHelper = repoPathHelper = null; kitaev@213: normalizePath = null; kitaev@213: } kitaev@213: kitaev@213: HgRepository(String repositoryPath, File repositoryRoot) { kitaev@213: assert ".hg".equals(repositoryRoot.getName()) && repositoryRoot.isDirectory(); kitaev@213: assert repositoryPath != null; kitaev@213: assert repositoryRoot != null; kitaev@213: repoDir = repositoryRoot; kitaev@213: repoLocation = repositoryPath; kitaev@213: dataAccess = new DataAccessProvider(); kitaev@213: final boolean runningOnWindows = System.getProperty("os.name").indexOf("Windows") != -1; kitaev@213: if (runningOnWindows) { kitaev@213: normalizePath = new PathRewrite() { kitaev@213: kitaev@213: public String rewrite(String path) { kitaev@213: // TODO handle . and .. (although unlikely to face them from GUI client) kitaev@213: path = path.replace('\\', '/').replace("//", "/"); kitaev@213: if (path.startsWith("/")) { kitaev@213: path = path.substring(1); kitaev@213: } kitaev@213: return path; kitaev@213: } kitaev@213: }; kitaev@213: } else { kitaev@213: normalizePath = new PathRewrite.Empty(); // or strip leading slash, perhaps? kitaev@213: } kitaev@213: parseRequires(); kitaev@213: dataPathHelper = impl.buildDataFilesHelper(); kitaev@213: repoPathHelper = impl.buildRepositoryFilesHelper(); kitaev@213: } kitaev@213: kitaev@213: @Override kitaev@213: public String toString() { kitaev@213: return getClass().getSimpleName() + "[" + getLocation() + (isInvalid() ? "(BAD)" : "") + "]"; kitaev@213: } kitaev@213: kitaev@213: public String getLocation() { kitaev@213: return repoLocation; kitaev@213: } kitaev@213: kitaev@213: public boolean isInvalid() { kitaev@213: return repoDir == null || !repoDir.exists() || !repoDir.isDirectory(); kitaev@213: } kitaev@213: kitaev@213: public HgChangelog getChangelog() { kitaev@213: if (this.changelog == null) { kitaev@213: String storagePath = repoPathHelper.rewrite("00changelog.i"); kitaev@213: RevlogStream content = resolve(Path.create(storagePath), true); kitaev@213: this.changelog = new HgChangelog(this, content); kitaev@213: } kitaev@213: return this.changelog; kitaev@213: } kitaev@213: kitaev@213: public HgManifest getManifest() { kitaev@213: if (this.manifest == null) { kitaev@213: RevlogStream content = resolve(Path.create(repoPathHelper.rewrite("00manifest.i")), true); kitaev@213: this.manifest = new HgManifest(this, content); kitaev@213: } kitaev@213: return this.manifest; kitaev@213: } kitaev@213: kitaev@213: public final HgTags getTags() { kitaev@213: if (tags == null) { kitaev@213: tags = new HgTags(); kitaev@213: try { kitaev@213: tags.readGlobal(new File(repoDir.getParentFile(), ".hgtags")); kitaev@213: tags.readLocal(new File(repoDir, "localtags")); kitaev@213: } catch (IOException ex) { kitaev@213: ex.printStackTrace(); // FIXME log or othewise report kitaev@213: } kitaev@213: } kitaev@213: return tags; kitaev@213: } kitaev@213: kitaev@213: public HgDataFile getFileNode(String path) { kitaev@213: String nPath = normalizePath.rewrite(path); kitaev@213: String storagePath = dataPathHelper.rewrite(nPath); kitaev@213: RevlogStream content = resolve(Path.create(storagePath), false); kitaev@213: Path p = Path.create(nPath); kitaev@213: if (content == null) { kitaev@213: return new HgDataFile(this, p); kitaev@213: } kitaev@213: return new HgDataFile(this, p, content); kitaev@213: } kitaev@213: kitaev@213: public HgDataFile getFileNode(Path path) { kitaev@213: String storagePath = dataPathHelper.rewrite(path.toString()); kitaev@213: RevlogStream content = resolve(Path.create(storagePath), false); kitaev@213: // XXX no content when no file? or HgDataFile.exists() to detect that? kitaev@213: if (content == null) { kitaev@213: return new HgDataFile(this, path); kitaev@213: } kitaev@213: return new HgDataFile(this, path, content); kitaev@213: } kitaev@213: kitaev@213: /* clients need to rewrite path from their FS to a repository-friendly paths, and, perhaps, vice versa*/ kitaev@213: public PathRewrite getToRepoPathHelper() { kitaev@213: return normalizePath; kitaev@213: } kitaev@213: kitaev@213: // local to hide use of io.File. kitaev@213: /*package-local*/ File getRepositoryRoot() { kitaev@213: return repoDir; kitaev@213: } kitaev@213: kitaev@213: // XXX package-local, unless there are cases when required from outside (guess, working dir/revision walkers may hide dirstate access and no public visibility needed) kitaev@213: /*package-local*/ final HgDirstate loadDirstate() { kitaev@213: return new HgDirstate(getDataAccess(), new File(repoDir, "dirstate")); kitaev@213: } kitaev@213: kitaev@213: // package-local, see comment for loadDirstate kitaev@213: /*package-local*/ final HgIgnore getIgnore() { kitaev@213: // TODO read config for additional locations kitaev@213: if (ignore == null) { kitaev@213: ignore = new HgIgnore(); kitaev@213: try { kitaev@213: File ignoreFile = new File(repoDir.getParentFile(), ".hgignore"); kitaev@213: ignore.read(ignoreFile); kitaev@213: } catch (IOException ex) { kitaev@213: ex.printStackTrace(); // log warn kitaev@213: } kitaev@213: } kitaev@213: return ignore; kitaev@213: } kitaev@213: kitaev@213: /*package-local*/ DataAccessProvider getDataAccess() { kitaev@213: return dataAccess; kitaev@213: } kitaev@213: kitaev@213: // FIXME not sure repository shall create walkers kitaev@213: /*package-local*/ FileIterator createWorkingDirWalker() { kitaev@213: File repoRoot = repoDir.getParentFile(); kitaev@213: Path.Source pathSrc = new Path.SimpleSource(new PathRewrite.Composite(new RelativePathRewrite(repoRoot), getToRepoPathHelper())); kitaev@213: // Impl note: simple source is enough as files in the working dir are all unique kitaev@213: // even if they might get reused (i.e. after FileIterator#reset() and walking once again), kitaev@213: // path caching is better to be done in the code which knows that path are being reused kitaev@213: return new FileWalker(repoRoot, pathSrc); kitaev@213: } kitaev@213: kitaev@213: /** kitaev@213: * Perhaps, should be separate interface, like ContentLookup kitaev@213: * path - repository storage path (i.e. one usually with .i or .d) kitaev@213: */ kitaev@213: /*package-local*/ RevlogStream resolve(Path path, boolean shallFakeNonExistent) { kitaev@213: final SoftReference ref = streamsCache.get(path); kitaev@213: RevlogStream cached = ref == null ? null : ref.get(); kitaev@213: if (cached != null) { kitaev@213: return cached; kitaev@213: } kitaev@213: File f = new File(repoDir, path.toString()); kitaev@213: if (f.exists()) { kitaev@213: RevlogStream s = new RevlogStream(dataAccess, f); kitaev@213: streamsCache.put(path, new SoftReference(s)); kitaev@213: return s; kitaev@213: } else { kitaev@213: if (shallFakeNonExistent) { kitaev@213: try { kitaev@213: File fake = File.createTempFile(f.getName(), null); kitaev@213: fake.deleteOnExit(); kitaev@213: return new RevlogStream(dataAccess, fake); kitaev@213: } catch (IOException ex) { kitaev@213: ex.printStackTrace(); // FIXME report in debug kitaev@213: } kitaev@213: } kitaev@213: } kitaev@213: return null; // XXX empty stream instead? kitaev@213: } kitaev@213: kitaev@213: // can't expose internal class, otherwise seems reasonable to have it in API kitaev@213: /*package-local*/ ConfigFile getConfigFile() { kitaev@213: if (configFile == null) { kitaev@213: configFile = impl.newConfigFile(); kitaev@213: configFile.addLocation(new File(System.getProperty("user.home"), ".hgrc")); kitaev@213: // last one, overrides anything else kitaev@213: // /.hg/hgrc kitaev@213: configFile.addLocation(new File(getRepositoryRoot(), "hgrc")); kitaev@213: } kitaev@213: return configFile; kitaev@213: } kitaev@213: kitaev@213: /*package-local*/ List getFiltersFromRepoToWorkingDir(Path p) { kitaev@213: return instantiateFilters(p, new Filter.Options(Filter.Direction.FromRepo)); kitaev@213: } kitaev@213: kitaev@213: /*package-local*/ List getFiltersFromWorkingDirToRepo(Path p) { kitaev@213: return instantiateFilters(p, new Filter.Options(Filter.Direction.ToRepo)); kitaev@213: } kitaev@213: kitaev@213: private List instantiateFilters(Path p, Filter.Options opts) { kitaev@213: List factories = impl.getFilters(this, getConfigFile()); kitaev@213: if (factories.isEmpty()) { kitaev@213: return Collections.emptyList(); kitaev@213: } kitaev@213: ArrayList rv = new ArrayList(factories.size()); kitaev@213: for (Filter.Factory ff : factories) { kitaev@213: Filter f = ff.create(p, opts); kitaev@213: if (f != null) { kitaev@213: rv.add(f); kitaev@213: } kitaev@213: } kitaev@213: return rv; kitaev@213: } kitaev@213: kitaev@213: private void parseRequires() { kitaev@213: new RequiresFile().parse(impl, new File(repoDir, "requires")); kitaev@213: } kitaev@213: kitaev@213: }