# HG changeset patch # User Artem Tikhomirov # Date 1329343076 -3600 # Node ID b015f3918120957f46b1acd71c0edbbb756252e0 # Parent cdea37239b01f06b221aaa4302d7c7903f084f8d Work on FIXME: correct HgDataFile#workingCopy with tests; BasicSessionContext with property override; platform-specific options to internals diff -r cdea37239b01 -r b015f3918120 src/org/tmatesoft/hg/internal/BasicSessionContext.java --- a/src/org/tmatesoft/hg/internal/BasicSessionContext.java Mon Feb 13 15:11:27 2012 +0100 +++ b/src/org/tmatesoft/hg/internal/BasicSessionContext.java Wed Feb 15 22:57:56 2012 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 TMate Software Ltd + * Copyright (c) 2011-2012 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 @@ -16,6 +16,9 @@ */ package org.tmatesoft.hg.internal; +import java.util.Collections; +import java.util.Map; + import org.tmatesoft.hg.core.SessionContext; import org.tmatesoft.hg.util.LogFacility; import org.tmatesoft.hg.util.PathPool; @@ -30,10 +33,17 @@ private PathPool pathPool; private final LogFacility logFacility; + private final Map properties; public BasicSessionContext(PathPool pathFactory, LogFacility log) { + this(null, pathFactory, log); + } + + @SuppressWarnings("unchecked") + public BasicSessionContext(Map propertyOverrides, PathPool pathFactory, LogFacility log) { pathPool = pathFactory; logFacility = log != null ? log : new StreamLogFacility(true, true, true, System.out); + properties = propertyOverrides == null ? Collections.emptyMap() : (Map) propertyOverrides; } public PathPool getPathPool() { @@ -49,7 +59,11 @@ } public Object getProperty(String name, Object defaultValue) { - String value = System.getProperty(name); + Object value = properties.get(name); + if (value != null) { + return value; + } + value = System.getProperty(name); return value == null ? defaultValue : value; } } diff -r cdea37239b01 -r b015f3918120 src/org/tmatesoft/hg/internal/Internals.java --- a/src/org/tmatesoft/hg/internal/Internals.java Mon Feb 13 15:11:27 2012 +0100 +++ b/src/org/tmatesoft/hg/internal/Internals.java Wed Feb 15 22:57:56 2012 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 TMate Software Ltd + * Copyright (c) 2011-2012 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 @@ -40,15 +40,31 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class Internals { +public final class Internals { + /** + * 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"; private int requiresFlags = 0; private List filterFactories; + private final boolean isCaseSensitiveFileSystem; + private final boolean shallCacheRevlogsInRepo; - public Internals() { + public Internals(SessionContext ctx) { + isCaseSensitiveFileSystem = !runningOnWindows(); + Object p = ctx.getProperty(CFG_PROPERTY_REVLOG_STREAM_CACHE, true); + shallCacheRevlogsInRepo = p instanceof Boolean ? ((Boolean) p).booleanValue() : Boolean.parseBoolean(String.valueOf(p)); } public void parseRequires(HgRepository hgRepo, File requiresFile) { @@ -63,6 +79,25 @@ public/*for tests, otherwise pkg*/ void setStorageConfig(int version, int flags) { requiresFlags = flags; } + + public PathRewrite buildNormalizePathRewrite() { + if (runningOnWindows()) { + return new PathRewrite() { + + public CharSequence rewrite(CharSequence p) { + // TODO handle . and .. (although unlikely to face them from GUI client) + String path = p.toString(); + path = path.replace('\\', '/').replace("//", "/"); + if (path.startsWith("/")) { + path = path.substring(1); + } + return path; + } + }; + } else { + return new PathRewrite.Empty(); // or strip leading slash, perhaps? + } + } // XXX perhaps, should keep both fields right here, not in the HgRepository public PathRewrite buildDataFilesHelper() { @@ -117,6 +152,10 @@ requiresFile.close(); new File(hgDir, "store").mkdir(); // with that, hg verify says ok. } + + public boolean isCaseSensitiveFileSystem() { + return isCaseSensitiveFileSystem; + } public static boolean runningOnWindows() { return System.getProperty("os.name").indexOf("Windows") != -1; @@ -277,4 +316,8 @@ // fallback to default, let calling code fail with Exception if can't write return new File(System.getProperty("user.home"), ".hgrc"); } + + public boolean shallCacheRevlogs() { + return shallCacheRevlogsInRepo; + } } diff -r cdea37239b01 -r b015f3918120 src/org/tmatesoft/hg/repo/HgDataFile.java --- a/src/org/tmatesoft/hg/repo/HgDataFile.java Mon Feb 13 15:11:27 2012 +0100 +++ b/src/org/tmatesoft/hg/repo/HgDataFile.java Wed Feb 15 22:57:56 2012 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2011 TMate Software Ltd + * Copyright (c) 2010-2012 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 @@ -45,6 +45,7 @@ import org.tmatesoft.hg.util.ByteChannel; import org.tmatesoft.hg.util.CancelSupport; import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.util.LogFacility; import org.tmatesoft.hg.util.Pair; import org.tmatesoft.hg.util.Path; import org.tmatesoft.hg.util.ProgressSupport; @@ -161,9 +162,25 @@ } } } else { - // FIXME not TIP, but revision according to dirstate!!! - // add tests for this case - contentWithFilters(TIP, sink); + final Pair wcParents = getRepo().getWorkingCopyParents(); + Nodeid p = wcParents.first().isNull() ? wcParents.second() : wcParents.first(); + if (p.isNull()) { + // no dirstate parents - no content + return; + } + final HgChangelog clog = getRepo().getChangelog(); + // common case to avoid searching complete changelog for nodeid match + final Nodeid tipRev = clog.getRevision(TIP); + final int csetRevIndex; + if (tipRev.equals(p)) { + csetRevIndex = clog.getLastRevision(); + } else { + // bad luck, need to search honestly + csetRevIndex = getRepo().getChangelog().getRevisionIndex(p); + } + Nodeid fileRev = getRepo().getManifest().getFileRevision(csetRevIndex, getPath()); + final int fileRevIndex = getRevisionIndex(fileRev); + contentWithFilters(fileRevIndex, sink); } } @@ -231,13 +248,14 @@ metadata = new Metadata(); } ErrorHandlingInspector insp; + final LogFacility lf = getRepo().getContext().getLog(); if (metadata.none(fileRevisionIndex)) { - insp = new ContentPipe(sink, 0, getRepo().getContext().getLog()); + insp = new ContentPipe(sink, 0, lf); } else if (metadata.known(fileRevisionIndex)) { - insp = new ContentPipe(sink, metadata.dataOffset(fileRevisionIndex), getRepo().getContext().getLog()); + insp = new ContentPipe(sink, metadata.dataOffset(fileRevisionIndex), lf); } else { // do not know if there's metadata - insp = new MetadataInspector(metadata, getPath(), new ContentPipe(sink, 0, getRepo().getContext().getLog())); + insp = new MetadataInspector(metadata, lf, getPath(), new ContentPipe(sink, 0, lf)); } insp.checkCancelled(); super.content.iterate(fileRevisionIndex, fileRevisionIndex, true, insp); @@ -509,19 +527,26 @@ private static final class MetadataEntry { private final String entry; private final int valueStart; + + // key may be null /*package-local*/MetadataEntry(String key, String value) { - entry = key + value; - valueStart = key.length(); + if (key == null) { + entry = value; + valueStart = -1; // not 0 to tell between key == null and key == "" + } else { + entry = key + value; + valueStart = key.length(); + } } /*package-local*/boolean matchKey(String key) { - return key.length() == valueStart && entry.startsWith(key); + return key == null ? valueStart == -1 : key.length() == valueStart && entry.startsWith(key); } // uncomment once/if needed // public String key() { // return entry.substring(0, valueStart); // } public String value() { - return entry.substring(valueStart); + return valueStart == -1 ? entry : entry.substring(valueStart); } } @@ -590,12 +615,14 @@ private static class MetadataInspector extends ErrorHandlingInspector implements RevlogStream.Inspector { private final Metadata metadata; - private final Path fname; // need this only for error reporting private final RevlogStream.Inspector delegate; + private final Path fname; // need these only for error reporting + private final LogFacility log; - public MetadataInspector(Metadata _metadata, Path file, RevlogStream.Inspector chain) { + public MetadataInspector(Metadata _metadata, LogFacility logFacility, Path file, RevlogStream.Inspector chain) { metadata = _metadata; fname = file; + log = logFacility; delegate = chain; setCancelSupport(CancelSupport.Factory.get(chain)); } @@ -647,7 +674,7 @@ break; } if (key == null || lastColon == -1 || i <= lastColon) { - throw new IllegalStateException(); // FIXME log instead and record null key in the metadata. Ex just to fail fast during dev + log.error(getClass(), "Missing key in file revision metadata at index %d", i); } value = new String(bos.toByteArray()).trim(); bos.reset(); diff -r cdea37239b01 -r b015f3918120 src/org/tmatesoft/hg/repo/HgDirstate.java --- a/src/org/tmatesoft/hg/repo/HgDirstate.java Mon Feb 13 15:11:27 2012 +0100 +++ b/src/org/tmatesoft/hg/repo/HgDirstate.java Wed Feb 15 22:57:56 2012 +0100 @@ -142,7 +142,7 @@ } else if (state == 'm') { merged.put(r.name1, r); } else { - // FIXME log error? + repo.getContext().getLog().warn(getClass(), "Dirstate record for file %s (size: %d, tstamp:%d) has unknown state '%c'", r.name1, r.size(), r.time, state); } } } catch (IOException ex) { diff -r cdea37239b01 -r b015f3918120 src/org/tmatesoft/hg/repo/HgRepository.java --- a/src/org/tmatesoft/hg/repo/HgRepository.java Mon Feb 13 15:11:27 2012 +0100 +++ b/src/org/tmatesoft/hg/repo/HgRepository.java Wed Feb 15 22:57:56 2012 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2011 TMate Software Ltd + * Copyright (c) 2010-2012 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 @@ -34,7 +34,6 @@ import org.tmatesoft.hg.internal.DataAccessProvider; import org.tmatesoft.hg.internal.Experimental; import org.tmatesoft.hg.internal.Filter; -import org.tmatesoft.hg.internal.Internals; import org.tmatesoft.hg.internal.RevlogStream; import org.tmatesoft.hg.internal.SubrepoManager; import org.tmatesoft.hg.util.CancelledException; @@ -73,7 +72,6 @@ private final PathRewrite normalizePath; private final PathRewrite dataPathHelper; private final PathRewrite repoPathHelper; - private final boolean isCaseSensitiveFileSystem; // FIXME keep this inside Internals impl and delegate to Internals all os/fs-specific tasks private final SessionContext sessionContext; private HgChangelog changelog; @@ -86,7 +84,7 @@ // XXX perhaps, shall enable caching explicitly private final HashMap> streamsCache = new HashMap>(); - private final org.tmatesoft.hg.internal.Internals impl = new org.tmatesoft.hg.internal.Internals(); + private final org.tmatesoft.hg.internal.Internals impl; private HgIgnore ignore; private HgRepoConfig repoConfig; @@ -98,7 +96,7 @@ dataPathHelper = repoPathHelper = null; normalizePath = null; sessionContext = null; - isCaseSensitiveFileSystem = !Internals.runningOnWindows(); + impl = null; } HgRepository(SessionContext ctx, String repositoryPath, File repositoryRoot) { @@ -111,28 +109,12 @@ if (workingDir == null) { throw new IllegalArgumentException(repoDir.toString()); } + impl = new org.tmatesoft.hg.internal.Internals(ctx); repoLocation = repositoryPath; sessionContext = ctx; dataAccess = new DataAccessProvider(ctx); - final boolean runningOnWindows = Internals.runningOnWindows(); - isCaseSensitiveFileSystem = !runningOnWindows; - if (runningOnWindows) { - normalizePath = new PathRewrite() { - - public CharSequence rewrite(CharSequence p) { - // TODO handle . and .. (although unlikely to face them from GUI client) - String path = p.toString(); - path = path.replace('\\', '/').replace("//", "/"); - if (path.startsWith("/")) { - path = path.substring(1); - } - return path; - } - }; - } else { - normalizePath = new PathRewrite.Empty(); // or strip leading slash, perhaps? - } impl.parseRequires(this, new File(repoDir, "requires")); + normalizePath = impl.buildNormalizePathRewrite(); dataPathHelper = impl.buildDataFilesHelper(); repoPathHelper = impl.buildRepositoryFilesHelper(); } @@ -151,20 +133,20 @@ } public HgChangelog getChangelog() { - if (this.changelog == null) { + if (changelog == null) { CharSequence storagePath = repoPathHelper.rewrite("00changelog.i"); RevlogStream content = resolve(Path.create(storagePath), true); - this.changelog = new HgChangelog(this, content); + changelog = new HgChangelog(this, content); } - return this.changelog; + return changelog; } public HgManifest getManifest() { - if (this.manifest == null) { + if (manifest == null) { RevlogStream content = resolve(Path.create(repoPathHelper.rewrite("00manifest.i")), true); - this.manifest = new HgManifest(this, content); + manifest = new HgManifest(this, content); } - return this.manifest; + return manifest; } public HgTags getTags() throws HgInvalidControlFileException { @@ -316,7 +298,7 @@ // XXX consider passing Path pool or factory to produce (shared) Path instead of Strings /*package-local*/ final HgDirstate loadDirstate(PathPool pathPool) throws HgInvalidControlFileException { PathRewrite canonicalPath = null; - if (!isCaseSensitiveFileSystem) { + if (!impl.isCaseSensitiveFileSystem()) { canonicalPath = new PathRewrite() { public CharSequence rewrite(CharSequence path) { @@ -369,7 +351,9 @@ File f = new File(repoDir, path.toString()); if (f.exists()) { RevlogStream s = new RevlogStream(dataAccess, f); - streamsCache.put(path, new SoftReference(s)); + if (impl.shallCacheRevlogs()) { + streamsCache.put(path, new SoftReference(s)); + } return s; } else { if (shallFakeNonExistent) { diff -r cdea37239b01 -r b015f3918120 src/org/tmatesoft/hg/repo/Revlog.java --- a/src/org/tmatesoft/hg/repo/Revlog.java Mon Feb 13 15:11:27 2012 +0100 +++ b/src/org/tmatesoft/hg/repo/Revlog.java Wed Feb 15 22:57:56 2012 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2010-2011 TMate Software Ltd + * Copyright (c) 2010-2012 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 @@ -91,9 +91,9 @@ } /** - * Map revision index to unique revision identifier (nodeid) + * Map revision index to unique revision identifier (nodeid). * - * @param revision index of the entry in this revlog + * @param revision index of the entry in this revlog, may be {@link HgRepository#TIP} * @return revision nodeid of the entry * * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog diff -r cdea37239b01 -r b015f3918120 src/org/tmatesoft/hg/util/Pair.java --- a/src/org/tmatesoft/hg/util/Pair.java Mon Feb 13 15:11:27 2012 +0100 +++ b/src/org/tmatesoft/hg/util/Pair.java Wed Feb 15 22:57:56 2012 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 TMate Software Ltd + * Copyright (c) 2011-2012 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 @@ -46,5 +46,16 @@ public boolean hasSecond() { return value2 != null; } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append('<'); + sb.append(first()); + sb.append(':'); + sb.append(second()); + sb.append('>'); + return sb.toString(); + } } diff -r cdea37239b01 -r b015f3918120 test/org/tmatesoft/hg/test/Configuration.java --- a/test/org/tmatesoft/hg/test/Configuration.java Mon Feb 13 15:11:27 2012 +0100 +++ b/test/org/tmatesoft/hg/test/Configuration.java Wed Feb 15 22:57:56 2012 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 TMate Software Ltd + * Copyright (c) 2011-2012 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 @@ -92,6 +92,9 @@ return rv; } + /** + * @return temporary directory to use in tests, may be configured from outside + */ public File getTempDir() { if (tempDir == null) { String td = System.getProperty("hg4j.tests.tmpdir", System.getProperty("java.io.tmpdir")); @@ -100,6 +103,9 @@ return tempDir; } + /** + * @return location with various files used in tests + */ public File getTestDataDir() { if (testDataDir == null) { testDataDir = new File(System.getProperty("user.dir"), "test-data"); diff -r cdea37239b01 -r b015f3918120 test/org/tmatesoft/hg/test/ExecHelper.java --- a/test/org/tmatesoft/hg/test/ExecHelper.java Mon Feb 13 15:11:27 2012 +0100 +++ b/test/org/tmatesoft/hg/test/ExecHelper.java Wed Feb 15 22:57:56 2012 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 TMate Software Ltd + * Copyright (c) 2011-2012 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 @@ -50,7 +50,7 @@ if (new File(pe, cmd[0] + ".exe").exists()) { break; } - // PATHEXT controls precedence of .exe, .bat and .cmd files, ususlly .exe wins + // PATHEXT controls precedence of .exe, .bat and .cmd files, usually .exe wins if (new File(pe, cmd[0] + ".bat").exists() || new File(pe, cmd[0] + ".cmd").exists()) { ArrayList command = new ArrayList(); command.add("cmd.exe"); diff -r cdea37239b01 -r b015f3918120 test/org/tmatesoft/hg/test/TestByteChannel.java --- a/test/org/tmatesoft/hg/test/TestByteChannel.java Mon Feb 13 15:11:27 2012 +0100 +++ b/test/org/tmatesoft/hg/test/TestByteChannel.java Wed Feb 15 22:57:56 2012 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 TMate Software Ltd + * Copyright (c) 2011-2012 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 @@ -16,12 +16,21 @@ */ package org.tmatesoft.hg.test; -import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.*; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Collections; +import java.util.Map; import org.junit.Assert; import org.junit.Test; +import org.tmatesoft.hg.internal.BasicSessionContext; import org.tmatesoft.hg.internal.ByteArrayChannel; +import org.tmatesoft.hg.internal.Internals; import org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgLookup; import org.tmatesoft.hg.repo.HgRepository; /** @@ -84,4 +93,55 @@ dir_b.content(0, ch = new ByteArrayChannel()); assertArrayEquals("a \r\n".getBytes(), ch.toArray()); } + + @Test + public void testWorkingCopyFileAccess() throws Exception { + final File repoDir = TestIncoming.initEmptyTempRepo("testWorkingCopyFileAccess"); + final Map props = Collections.singletonMap(Internals.CFG_PROPERTY_REVLOG_STREAM_CACHE, false); + repo = new HgLookup(new BasicSessionContext(props, null, null)).detect(repoDir); + File f1 = new File(repoDir, "file1"); + final String c1 = "First", c2 = "Second", c3 = "Third"; + ByteArrayChannel ch; + ExecHelper exec = new ExecHelper(new OutputParser.Stub(), repoDir); + // commit cset 0 + write(f1, c1); + exec.run("hg", "add"); + Assert.assertEquals(0, exec.getExitValue()); + exec.run("hg", "commit", "-m", "c0"); + Assert.assertEquals(0, exec.getExitValue()); + // commit cset 1 + write(f1, c2); + exec.run("hg", "commit", "-m", "c1"); + assertEquals(0, exec.getExitValue()); + // + // modify working copy + write(f1, c3); + // + HgDataFile df = repo.getFileNode(f1.getName()); + // 1. Shall take content of the file from the dir + df.workingCopy(ch = new ByteArrayChannel()); + assertArrayEquals(c3.getBytes(), ch.toArray()); + // 2. Shall supply working copy even if no local file is there + f1.delete(); + assertFalse(f1.exists()); + df = repo.getFileNode(f1.getName()); + df.workingCopy(ch = new ByteArrayChannel()); + assertArrayEquals(c2.getBytes(), ch.toArray()); + // + // 3. Shall extract revision of the file that corresponds actual parents (from dirstate) not the TIP as it was + exec.run("hg", "update", "-r", "0"); + assertEquals(0, exec.getExitValue()); + f1.delete(); + assertFalse(f1.exists()); + // there's no file and workingCopy shall do some extra work to find out actual revision to check out + df = repo.getFileNode(f1.getName()); + df.workingCopy(ch = new ByteArrayChannel()); + assertArrayEquals(c1.getBytes(), ch.toArray()); + } + + private static void write(File f, String content) throws IOException { + FileWriter fw = new FileWriter(f); + fw.write(content); + fw.close(); + } } diff -r cdea37239b01 -r b015f3918120 test/org/tmatesoft/hg/test/TestIncoming.java --- a/test/org/tmatesoft/hg/test/TestIncoming.java Mon Feb 13 15:11:27 2012 +0100 +++ b/test/org/tmatesoft/hg/test/TestIncoming.java Wed Feb 15 22:57:56 2012 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 TMate Software Ltd + * Copyright (c) 2011-2012 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 @@ -32,6 +32,7 @@ import org.tmatesoft.hg.core.HgIncomingCommand; import org.tmatesoft.hg.core.HgLogCommand; import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.BasicSessionContext; import org.tmatesoft.hg.internal.Internals; import org.tmatesoft.hg.repo.HgLookup; import org.tmatesoft.hg.repo.HgRemoteRepository; @@ -134,7 +135,7 @@ static File initEmptyTempRepo(String dirName) throws IOException { File dest = createEmptyDir(dirName); - Internals implHelper = new Internals(); + Internals implHelper = new Internals(new BasicSessionContext(null, null, null)); implHelper.setStorageConfig(1, STORE | FNCACHE | DOTENCODE); implHelper.initEmptyRepository(new File(dest, ".hg")); return dest;