# HG changeset patch # User Artem Tikhomirov # Date 1342032047 -7200 # Node ID 7bcfbc255f48e19b4a3de98682e2f5f0ca961628 # Parent 31bd09da0dcfe48e1fc662143f91ff402238aa84# Parent 2078692eeb586249eaccfb4a470b60dd8ef2c81f Merge changes from smartgit3 branch into 1.1 stream diff -r 2078692eeb58 -r 7bcfbc255f48 .hgignore --- a/.hgignore Thu Jun 21 21:36:06 2012 +0200 +++ b/.hgignore Wed Jul 11 20:40:47 2012 +0200 @@ -2,3 +2,5 @@ bin hg4j*.jar TEST-*.xml +build/ +.gradle/ diff -r 2078692eeb58 -r 7bcfbc255f48 .hgtags --- a/.hgtags Thu Jun 21 21:36:06 2012 +0200 +++ b/.hgtags Wed Jul 11 20:40:47 2012 +0200 @@ -1,2 +1,6 @@ c2601c0b4a1fd5940054eec95b92ea719e66cb78 v0.5.0 fc8bc2f1edbe876c66c8fcbfc054bb836c733b06 v0.7.0 +f52ca9530774436ce5a9192e7c5a825a5018b65d v0.8.0 +fdd7d756dea0aa95152536db03ddf4b7759e2d31 v0.8.5 +7e1912b4ce991d4c220a5a77246a2aab60ae7750 v0.9.0 +3ca4ae7bdd3890b8ed89bfea1b42af593e04b373 v1.0.0 diff -r 2078692eeb58 -r 7bcfbc255f48 COPYING --- a/COPYING Thu Jun 21 21:36:06 2012 +0200 +++ b/COPYING Wed Jul 11 20:40:47 2012 +0200 @@ -1,4 +1,4 @@ -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 diff -r 2078692eeb58 -r 7bcfbc255f48 TODO --- a/TODO Thu Jun 21 21:36:06 2012 +0200 +++ b/TODO Wed Jul 11 20:40:47 2012 +0200 @@ -57,7 +57,10 @@ RELEASE NOTES 1.0 * Known issues and limitations: - ** Configuration files listed under HKEY_LOCAL_MACHINE\SOFTWARE\Mercurial are not processed + ** Configuration files listed under HKEY_LOCAL_MACHINE\SOFTWARE\Mercurial are not processed + ** %include and %unset directives in config files + ** additional locations of ignore configuration are not read/processed from ui.ignore.* + ** subrepositories and path mapping in cfg Read-only support, version 1.1 ============================== diff -r 2078692eeb58 -r 7bcfbc255f48 build.gradle --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/build.gradle Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2012 TMate Software Ltd + */ +def isRelease = false + + version = '1.1.0-SNAPSHOT' + description = 'Pure Java API and Toolkit for Mercurial DVCS' + group = 'org.tmatesoft.hg4j' + + apply plugin : 'java' + apply plugin : 'maven' + + sourceCompatibility = '1.5' + targetCompatibility = '1.5' + + sourceSets { + main { + java { + srcDir 'src/' + } + } + test { + java { + srcDir 'test/' + } + resources { + srcDir 'test-data/' + } + } + cli { + java { + srcDir 'cmdline/' + } + } + } + + repositories { + mavenLocal() + mavenCentral() + } + + configurations { + deployJars + } + + dependencies { + testCompile 'junit:junit:4.8.2' + cliCompile 'junit:junit:4.8.2' + cliCompile files(sourceSets.main.output) { + builtBy compileJava + } + deployJars "org.apache.maven.wagon:wagon-http:1.0-beta-2" + } + + def sharedMetaInf = { + from project.file('COPYING') + } + + + task sourcesJar(type: Jar) { + classifier = 'src' + from sourceSets.main.java, sourceSets.test.java, sourceSets.cli.java + metaInf sharedMetaInf + } + + task cmdlineJar(type: Jar) { + appendix = 'console' + from sourceSets.cli.output + metaInf sharedMetaInf + } + + jar { + manifest { + attributes ("Implementation-Version": version) + } + metaInf sharedMetaInf + } + + artifacts { + archives sourcesJar, cmdlineJar + deployJars jar, sourcesJar + } + + install { + configuration = configurations.deployJars +/* + repositories.mavenDeployer { + addFilter('f1') { artifact, file -> + println file.name + println artifact.ext + println file.name - ('.' + artifact.ext) + println ' ' + def fname = file.name - ('.' + artifact.ext) + println fname.endsWith('src') + fname.endsWith('src') + } + addFilter('f2') { artifact, file -> + def fname = file.name - ('.' + artifact.ext) + fname.endsWith('console') + } + addFilter('f3') { artifact, file -> + def fname = file.name - ('.' + artifact.ext) + fname.endsWith(version) + } + } +*/ + } + + uploadArchives { + configuration = configurations.deployJars + repositories { + mavenDeployer { + configuration = configurations.deployJars + repository(url: "http://maven.tmatesoft.com/content/repositories/snapshots/") { + authentication(userName: project.ext.deploySnapshotsRepositoryUser, password: project.ext.deploySnapshotsRepositoryPassword) + } + } + } +} + + + task findOutWhyProjectCopyDoesntWork() << { + // files under .hg/ are not copied with copy {} + ext.myjar = zipTree( 'test-data/test-repos.jar' ) + ext.destDir = new File(project.getBuildDir(), "hg4j-tests1/") + outputs.dir ext.destDir + CopySpec ss = copySpec { + from ext.myjar + into ext.destDir + include '*', '*/.*', '**/.*/*', '**/.*', '**/*', '*/.*/**', '**/**', '.*/**', '**/.hg/*', '**/.hg*' + eachFile {element -> + println "AAA:$element.relativePath" + } + } + println "includeEmptyDir: $ss.includeEmptyDirs" + println "includes: $ss.includes" + println "allIncludes: $ss.allIncludes" + println "excludes: $ss.excludes" + println "allExcludes: $ss.allExcludes" + } + + + test { +// +// +// + + File testReposRoot = new File(project.getBuildDir(), "hg4j-tests/"); + if ( testReposRoot.exists() ) { + project.delete(testReposRoot) + } + testReposRoot.mkdirs(); + + zipTree('test-data/test-repos.jar').visit {element -> + element.copyTo(element.relativePath.getFile(testReposRoot)) + } + + systemProperties 'hg4j.tests.repos' : testReposRoot + systemProperties 'hg4j.tests.remote' : 'http://hg.serpentine.com/tutorial/hello' + } \ No newline at end of file diff -r 2078692eeb58 -r 7bcfbc255f48 build.xml --- a/build.xml Thu Jun 21 21:36:06 2012 +0200 +++ b/build.xml Wed Jul 11 20:40:47 2012 +0200 @@ -1,6 +1,6 @@ diff -r 2078692eeb58 -r 7bcfbc255f48 cmdline/org/tmatesoft/hg/console/Bundle.java --- a/cmdline/org/tmatesoft/hg/console/Bundle.java Thu Jun 21 21:36:06 2012 +0200 +++ b/cmdline/org/tmatesoft/hg/console/Bundle.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -20,7 +20,6 @@ import java.util.Collections; import java.util.LinkedList; -import org.tmatesoft.hg.core.HgCallbackTargetException; import org.tmatesoft.hg.core.HgException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.repo.HgBundle; @@ -62,16 +61,12 @@ private final HgChangelog changelog = hgRepo.getChangelog(); public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { - try { - if (changelog.isKnown(nodeid)) { - System.out.print("+"); - } else { - System.out.print("-"); - } - System.out.printf("%d:%s\n%s\n", revisionNumber, nodeid.shortNotation(), cset.toString()); - } catch (HgException ex) { - throw new HgCallbackTargetException.Wrap(ex); + if (changelog.isKnown(nodeid)) { + System.out.print("+"); + } else { + System.out.print("-"); } + System.out.printf("%d:%s\n%s\n", revisionNumber, nodeid.shortNotation(), cset.toString()); } }); } diff -r 2078692eeb58 -r 7bcfbc255f48 cmdline/org/tmatesoft/hg/console/ChangesetDumpHandler.java --- a/cmdline/org/tmatesoft/hg/console/ChangesetDumpHandler.java Thu Jun 21 21:36:06 2012 +0200 +++ b/cmdline/org/tmatesoft/hg/console/ChangesetDumpHandler.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -63,7 +63,7 @@ return this; } - public void next(HgChangeset changeset) { + public void cset(HgChangeset changeset) { try { final String s = print(changeset); if (reverseOrder) { @@ -93,14 +93,14 @@ StringBuilder sb = new StringBuilder(); Formatter f = new Formatter(sb); final Nodeid csetNodeid = cset.getNodeid(); - f.format("changeset: %d:%s\n", cset.getRevision(), complete ? csetNodeid : csetNodeid.shortNotation()); - if (cset.getRevision() == tip || repo.getTags().isTagged(csetNodeid)) { + f.format("changeset: %d:%s\n", cset.getRevisionIndex(), complete ? csetNodeid : csetNodeid.shortNotation()); + if (cset.getRevisionIndex() == tip || repo.getTags().isTagged(csetNodeid)) { sb.append("tag: "); for (String t : repo.getTags().tags(csetNodeid)) { sb.append(t); sb.append(' '); } - if (cset.getRevision() == tip) { + if (cset.getRevisionIndex() == tip) { sb.append("tip"); } sb.append('\n'); diff -r 2078692eeb58 -r 7bcfbc255f48 cmdline/org/tmatesoft/hg/console/Log.java --- a/cmdline/org/tmatesoft/hg/console/Log.java Thu Jun 21 21:36:06 2012 +0200 +++ b/cmdline/org/tmatesoft/hg/console/Log.java Wed Jul 11 20:40:47 2012 +0200 @@ -20,6 +20,7 @@ import java.util.List; +import org.tmatesoft.hg.core.HgChangesetHandler; import org.tmatesoft.hg.core.HgFileRevision; import org.tmatesoft.hg.core.HgLogCommand; import org.tmatesoft.hg.repo.HgDataFile; @@ -119,7 +120,7 @@ return rv; } - private static final class Dump extends ChangesetDumpHandler implements HgLogCommand.FileHistoryHandler { + private static final class Dump extends ChangesetDumpHandler implements HgChangesetHandler.WithCopyHistory { public Dump(HgRepository hgRepo) { super(hgRepo); diff -r 2078692eeb58 -r 7bcfbc255f48 cmdline/org/tmatesoft/hg/console/Main.java --- a/cmdline/org/tmatesoft/hg/console/Main.java Thu Jun 21 21:36:06 2012 +0200 +++ b/cmdline/org/tmatesoft/hg/console/Main.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -17,6 +17,8 @@ package org.tmatesoft.hg.console; import static org.tmatesoft.hg.repo.HgRepository.TIP; +import static org.tmatesoft.hg.repo.HgRepository.WORKING_COPY; +import static org.tmatesoft.hg.util.LogFacility.Severity.*; import java.io.File; import java.io.IOException; @@ -28,18 +30,18 @@ import java.util.Map; import org.junit.Assert; -import org.tmatesoft.hg.core.HgBadStateException; +import org.tmatesoft.hg.core.HgManifestHandler; import org.tmatesoft.hg.core.HgCallbackTargetException; import org.tmatesoft.hg.core.HgCatCommand; import org.tmatesoft.hg.core.HgChangeset; +import org.tmatesoft.hg.core.HgChangesetFileSneaker; import org.tmatesoft.hg.core.HgChangesetTreeHandler; -import org.tmatesoft.hg.core.HgDataStreamException; import org.tmatesoft.hg.core.HgException; -import org.tmatesoft.hg.core.HgFileInformer; import org.tmatesoft.hg.core.HgFileRevision; import org.tmatesoft.hg.core.HgLogCommand; import org.tmatesoft.hg.core.HgManifestCommand; import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.BasicSessionContext; import org.tmatesoft.hg.internal.ByteArrayChannel; import org.tmatesoft.hg.internal.DigestHelper; import org.tmatesoft.hg.internal.PathGlobMatcher; @@ -59,8 +61,10 @@ import org.tmatesoft.hg.repo.HgManifest; import org.tmatesoft.hg.repo.HgManifest.Flags; import org.tmatesoft.hg.repo.HgMergeState; +import org.tmatesoft.hg.repo.HgParentChildMap; import org.tmatesoft.hg.repo.HgPhase; import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgRuntimeException; import org.tmatesoft.hg.repo.HgStatusCollector; import org.tmatesoft.hg.repo.HgStatusInspector; import org.tmatesoft.hg.repo.HgSubrepoLocation; @@ -68,11 +72,14 @@ import org.tmatesoft.hg.repo.ext.MqManager; import org.tmatesoft.hg.repo.ext.MqManager.PatchRecord; import org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector; +import org.tmatesoft.hg.repo.HgRevisionMap; import org.tmatesoft.hg.util.FileWalker; import org.tmatesoft.hg.util.LogFacility; import org.tmatesoft.hg.util.Pair; import org.tmatesoft.hg.util.Path; import org.tmatesoft.hg.util.PathRewrite; +import org.tmatesoft.hg.util.ProgressSupport; +import org.tmatesoft.hg.util.LogFacility.Severity; /** * Various debug dumps. @@ -98,6 +105,9 @@ public static void main(String[] args) throws Exception { Main m = new Main(args); +// m.checkWalkFileRevisions(); +// m.checkSubProgress(); +// m.checkFileFlags(); m.testMqManager(); // m.testRevisionDescendants(); // m.dumpPhases(); @@ -105,7 +115,7 @@ // m.testConsoleLog(); // m.testTreeTraversal(); // m.testRevisionMap(); -// m.testSubrepos(); + m.testSubrepos(); // m.testReadWorkingCopy(); // m.testParents(); // m.testEffectiveFileLog(); @@ -174,7 +184,7 @@ HgPhase[] result1 = new HgPhase[hgRepo.getChangelog().getRevisionCount()]; HgPhase[] result2 = new HgPhase[hgRepo.getChangelog().getRevisionCount()]; final long start1 = System.nanoTime(); - HgChangelog.ParentWalker pw = hgRepo.getChangelog().new ParentWalker(); + HgParentChildMap pw = new HgParentChildMap(hgRepo.getChangelog()); pw.init(); final long start1bis = System.nanoTime(); PhasesHelper ph = new PhasesHelper(hgRepo, pw); @@ -199,48 +209,98 @@ } } + // hg4j repo + public void checkWalkFileRevisions() throws Exception { + // hg --debug manifest --rev 150 | grep cmdline/org/tmatesoft/hg/console/Main.java + hgRepo.getManifest().walkFileRevisions(Path.create("cmdline/org/tmatesoft/hg/console/Main.java"), new ManifestDump(), 100, 150, 200, 210, 300); + } + + // no repo + // FIXME as test, perhaps in TestAuxUtilities + private void checkSubProgress() { + ProgressSupport ps = new ProgressSupport() { + private int units; + + public void start(int totalUnits) { + units = totalUnits; + System.out.printf("%d:", totalUnits); + + } + public void worked(int wu) { + for (int i = 0; i < wu; i++) { + System.out.print(units-- == 0 ? '!' : '.'); + } + } + public void done() { + System.out.println("DONE"); + } + }; + ps.start(10); + ProgressSupport.Sub s1 = new ProgressSupport.Sub(ps, 3); + ProgressSupport.Sub s2 = new ProgressSupport.Sub(ps, 7); + s1.start(10); + s1.worked(1); + s1.worked(2); + s1.worked(3); + s1.worked(4); + s1.done(); + // + s2.start(5); + s2.worked(3); + s2.worked(2); + s2.done(); + } + + private void checkFileFlags() throws Exception { + // ~/hg/test-flags repo + // TODO transform to a test once I keep test-flags in test-repos.jar + HgDataFile link = hgRepo.getFileNode("file-link"); + HgDataFile exec = hgRepo.getFileNode("file-exec"); + HgDataFile file = hgRepo.getFileNode("regular-file"); + System.out.println("Link: " + link.getFlags(TIP)); + System.out.println("Exec: " + exec.getFlags(TIP)); + System.out.println("File: " + file.getFlags(TIP)); + } + + private void buildFileLog() throws Exception { final long start = System.nanoTime(); HgLogCommand cmd = new HgLogCommand(hgRepo); cmd.file("file1", false); cmd.execute(new HgChangesetTreeHandler() { - public void next(HgChangesetTreeHandler.TreeElement entry) { - try { - StringBuilder sb = new StringBuilder(); - HashSet test = new HashSet(entry.childRevisions()); - for (HgChangeset cc : entry.children()) { - sb.append(cc.getRevision()); - sb.append(':'); - sb.append(cc.getNodeid().shortNotation()); - sb.append(", "); - } - final Pair parents = entry.parentRevisions(); - final boolean isJoin = !parents.first().isNull() && !parents.second().isNull(); - final boolean isFork = entry.children().size() > 1; - final HgChangeset cset = entry.changeset(); - System.out.printf("%d:%s - %s (%s)\n", cset.getRevision(), cset.getNodeid().shortNotation(), cset.getComment(), cset.getPhase()); - if (!isJoin && !isFork && !entry.children().isEmpty()) { - System.out.printf("\t=> %s\n", sb); + public void treeElement(HgChangesetTreeHandler.TreeElement entry) { + StringBuilder sb = new StringBuilder(); + HashSet test = new HashSet(entry.childRevisions()); + for (HgChangeset cc : entry.children()) { + sb.append(cc.getRevisionIndex()); + sb.append(':'); + sb.append(cc.getNodeid().shortNotation()); + sb.append(", "); + } + final Pair parents = entry.parentRevisions(); + final boolean isJoin = !parents.first().isNull() && !parents.second().isNull(); + final boolean isFork = entry.children().size() > 1; + final HgChangeset cset = entry.changeset(); + System.out.printf("%d:%s - %s (%s)\n", cset.getRevisionIndex(), cset.getNodeid().shortNotation(), cset.getComment(), cset.getPhase()); + if (!isJoin && !isFork && !entry.children().isEmpty()) { + System.out.printf("\t=> %s\n", sb); + } + if (isJoin) { + HgChangeset p1 = entry.parents().first(); + HgChangeset p2 = entry.parents().second(); + System.out.printf("\tjoin <= (%d:%s, %d:%s)", p1.getRevisionIndex(), p1.getNodeid().shortNotation(), p2.getRevisionIndex(), p2.getNodeid().shortNotation()); + if (isFork) { + System.out.print(", "); } - if (isJoin) { - HgChangeset p1 = entry.parents().first(); - HgChangeset p2 = entry.parents().second(); - System.out.printf("\tjoin <= (%d:%s, %d:%s)", p1.getRevision(), p1.getNodeid().shortNotation(), p2.getRevision(), p2.getNodeid().shortNotation()); - if (isFork) { - System.out.print(", "); - } + } + if (isFork) { + if (!isJoin) { + System.out.print('\t'); } - if (isFork) { - if (!isJoin) { - System.out.print('\t'); - } - System.out.printf("fork => [%s]", sb); - } - if (isJoin || isFork) { - System.out.println(); - } - } catch (HgException ex) { - ex.printStackTrace(); + System.out.printf("fork => [%s]", sb); + } + if (isJoin || isFork) { + System.out.println(); } } }); @@ -273,7 +333,7 @@ System.out.print("]"); } fileLocalRevisions++; - } catch (HgException ex) { + } catch (HgRuntimeException ex) { ex.printStackTrace(); } } @@ -282,23 +342,23 @@ } private void testConsoleLog() { - LogFacility fc = new StreamLogFacility(true, true, true, System.out); - System.out.printf("isDebug: %s, isInfo:%s\n", fc.isDebug(), fc.isInfo()); - fc.debug(getClass(), "%d", 1); - fc.info(getClass(), "%d\n", 2); - fc.warn(getClass(), "%d\n", 3); - fc.error(getClass(), "%d", 4); + LogFacility fc = new StreamLogFacility(Debug, true, System.out); + System.out.printf("isDebug: %s, isInfo:%s\n", fc.isDebug(), fc.getLevel() == Info); + fc.dump(getClass(), Debug, "%d", 1); + fc.dump(getClass(), Info, "%d\n", 2); + fc.dump(getClass(), Warn, "%d\n", 3); + fc.dump(getClass(), Error, "%d", 4); Exception ex = new Exception(); - fc.debug(getClass(), ex, "message"); - fc.info(getClass(), ex, null); - fc.warn(getClass(), ex, null); - fc.error(getClass(), ex, "message"); + fc.dump(getClass(), Debug, ex, "message"); + fc.dump(getClass(), Info, ex, null); + fc.dump(getClass(), Warn, ex, null); + fc.dump(getClass(), Error, ex, "message"); } private void testTreeTraversal() throws Exception { File repoRoot = hgRepo.getWorkingDir(); Path.Source pathSrc = new Path.SimpleSource(new PathRewrite.Composite(new RelativePathRewrite(repoRoot), hgRepo.getToRepoPathHelper())); - FileWalker w = new FileWalker(repoRoot, pathSrc); + FileWalker w = new FileWalker(new BasicSessionContext(null), repoRoot, pathSrc); int count = 0; final long start = System.currentTimeMillis(); while (w.hasNext()) { @@ -321,7 +381,7 @@ */ private void testRevisionMap() throws Exception { HgChangelog changelog = hgRepo.getChangelog(); - HgChangelog.RevisionMap rmap = changelog.new RevisionMap().init(); // warm-up, ensure complete file read + HgRevisionMap rmap = new HgRevisionMap(changelog).init(); // warm-up, ensure complete file read int tip = changelog.getLastRevision(); // take 5 arbitrary revisions at 0, 1/4, 2/4, 3/4 and 4/4 final Nodeid[] revs = new Nodeid[5]; @@ -339,7 +399,7 @@ System.out.println(); // start = System.currentTimeMillis(); - rmap = changelog.new RevisionMap().init(); + rmap = new HgRevisionMap(changelog).init(); long s2 = System.currentTimeMillis(); for (int i = 0; i < revs.length; i++) { final int localRev = rmap.revisionIndex(revs[i]); @@ -350,7 +410,9 @@ } + // any repository with subrepositories private void testSubrepos() throws Exception { + // @see TestSubrepo#testAccessAPI for (HgSubrepoLocation l : hgRepo.getSubrepositories()) { System.out.println(l.getLocation()); System.out.println(l.getSource()); @@ -358,10 +420,10 @@ System.out.println(l.isCommitted() ? l.getRevision() : "not yet committed"); if (l.getType() == Kind.Hg) { HgRepository r = l.getRepo(); - System.out.printf("%s has %d revisions\n", l.getLocation(), r.getChangelog().getLastRevision() + 1); + System.out.printf("%s (%s) has %d revisions\n", l.getLocation(), r.getLocation(), r.getChangelog().getLastRevision() + 1); if (r.getChangelog().getLastRevision() >= 0) { final RawChangeset c = r.getChangelog().range(TIP, TIP).get(0); - System.out.printf("TIP: %s %s %s\n", c.user(), c.dateString(), c.comment()); + System.out.printf("TIP: %s %s '%s'\n", c.user(), c.dateString(), c.comment()); } } } @@ -428,7 +490,7 @@ final ByteArrayChannel sink = new ByteArrayChannel(); cmd.execute(sink); System.out.println(sink.toArray().length); - HgFileInformer i = new HgFileInformer(hgRepo); + HgChangesetFileSneaker i = new HgChangesetFileSneaker(hgRepo); boolean result = i.changeset(cset).checkExists(file); Assert.assertFalse(result); Assert.assertFalse(i.exists()); @@ -462,13 +524,13 @@ return String.format("%s %s (%d bytes)", r.getPath(), r.getRevision(), sink.toArray().length); } - private void testFileStatus() throws HgException, IOException { + private void testFileStatus() throws Exception { // final Path path = Path.create("src/org/tmatesoft/hg/util/"); // final Path path = Path.create("src/org/tmatesoft/hg/internal/Experimental.java"); // final Path path = Path.create("missing-dir/"); // HgWorkingCopyStatusCollector wcsc = HgWorkingCopyStatusCollector.create(hgRepo, path); HgWorkingCopyStatusCollector wcsc = HgWorkingCopyStatusCollector.create(hgRepo, new PathGlobMatcher("mi**")); - wcsc.walk(TIP, new StatusDump()); + wcsc.walk(WORKING_COPY, new StatusDump()); } /* @@ -575,15 +637,12 @@ hgRepo.getManifest().walk(0, TIP, new ManifestDump()); } - public static final class ManifestDump implements HgManifest.Inspector2 { + public static final class ManifestDump implements HgManifest.Inspector { public boolean begin(int manifestRevision, Nodeid nid, int changelogRevision) { System.out.printf("%d : %s\n", manifestRevision, nid); return true; } - public boolean next(Nodeid nid, String fname, String flags) { - throw new HgBadStateException(HgManifest.Inspector2.class.getName()); - } public boolean next(Nodeid nid, Path fname, Flags flags) { System.out.println(nid + "\t" + fname + "\t\t" + flags); return true; @@ -596,7 +655,7 @@ } private void dumpCompleteManifestHigh() throws Exception { - new HgManifestCommand(hgRepo).dirs(true).execute(new HgManifestCommand.Handler() { + new HgManifestCommand(hgRepo).dirs(true).execute(new HgManifestHandler() { public void begin(Nodeid manifestRevision) { System.out.println(">> " + manifestRevision); @@ -605,15 +664,11 @@ System.out.println(p); } public void file(HgFileRevision fileRevision) { - try { - System.out.print(fileRevision.getRevision());; - System.out.print(" "); - System.out.printf("%s %s", fileRevision.getParents().first().shortNotation(), fileRevision.getParents().second().shortNotation()); - System.out.print(" "); - System.out.println(fileRevision.getPath()); - } catch (HgException ex) { - throw new HgCallbackTargetException.Wrap(ex); - } + System.out.print(fileRevision.getRevision());; + System.out.print(" "); + System.out.printf("%s %s", fileRevision.getParents().first().shortNotation(), fileRevision.getParents().second().shortNotation()); + System.out.print(" "); + System.out.println(fileRevision.getPath()); } public void end(Nodeid manifestRevision) { @@ -641,7 +696,7 @@ sc.change(0, dump); System.out.println("\nStatus against working dir:"); HgWorkingCopyStatusCollector wcc = new HgWorkingCopyStatusCollector(hgRepo); - wcc.walk(TIP, dump); + wcc.walk(WORKING_COPY, dump); System.out.println(); System.out.printf("Manifest of the revision %d:\n", r2); hgRepo.getManifest().walk(r2, r2, new ManifestDump()); @@ -677,7 +732,7 @@ // expected: 359, 2123, 3079 byte[] b = s.getBytes(); final Nodeid nid = Nodeid.fromAscii(b, 0, b.length); - System.out.println(s + " : " + n.length(nid)); + System.out.println(s + " : " + n.getLength(nid)); } } diff -r 2078692eeb58 -r 7bcfbc255f48 cmdline/org/tmatesoft/hg/console/Manifest.java --- a/cmdline/org/tmatesoft/hg/console/Manifest.java Thu Jun 21 21:36:06 2012 +0200 +++ b/cmdline/org/tmatesoft/hg/console/Manifest.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -19,9 +19,13 @@ import static org.tmatesoft.hg.console.Options.asSet; import static org.tmatesoft.hg.repo.HgRepository.TIP; +import org.tmatesoft.hg.core.HgManifestHandler; import org.tmatesoft.hg.core.HgFileRevision; import org.tmatesoft.hg.core.HgManifestCommand; import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.HgInvalidControlFileException; +import org.tmatesoft.hg.repo.HgInvalidRevisionException; +import org.tmatesoft.hg.repo.HgManifest; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.util.Path; @@ -42,27 +46,43 @@ } final boolean debug = cmdLineOpts.getBoolean("--debug"); final boolean verbose = cmdLineOpts.getBoolean("-v", "--verbose"); - HgManifestCommand.Handler h = new HgManifestCommand.Handler() { + HgManifestHandler h = new HgManifestHandler() { public void begin(Nodeid manifestRevision) { } public void dir(Path p) { } public void file(HgFileRevision fileRevision) { - if (debug) { - System.out.print(fileRevision.getRevision());; + try { + if (debug) { + System.out.print(fileRevision.getRevision());; + } + if (debug || verbose) { + HgManifest.Flags flags = fileRevision.getFileFlags(); + Object s; + if (flags == HgManifest.Flags.RegularFile) { + s = Integer.toOctalString(0644); + } else if (flags == HgManifest.Flags.Exec) { + s = Integer.toOctalString(0755); + } else if (flags == HgManifest.Flags.Link) { + s = "lnk"; + } else { + s = String.valueOf(flags); + } + System.out.printf(" %s ", s); + } + System.out.println(fileRevision.getPath()); + } catch (HgInvalidControlFileException e) { + e.printStackTrace(); + } catch (HgInvalidRevisionException e) { + e.printStackTrace(); } - if (debug || verbose) { - System.out.print(" 644"); // FIXME real flags! - System.out.print(" "); - } - System.out.println(fileRevision.getPath()); } public void end(Nodeid manifestRevision) { } }; int rev = cmdLineOpts.getSingleInt(TIP, "-r", "--rev"); - new HgManifestCommand(hgRepo).dirs(false).revision(rev).execute(h); + new HgManifestCommand(hgRepo).dirs(false).changeset(rev).execute(h); } } diff -r 2078692eeb58 -r 7bcfbc255f48 cmdline/org/tmatesoft/hg/console/Status.java --- a/cmdline/org/tmatesoft/hg/console/Status.java Thu Jun 21 21:36:06 2012 +0200 +++ b/cmdline/org/tmatesoft/hg/console/Status.java Wed Jul 11 20:40:47 2012 +0200 @@ -33,6 +33,7 @@ import org.tmatesoft.hg.core.HgStatus.Kind; import org.tmatesoft.hg.core.HgStatusCommand; import org.tmatesoft.hg.core.HgStatusHandler; +import org.tmatesoft.hg.util.Outcome; import org.tmatesoft.hg.util.Path; /** @@ -74,7 +75,7 @@ final EnumMap> data = new EnumMap>(HgStatus.Kind.class); final Map copies = showCopies ? new HashMap() : null; - public void handleStatus(HgStatus s) { + public void status(HgStatus s) { List l = data.get(s.getKind()); if (l == null) { l = new LinkedList(); @@ -86,7 +87,7 @@ } } - public void handleError(Path file, org.tmatesoft.hg.util.Status s) { + public void error(Path file, Outcome s) { System.out.printf("FAILURE: %s %s\n", s.getMessage(), file); s.getException().printStackTrace(System.out); } diff -r 2078692eeb58 -r 7bcfbc255f48 cmdline/org/tmatesoft/hg/console/Tags.java --- a/cmdline/org/tmatesoft/hg/console/Tags.java Thu Jun 21 21:36:06 2012 +0200 +++ b/cmdline/org/tmatesoft/hg/console/Tags.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -54,7 +54,7 @@ return x1 < x2 ? 1 : -1; } }); - for (TagInfo ti : tags.getTags().values()) { + for (TagInfo ti : tags.getAllTags().values()) { int x = clog.getRevisionIndex(ti.revision()); // XXX in fact, performance hog. Need batch revisionIndex or another improvement ti2index.put(ti, x); sorted.add(ti); diff -r 2078692eeb58 -r 7bcfbc255f48 design.txt --- a/design.txt Thu Jun 21 21:36:06 2012 +0200 +++ b/design.txt Wed Jul 11 20:40:47 2012 +0200 @@ -44,6 +44,7 @@ delta merge DataAccess - collect debug info (buffer misses, file size/total read operations) to find out better strategy to buffer size detection. Compare performance. +RevlogStream - inflater buffer (and other buffers) size may be too small for repositories out there (i.e. inflater buffer of 512 bytes for 200k revision) Parameterize StatusCollector to produce copy only when needed. And HgDataFile.metadata perhaps should be moved to cacheable place? @@ -138,4 +139,10 @@ Unfortunately, Revision would be a nice name for a class . As long as I don't want to keep methods to access int/nodeid separately and not to stick to Revision struct only (to avoid massive instances of Revision when only one is sufficient), I'll need to name these separate methods anyway. Present opinion is that I don't need the object right now (will have to live with RevisionObject or RevisionDescriptor -once change my mind) \ No newline at end of file +once change my mind) + +Handlers (HgStatusHandler, HgManifestHandler, HgChangesetHandler, HgChangesetTreeHandler) +methods DO NOT throw CancelledException. cancellation is separate from processing logic. handlers can implements CancelSupport to become a source of cancellation, if necessary +methods DO throw HgCallbackTargetException to propagate own errors/exceptions +methods are supposed to silently pass HgRuntimeExceptions (although callback implementers may decide to wrap them into HgCallbackTargetException) +descriptive names for the methods, whenever possible (not bare #next) \ No newline at end of file diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/ChangesetTransformer.java --- a/src/org/tmatesoft/hg/core/ChangesetTransformer.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/ChangesetTransformer.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -18,24 +18,26 @@ import java.util.Set; +import org.tmatesoft.hg.internal.PathPool; import org.tmatesoft.hg.repo.HgChangelog; import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.repo.HgStatusCollector; +import org.tmatesoft.hg.repo.HgParentChildMap; +import org.tmatesoft.hg.util.Adaptable; import org.tmatesoft.hg.util.CancelSupport; import org.tmatesoft.hg.util.CancelledException; -import org.tmatesoft.hg.util.PathPool; import org.tmatesoft.hg.util.PathRewrite; import org.tmatesoft.hg.util.ProgressSupport; /** * Bridges {@link HgChangelog.RawChangeset} with high-level {@link HgChangeset} API - * FIXME move to .internal once access to package-local HgChangeset cons is resolved + * TODO post-1.0 Move to .internal once access to package-local HgChangeset cons is resolved. For 1.0, enough it's package-local * * @author Artem Tikhomirov * @author TMate Software Ltd. */ -/*package-local*/ class ChangesetTransformer implements HgChangelog.Inspector { +/*package-local*/ class ChangesetTransformer implements HgChangelog.Inspector, Adaptable, CancelSupport { private final HgChangesetHandler handler; private final ProgressSupport progressHelper; private final CancelSupport cancelHelper; @@ -46,7 +48,7 @@ // repo and delegate can't be null, parent walker can // ps and cs can't be null - public ChangesetTransformer(HgRepository hgRepo, HgChangesetHandler delegate, HgChangelog.ParentWalker pw, ProgressSupport ps, CancelSupport cs) { + public ChangesetTransformer(HgRepository hgRepo, HgChangesetHandler delegate, HgParentChildMap pw, ProgressSupport ps, CancelSupport cs) { if (hgRepo == null || delegate == null) { throw new IllegalArgumentException(); } @@ -56,25 +58,24 @@ HgStatusCollector statusCollector = new HgStatusCollector(hgRepo); t = new Transformation(statusCollector, pw); handler = delegate; + // we let HgChangelog#range deal with progress (pipe through getAdapter) + // but use own cancellation (which involves CallbackTargetException as well, and preserves original cancellation + // exception in case clients care) cancelHelper = cs; progressHelper = ps; } public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { - if (failure != null || cancellation != null) { - return; // FIXME need a better way to stop iterating revlog - } if (branches != null && !branches.contains(cset.branch())) { return; } HgChangeset changeset = t.handle(revisionNumber, nodeid, cset); try { - handler.next(changeset); - progressHelper.worked(1); + handler.cset(changeset); cancelHelper.checkCancelled(); - } catch (RuntimeException ex) { - failure = new HgCallbackTargetException(ex).setRevision(nodeid).setRevisionIndex(revisionNumber); + } catch (HgCallbackTargetException ex) { + failure = ex.setRevision(nodeid).setRevisionIndex(revisionNumber); } catch (CancelledException ex) { cancellation = ex; } @@ -101,17 +102,37 @@ static class Transformation { private final HgChangeset changeset; - public Transformation(HgStatusCollector statusCollector, HgChangelog.ParentWalker pw) { + public Transformation(HgStatusCollector statusCollector, HgParentChildMap pw) { // files listed in a changeset don't need their names to be rewritten (they are normalized already) + // pp serves as a cache for all filenames encountered and as a source for Path listed in the changeset PathPool pp = new PathPool(new PathRewrite.Empty()); statusCollector.setPathPool(pp); changeset = new HgChangeset(statusCollector, pp); changeset.setParentHelper(pw); } - + + /** + * Callers shall not assume they get new HgChangeset instance each time, implementation may reuse instances. + * @return hi-level changeset description + */ HgChangeset handle(int revisionNumber, Nodeid nodeid, RawChangeset cset) { changeset.init(revisionNumber, nodeid, cset); return changeset; } } + + public void checkCancelled() throws CancelledException { + if (failure != null || cancellation != null) { + // stop HgChangelog.Iterator. Our exception is for the purposes of cancellation only, + // the one we have stored (this.cancellation) is for user + throw new CancelledException(); + } + } + + public T getAdapter(Class adapterClass) { + if (adapterClass == ProgressSupport.class) { + return adapterClass.cast(progressHelper); + } + return null; + } } \ No newline at end of file diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgBadArgumentException.java --- a/src/org/tmatesoft/hg/core/HgBadArgumentException.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgBadArgumentException.java Wed Jul 11 20:40:47 2012 +0200 @@ -34,4 +34,10 @@ public HgBadArgumentException(String message, Throwable cause) { super(message, cause); } + + @Override + public HgBadArgumentException setRevision(Nodeid r) { + super.setRevision(r); + return this; + } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgBadStateException.java --- a/src/org/tmatesoft/hg/core/HgBadStateException.java Thu Jun 21 21:36:06 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2011 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.core; - -/** - * hg4j's own internal error or unexpected state. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -@SuppressWarnings("serial") -public class HgBadStateException extends RuntimeException { - - // FIXME quick-n-dirty fix, don't allow exceptions without a cause - public HgBadStateException() { - super("Internal error"); - } - - public HgBadStateException(String message) { - super(message); - } - - public HgBadStateException(Throwable cause) { - super(cause); - } -} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgCallbackTargetException.java --- a/src/org/tmatesoft/hg/core/HgCallbackTargetException.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgCallbackTargetException.java Wed Jul 11 20:40:47 2012 +0200 @@ -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,25 +16,33 @@ */ package org.tmatesoft.hg.core; +import org.tmatesoft.hg.internal.ExceptionInfo; import org.tmatesoft.hg.util.Path; /** - * Checked exception that indicates errors in client code and tries to supply extra information about the context it occurred in. + * Checked exception that client supplied callback code can use to indicates its own errors. * - * Generally, client need to pass own error information/exceptions from within implementations of the callback methods they supply. + *

Generally, client need to pass own error information/exceptions from within implementations of the callback methods they supply. * However, there's no straightforward way to alter throws clause for these methods, and alternatives like generic {@link Exception} or * library's own {@link HgException} are rather obscure. Suggested approach is to wrap whatever exception user code produces with - * {@link RuntimeException} subclass, {@link Wrap}. Then, unwrap and re-throw with checked {@link HgCallbackTargetException}. + * {@link HgCallbackTargetException}, the only checked exception allowed out from a callback. * - * FIXME REVISIT perhaps, shall just throw HgCallbackTargetException from any handler, and do not catch anything in commands at all? + *

It's intentionally not a subclass of {@link HgException} to avoid get mixed with library own errors and be processed separately. + * + *

Top-level API handlers ({@link HgStatusHandler}, {@link HgManifestHandler}, {@link HgChangesetHandler}, etc) allow to throw + * HgCallbackTargetException from their methods. Exceptions throws this way are not handled in corresponding commands, except for + * revision or file name specification, unless already set. The, these exceptions go straight to the command caller. * * @author Artem Tikhomirov * @author TMate Software Ltd. */ @SuppressWarnings("serial") -public class HgCallbackTargetException extends HgException { +public class HgCallbackTargetException extends Exception { + + protected final ExceptionInfo details = new ExceptionInfo(this); + /** * @param cause can't be null */ @@ -43,16 +51,11 @@ if (cause == null) { throw new IllegalArgumentException(); } - if (cause.getClass() == Wrap.class) { - // eliminate wrapper - initCause(cause.getCause()); - } else { - initCause(cause); - } + initCause(cause); } @SuppressWarnings("unchecked") - public T getTargetException() { + public T getTargetException() { return (T) getCause(); } @@ -63,39 +66,22 @@ @Override public String getMessage() { StringBuilder sb = new StringBuilder(); - sb.append("Original exception thrown: "); + sb.append("Error from callback. Original exception thrown: "); sb.append(getCause().getClass().getName()); sb.append(" at "); - appendDetails(sb); + details.appendDetails(sb); return sb.toString(); } - @Override public HgCallbackTargetException setRevision(Nodeid r) { - return (HgCallbackTargetException) super.setRevision(r); - } - @Override - public HgCallbackTargetException setRevisionIndex(int rev) { - return (HgCallbackTargetException) super.setRevisionIndex(rev); - } - @Override - public HgCallbackTargetException setFileName(Path name) { - return (HgCallbackTargetException) super.setFileName(name); + return details.setRevision(r); } - /** - * Given the approach high-level handlers throw RuntimeExceptions to indicate errors, and - * a need to throw reasonable checked exception from client code, clients may utilize this class - * to get their checked exceptions unwrapped by {@link HgCallbackTargetException} and serve as that - * exception cause, eliminating {@link RuntimeException} mediator. - */ - public static final class Wrap extends RuntimeException { + public HgCallbackTargetException setRevisionIndex(int rev) { + return details.setRevisionIndex(rev); + } - public Wrap(Throwable cause) { - super(cause); - if (cause == null) { - throw new IllegalArgumentException(); - } - } + public HgCallbackTargetException setFileName(Path name) { + return details.setFileName(name); } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgCatCommand.java --- a/src/org/tmatesoft/hg/core/HgCatCommand.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgCatCommand.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -20,12 +20,12 @@ import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; import static org.tmatesoft.hg.repo.HgRepository.TIP; -import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; import org.tmatesoft.hg.repo.HgDataFile; import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgRuntimeException; import org.tmatesoft.hg.util.Adaptable; import org.tmatesoft.hg.util.ByteChannel; import org.tmatesoft.hg.util.CancelSupport; @@ -132,10 +132,14 @@ * Runs the command with current set of parameters and pipes data to provided sink. * * @param sink output channel to write data to. - * @throws HgDataStreamException + * + * @throws HgBadArgumentException if no target file node found + * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state + * @throws CancelledException if execution of the command was cancelled * @throws IllegalArgumentException when command arguments are incomplete or wrong */ - public void execute(ByteChannel sink) throws HgDataStreamException, HgInvalidControlFileException, CancelledException { + public void execute(ByteChannel sink) throws HgException, CancelledException { + // XXX perhaps, IAE together with HgBadArgumentException is not the best idea if (revisionIndex == BAD_REVISION && revision == null && cset == null) { throw new IllegalArgumentException("File revision, corresponing local number, or a changset nodeid shall be specified"); } @@ -145,46 +149,53 @@ if (sink == null) { throw new IllegalArgumentException("Need an output channel"); } - HgDataFile dataFile = repo.getFileNode(file); - if (!dataFile.exists()) { - throw new HgDataStreamException(file, new FileNotFoundException(file.toString())); - } - int revToExtract; - if (cset != null) { - int csetRev = repo.getChangelog().getRevisionIndex(cset); - Nodeid toExtract = null; - do { - toExtract = repo.getManifest().getFileRevision(csetRev, file); + try { + HgDataFile dataFile = repo.getFileNode(file); + if (!dataFile.exists()) { + // TODO may benefit from repo.getStoragePath to print revlog location in addition to human-friendly file path + throw new HgPathNotFoundException(String.format("File %s not found in the repository", file), file); + } + int revToExtract; + if (cset != null) { + int csetRev = repo.getChangelog().getRevisionIndex(cset); + Nodeid toExtract = null; + do { + // TODO post-1.0 perhaps, HgChangesetFileSneaker may come handy? + toExtract = repo.getManifest().getFileRevision(csetRev, file); + if (toExtract == null) { + if (dataFile.isCopy()) { + file = dataFile.getCopySourceName(); + dataFile = repo.getFileNode(file); + } else { + break; + } + } + } while (toExtract == null); if (toExtract == null) { - if (dataFile.isCopy()) { - file = dataFile.getCopySourceName(); - dataFile = repo.getFileNode(file); - } else { - break; - } + String m = String.format("File %s nor its origins were known at repository's %s revision", file, cset.shortNotation()); + throw new HgPathNotFoundException(m, file).setRevision(cset); } - } while (toExtract == null); - if (toExtract == null) { - throw new HgBadStateException(String.format("File %s nor its origins were not known at repository %s revision", file, cset.shortNotation())); + revToExtract = dataFile.getRevisionIndex(toExtract); + } else if (revision != null) { + revToExtract = dataFile.getRevisionIndex(revision); + } else { + revToExtract = revisionIndex; } - revToExtract = dataFile.getRevisionIndex(toExtract); - } else if (revision != null) { - revToExtract = dataFile.getRevisionIndex(revision); - } else { - revToExtract = revisionIndex; + ByteChannel sinkWrap; + if (getCancelSupport(null, false) == null) { + // no command-specific cancel helper, no need for extra proxy + // sink itself still may supply CS + sinkWrap = sink; + } else { + // try CS from sink, if any. at least there is CS from command + CancelSupport cancelHelper = getCancelSupport(sink, true); + cancelHelper.checkCancelled(); + sinkWrap = new ByteChannelProxy(sink, cancelHelper); + } + dataFile.contentWithFilters(revToExtract, sinkWrap); + } catch (HgRuntimeException ex) { + throw new HgLibraryFailureException(ex); } - ByteChannel sinkWrap; - if (getCancelSupport(null, false) == null) { - // no command-specific cancel helper, no need for extra proxy - // sink itself still may supply CS - sinkWrap = sink; - } else { - // try CS from sink, if any. at least there is CS from command - CancelSupport cancelHelper = getCancelSupport(sink, true); - cancelHelper.checkCancelled(); - sinkWrap = new ByteChannelProxy(sink, cancelHelper); - } - dataFile.contentWithFilters(revToExtract, sinkWrap); } private static class ByteChannelProxy implements ByteChannel, Adaptable { diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgChangeset.java --- a/src/org/tmatesoft/hg/core/HgChangeset.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgChangeset.java Wed Jul 11 20:40:47 2012 +0200 @@ -25,8 +25,12 @@ import org.tmatesoft.hg.repo.HgChangelog; import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; import org.tmatesoft.hg.repo.HgPhase; +import org.tmatesoft.hg.repo.HgInvalidStateException; import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgRuntimeException; import org.tmatesoft.hg.repo.HgStatusCollector; +import org.tmatesoft.hg.repo.HgParentChildMap; +import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.Path; @@ -53,7 +57,7 @@ public final HgStatusCollector statusHelper; public final Path.Source pathHelper; - public HgChangelog.ParentWalker parentHelper; + public HgParentChildMap parentHelper; public PhasesHelper phaseHelper; }; @@ -82,7 +86,7 @@ // keep references to shared (and everything in there: parentHelper, statusHelper, phaseHelper and pathHelper) } - /*package-local*/ void setParentHelper(HgChangelog.ParentWalker pw) { + /*package-local*/ void setParentHelper(HgParentChildMap pw) { if (pw != null) { if (pw.getRepo() != shared.statusHelper.getRepo()) { throw new IllegalArgumentException(); @@ -91,18 +95,44 @@ shared.parentHelper = pw; } - public int getRevision() { + /** + * Index of the changeset in local repository. Note, this number is relevant only for local repositories/operations, use + * {@link Nodeid nodeid} to uniquely identify a revision. + * + * @return index of the changeset revision + */ + public int getRevisionIndex() { return revNumber; } + + /** + * Unique identity of this changeset revision + * @return revision identifier, never null + */ public Nodeid getNodeid() { return nodeid; } + + /** + * Name of the user who made this commit + * @return author of the commit, never null + */ public String getUser() { return changeset.user(); } + + /** + * Commit description + * @return content of the corresponding field in changeset record; empty string if none specified. + */ public String getComment() { return changeset.comment(); } + + /** + * Name of the branch this commit was made in. Returns "default" for main branch. + * @return name of the branch, non-empty string + */ public String getBranch() { return changeset.branch(); } @@ -113,10 +143,26 @@ public HgDate getDate() { return new HgDate(changeset.date().getTime(), changeset.timezone()); } + + /** + * Indicates revision of manifest that tracks state of repository at the moment of this commit. + * Note, may be {@link Nodeid#NULL} in certain scenarios (e.g. first changeset in an empty repository, usually by bogus tools) + * + * @return revision identifier, never null + */ public Nodeid getManifestRevision() { return changeset.manifest(); } + /** + * Lists names of files affected by this commit, as recorded in the changeset itself. Unlike {@link #getAddedFiles()}, + * {@link #getModifiedFiles()} and {@link #getRemovedFiles()}, this method doesn't analyze actual changes done + * in the commit, rather extracts value from the changeset record. + * + * List returned by this method may be empty, while aforementioned methods may produce non-empty result. + * + * @return list of filenames, never null + */ public List getAffectedFiles() { // reports files as recorded in changelog. Note, merge revisions may have no // files listed, and thus this method would return empty list, while @@ -129,37 +175,55 @@ return rv; } - public List getModifiedFiles() throws HgInvalidControlFileException { + /** + * Figures out files and specific revisions thereof that were modified in this commit + * + * @return revisions of files modified in this commit + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public List getModifiedFiles() throws HgRuntimeException { if (modifiedFiles == null) { initFileChanges(); } return modifiedFiles; } - public List getAddedFiles() throws HgInvalidControlFileException { + /** + * Figures out files added in this commit + * + * @return revisions of files added in this commit + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public List getAddedFiles() throws HgRuntimeException { if (addedFiles == null) { initFileChanges(); } return addedFiles; } - public List getRemovedFiles() throws HgInvalidControlFileException { + /** + * Figures out files that were deleted as part of this commit + * + * @return revisions of files deleted in this commit + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public List getRemovedFiles() throws HgRuntimeException { if (deletedFiles == null) { initFileChanges(); } return deletedFiles; } - public boolean isMerge() throws HgInvalidControlFileException { + public boolean isMerge() throws HgRuntimeException { // p1 == -1 and p2 != -1 is legitimate case return !(getFirstParentRevision().isNull() || getSecondParentRevision().isNull()); } /** * @return never null - * @throws HgInvalidControlFileException if access to revlog index/data entry failed + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public Nodeid getFirstParentRevision() throws HgInvalidControlFileException { + public Nodeid getFirstParentRevision() throws HgRuntimeException { if (shared.parentHelper != null) { return shared.parentHelper.safeFirstParent(nodeid); } @@ -174,9 +238,9 @@ /** * @return never null - * @throws HgInvalidControlFileException if access to revlog index/data entry failed + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public Nodeid getSecondParentRevision() throws HgInvalidControlFileException { + public Nodeid getSecondParentRevision() throws HgRuntimeException { if (shared.parentHelper != null) { return shared.parentHelper.safeSecondParent(nodeid); } @@ -187,12 +251,13 @@ } return Nodeid.fromBinary(parent2, 0); } - - /** + + /** * Tells the phase this changeset belongs to. * @return one of {@link HgPhase} values + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public HgPhase getPhase() throws HgInvalidControlFileException { + public HgPhase getPhase() throws HgRuntimeException { if (shared.phaseHelper == null) { // XXX would be handy to obtain ProgressSupport (perhaps, from statusHelper?) // and pass it to #init(), so that there could be indication of file being read and cache being built @@ -206,6 +271,9 @@ return shared.phaseHelper.getPhase(this); } + /** + * Create a copy of this changeset + */ @Override public HgChangeset clone() { try { @@ -221,27 +289,32 @@ return shared.statusHelper.getRepo(); } - private /*synchronized*/ void initFileChanges() throws HgInvalidControlFileException { + private /*synchronized*/ void initFileChanges() throws HgRuntimeException { ArrayList deleted = new ArrayList(); ArrayList modified = new ArrayList(); ArrayList added = new ArrayList(); HgStatusCollector.Record r = new HgStatusCollector.Record(); - shared.statusHelper.change(revNumber, r); + try { + shared.statusHelper.change(revNumber, r); + } catch (CancelledException ex) { + // Record can't cancel + throw new HgInvalidStateException("Internal error"); + } final HgRepository repo = getRepo(); for (Path s : r.getModified()) { Nodeid nid = r.nodeidAfterChange(s); if (nid == null) { - throw new HgBadStateException(); + throw new HgInvalidStateException(String.format("For the file %s recorded as modified in changeset %d couldn't find revision after change", s, revNumber)); } - modified.add(new HgFileRevision(repo, nid, s, null)); + modified.add(new HgFileRevision(repo, nid, null, s, null)); } final Map copied = r.getCopied(); for (Path s : r.getAdded()) { Nodeid nid = r.nodeidAfterChange(s); if (nid == null) { - throw new HgBadStateException(); + throw new HgInvalidStateException(String.format("For the file %s recorded as added in changeset %d couldn't find revision after change", s, revNumber)); } - added.add(new HgFileRevision(repo, nid, s, copied.get(s))); + added.add(new HgFileRevision(repo, nid, null, s, copied.get(s))); } for (Path s : r.getRemoved()) { // with Path from getRemoved, may just copy diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgChangesetFileSneaker.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/core/HgChangesetFileSneaker.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,226 @@ +/* + * 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 + * 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.core; + +import org.tmatesoft.hg.internal.ManifestRevision; +import org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgInvalidStateException; +import org.tmatesoft.hg.repo.HgManifest; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgRuntimeException; +import org.tmatesoft.hg.util.Path; +import org.tmatesoft.hg.util.Outcome; + +/** + * Primary purpose is to provide information about file revisions at specific changeset. Multiple {@link #check(Path)} calls + * are possible once {@link #changeset(Nodeid)} (and optionally, {@link #followRenames(boolean)}) were set. + * + *

Sample: + *


+ *   HgChangesetFileSneaker i = new HgChangesetFileSneaker(hgRepo).changeset(Nodeid.fromString("<40 digits>")).followRenames(true);
+ *   if (i.check(file)) {
+ *   	HgCatCommand catCmd = new HgCatCommand(hgRepo).revision(i.getFileRevision());
+ *   	catCmd.execute(...);
+ *   	...
+ *   }
+ * 
+ * + * TODO may add #manifest(Nodeid) to select manifest according to its revision (not only changeset revision as it's now) + * + *

Unlike {@link HgManifest#getFileRevision(int, Path)}, this class is useful when few files from the same changeset have to be inspected + * + * @see HgManifest#getFileRevision(int, Path) + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public final class HgChangesetFileSneaker { + + private final HgRepository repo; + private boolean followRenames; + private Nodeid cset; + private ManifestRevision cachedManifest; + private HgFileRevision fileRevision; + private boolean renamed; + private Outcome checkResult; + + public HgChangesetFileSneaker(HgRepository hgRepo) { + repo = hgRepo; + } + + /** + * Select specific changelog revision + * + * @param nid changeset identifier + * @return this for convenience + */ + public HgChangesetFileSneaker changeset(Nodeid nid) { + if (nid == null || nid.isNull()) { + throw new IllegalArgumentException(); + } + cset = nid; + cachedManifest = null; + fileRevision = null; + return this; + } + + /** + * Whether to check file origins, default is false (look up only the name supplied) + * + * @param follow true to check copy/rename origin of the file if it is a copy. + * @return this for convenience + */ + public HgChangesetFileSneaker followRenames(boolean follow) { + followRenames = follow; + fileRevision = null; + return this; + } + + /** + * Shortcut to perform {@link #check(Path)} and {@link #exists()}. Result of the check may be accessed via {@link #getCheckResult()}. + * Errors during the check, if any, are reported through exception. + * + * @param file name of the file in question + * @return true if file is known at the selected changeset. + * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state + * @throws IllegalArgumentException if {@link #changeset(Nodeid)} not specified or file argument is bad. + */ + public boolean checkExists(Path file) throws HgException { + check(file); + // next seems reasonable, however renders boolean return value useless. perhaps void or distinct method? +// if (checkResult.isOk() && !exists()) { +// throw new HgPathNotFoundException(checkResult.getMessage(), file); +// } + if (!checkResult.isOk() && checkResult.getException() instanceof HgRuntimeException) { + throw new HgLibraryFailureException((HgRuntimeException) checkResult.getException()); + } + return checkResult.isOk() && exists(); + } + + /** + * Find file (or its origin, if {@link #followRenames(boolean)} was set to true) among files known at specified {@link #changeset(Nodeid)}. + * + * @param file name of the file in question + * @return status object that describes outcome, {@link Outcome#isOk() Ok} status indicates successful completion of the operation, but doesn't imply + * file existence, use {@link #exists()} for that purpose. Message of the status may provide further hints on what exactly had happened. + * @throws IllegalArgumentException if {@link #changeset(Nodeid)} not specified or file argument is bad. + */ + public Outcome check(Path file) { + fileRevision = null; + checkResult = null; + renamed = false; + if (cset == null || file == null || file.isDirectory()) { + throw new IllegalArgumentException(); + } + HgDataFile dataFile = repo.getFileNode(file); + if (!dataFile.exists()) { + checkResult = new Outcome(Outcome.Kind.Success, String.format("File named %s is not known in the repository", file)); + return checkResult; + } + Nodeid toExtract = null; + HgManifest.Flags extractRevFlags = null; + String phaseMsg = "Extract manifest revision failed"; + try { + if (cachedManifest == null) { + int csetRev = repo.getChangelog().getRevisionIndex(cset); + cachedManifest = new ManifestRevision(null, null); // XXX how about context and cached manifest revisions + repo.getManifest().walk(csetRev, csetRev, cachedManifest); + // cachedManifest shall be meaningful - changelog.getRevisionIndex() above ensures we've got version that exists. + } + toExtract = cachedManifest.nodeid(file); + extractRevFlags = cachedManifest.flags(file); + phaseMsg = "Follow copy/rename failed"; + if (toExtract == null && followRenames) { + while (toExtract == null && dataFile.isCopy()) { + renamed = true; + file = dataFile.getCopySourceName(); + dataFile = repo.getFileNode(file); + toExtract = cachedManifest.nodeid(file); + extractRevFlags = cachedManifest.flags(file); + } + } + } catch (HgRuntimeException ex) { + checkResult = new Outcome(Outcome.Kind.Failure, phaseMsg, ex); + return checkResult; + } + if (toExtract != null) { + fileRevision = new HgFileRevision(repo, toExtract, extractRevFlags, dataFile.getPath()); + checkResult = new Outcome(Outcome.Kind.Success, String.format("File %s, revision %s found at changeset %s", dataFile.getPath(), toExtract.shortNotation(), cset.shortNotation())); + return checkResult; + } + checkResult = new Outcome(Outcome.Kind.Success, String.format("File %s nor its origins were known at repository %s revision", file, cset.shortNotation())); + return checkResult; + } + + /** + * Re-get latest check status object + */ + public Outcome getCheckResult() { + assertCheckRan(); + return checkResult; + } + + /** + * @return result of the last {@link #check(Path)} call. + */ + public boolean exists() { + assertCheckRan(); + return fileRevision != null; + } + + /** + * @return true if checked file was known by another name at the time of specified changeset. + */ + public boolean hasAnotherName() { + assertCheckRan(); + return renamed; + } + + /** + * @return holder for file revision information + */ + public HgFileRevision getFileRevision() { + assertCheckRan(); + return fileRevision; + } + + /** + * Name of the checked file as it was known at the time of the specified changeset. + * + * @return handy shortcut for getFileRevision().getPath() + */ + public Path filename() { + assertCheckRan(); + return fileRevision.getPath(); + } + + /** + * Revision of the checked file + * + * @return handy shortcut for getFileRevision().getRevision() + */ + public Nodeid revision() { + assertCheckRan(); + return fileRevision.getRevision(); + } + + private void assertCheckRan() { + if (checkResult == null) { + throw new HgInvalidStateException("Shall invoke #check(Path) first"); + } + } + +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgChangesetHandler.java --- a/src/org/tmatesoft/hg/core/HgChangesetHandler.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgChangesetHandler.java Wed Jul 11 20:40:47 2012 +0200 @@ -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,7 +16,8 @@ */ package org.tmatesoft.hg.core; -import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.internal.Callback; +import org.tmatesoft.hg.util.Path; /** * Callback to process {@link HgChangeset changesets}. @@ -24,12 +25,32 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public interface HgChangesetHandler/*XXX perhaps, shall parameterize with exception clients can throw, like: */ { +@Callback +public interface HgChangesetHandler { + /** - * @param changeset not necessarily a distinct instance each time, {@link HgChangeset#clone() clone()} if need a copy. - * @throws CancelledException if handler is not interested in more changesets and iteration shall stop - * @throws RuntimeException or any subclass thereof to indicate error. General contract is that RuntimeExceptions - * will be re-thrown wrapped into {@link HgCallbackTargetException}. + * @param changeset descriptor of a change, not necessarily a distinct instance each time, {@link HgChangeset#clone() clone()} if need a copy. + * @throws HgCallbackTargetException wrapper for any exception user code may produce */ - void next(HgChangeset changeset) throws CancelledException; + void cset(HgChangeset changeset) throws HgCallbackTargetException; + + + /** + * When {@link HgLogCommand} is executed against file, handler passed to {@link HgLogCommand#execute(HgChangesetHandler)} may optionally + * implement this interface to get information about file renames. Method {@link #copy(HgFileRevision, HgFileRevision)} would + * get invoked prior any changeset of the original file (if file history being followed) is reported via {@link #cset(HgChangeset)}. + * + * For {@link HgLogCommand#file(Path, boolean)} with renamed file path and follow argument set to false, + * {@link #copy(HgFileRevision, HgFileRevision)} would be invoked for the first copy/rename in the history of the file, but not + * followed by any changesets. + */ + @Callback + public interface WithCopyHistory extends HgChangesetHandler { + // XXX perhaps, should distinguish copy from rename? And what about merged revisions and following them? + + /** + * @throws HgCallbackTargetException wrapper object for any exception user code may produce + */ + void copy(HgFileRevision from, HgFileRevision to) throws HgCallbackTargetException; + } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgChangesetTreeHandler.java --- a/src/org/tmatesoft/hg/core/HgChangesetTreeHandler.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgChangesetTreeHandler.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -18,22 +18,25 @@ import java.util.Collection; -import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.internal.Callback; import org.tmatesoft.hg.util.Pair; /** - * + * Handler to iterate file history (generally, any revlog) with access to parent-child relations between changesets. + * + * @see HgLogCommand#execute(HgChangesetTreeHandler) + * * @author Artem Tikhomirov * @author TMate Software Ltd. */ +@Callback public interface HgChangesetTreeHandler { /** * @param entry access to various pieces of information about current tree node. Instances might be * reused across calls and shall not be kept by client's code - * @throws HgCallbackTargetException.Wrap wrapper object for any exception user code may produce. Wrapped exception would get re-thrown with {@link HgCallbackTargetException} - * @throws CancelledException if execution of the operation was cancelled + * @throws HgCallbackTargetException wrapper for any exception user code may produce */ - public void next(HgChangesetTreeHandler.TreeElement entry) throws HgCallbackTargetException.Wrap, CancelledException; + public void treeElement(HgChangesetTreeHandler.TreeElement entry) throws HgCallbackTargetException; interface TreeElement { /** @@ -45,22 +48,20 @@ /** * @return changeset associated with the current revision - * @throws HgException indicates failure dealing with Mercurial data */ - public HgChangeset changeset() throws HgException; + public HgChangeset changeset(); /** * Lightweight alternative to {@link #changeset()}, identifies changeset in which current file node has been modified - * @return changeset {@link Nodeid} + * @return changeset {@link Nodeid revision} */ public Nodeid changesetRevision(); /** * Node, these are not necessarily in direct relation to parents of changeset from {@link #changeset()} * @return changesets that correspond to parents of the current file node, either pair element may be null. - * @throws HgException indicates failure dealing with Mercurial data */ - public Pair parents() throws HgException; + public Pair parents(); /** * Lightweight alternative to {@link #parents()}, give {@link Nodeid nodeids} only @@ -71,9 +72,8 @@ /** * Changes that originate from the given change and bear it as their parent. * @return collection (possibly empty) of immediate children of the change - * @throws HgException indicates failure dealing with Mercurial data */ - public Collection children() throws HgException; + public Collection children(); /** * Lightweight alternative to {@link #children()}. diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgCloneCommand.java --- a/src/org/tmatesoft/hg/core/HgCloneCommand.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgCloneCommand.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -37,9 +37,13 @@ import org.tmatesoft.hg.internal.Internals; import org.tmatesoft.hg.repo.HgBundle; import org.tmatesoft.hg.repo.HgBundle.GroupElement; +import org.tmatesoft.hg.repo.HgInvalidControlFileException; +import org.tmatesoft.hg.repo.HgInvalidFileException; +import org.tmatesoft.hg.repo.HgInvalidStateException; import org.tmatesoft.hg.repo.HgLookup; import org.tmatesoft.hg.repo.HgRemoteRepository; import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgRuntimeException; import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.PathRewrite; @@ -49,7 +53,7 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class HgCloneCommand { +public class HgCloneCommand extends HgAbstractCommand { private File destination; private HgRemoteRepository srcRepo; @@ -72,7 +76,17 @@ return this; } - public HgRepository execute() throws HgBadArgumentException, HgRemoteConnectionException, HgInvalidFileException, CancelledException { + /** + * + * @return + * @throws HgBadArgumentException + * @throws HgRemoteConnectionException + * @throws HgRepositoryNotFoundException + * @throws HgException + * @throws CancelledException + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public HgRepository execute() throws HgException, CancelledException { if (destination == null) { throw new IllegalArgumentException("Destination not set", null); } @@ -133,7 +147,7 @@ public WriteDownMate(File destDir) { hgDir = new File(destDir, ".hg"); - implHelper = new Internals(new BasicSessionContext(null, null)); + implHelper = new Internals(new BasicSessionContext(null)); implHelper.setStorageConfig(1, STORE | FNCACHE | DOTENCODE); storagePathHelper = implHelper.buildDataFilesHelper(); } @@ -159,7 +173,7 @@ indexFile = new FileOutputStream(new File(hgDir, filename = "store/00changelog.i")); collectChangelogIndexes = true; } catch (IOException ex) { - throw new HgBadStateException(ex); + throw new HgInvalidControlFileException("Failed to write changelog", ex, new File(filename)); } } @@ -174,7 +188,7 @@ indexFile = null; filename = null; } catch (IOException ex) { - throw new HgBadStateException(ex); + throw new HgInvalidControlFileException("Failed to write changelog", ex, new File(filename)); } } @@ -185,7 +199,7 @@ revisionSequence.clear(); indexFile = new FileOutputStream(new File(hgDir, filename = "store/00manifest.i")); } catch (IOException ex) { - throw new HgBadStateException(ex); + throw new HgInvalidControlFileException("Failed to write manifest", ex, new File(filename)); } } @@ -199,7 +213,7 @@ indexFile = null; filename = null; } catch (IOException ex) { - throw new HgBadStateException(ex); + throw new HgInvalidControlFileException("Failed to write changelog", ex, new File(filename)); } } @@ -208,13 +222,14 @@ base = -1; offset = 0; revisionSequence.clear(); - fncacheFiles.add("data/" + name + ".i"); // FIXME this is pure guess, + fncacheFiles.add("data/" + name + ".i"); // TODO post-1.0 this is pure guess, // need to investigate more how filenames are kept in fncache File file = new File(hgDir, filename = storagePathHelper.rewrite(name).toString()); file.getParentFile().mkdirs(); indexFile = new FileOutputStream(file); } catch (IOException ex) { - throw new HgBadStateException(ex); + String m = String.format("Failed to write file %s", filename); + throw new HgInvalidControlFileException(m, ex, new File(filename)); } } @@ -228,7 +243,8 @@ indexFile = null; filename = null; } catch (IOException ex) { - throw new HgBadStateException(ex); + String m = String.format("Failed to write file %s", filename); + throw new HgInvalidControlFileException(m, ex, new File(filename)); } } @@ -242,7 +258,8 @@ } } } - throw new HgBadStateException(String.format("Can't find index of %s for file %s", p.shortNotation(), filename)); + String m = String.format("Can't find index of %s for file %s", p.shortNotation(), filename); + throw new HgInvalidControlFileException(m, null, null).setRevision(p); } public boolean element(GroupElement ge) { @@ -259,7 +276,8 @@ byte[] calculated = dh.sha1(p1, p2, content).asBinary(); final Nodeid node = ge.node(); if (!node.equalsTo(calculated)) { - throw new HgBadStateException(String.format("Checksum failed: expected %s, calculated %s. File %s", node, calculated, filename)); + // TODO post-1.0 custom exception ChecksumCalculationFailed? + throw new HgInvalidStateException(String.format("Checksum failed: expected %s, calculated %s. File %s", node, calculated, filename)); } final int link; if (collectChangelogIndexes) { @@ -268,7 +286,7 @@ } else { Integer csRev = changelogIndexes.get(ge.cset()); if (csRev == null) { - throw new HgBadStateException(String.format("Changelog doesn't contain revision %s of %s", ge.cset().shortNotation(), filename)); + throw new HgInvalidStateException(String.format("Changelog doesn't contain revision %s of %s", ge.cset().shortNotation(), filename)); } link = csRev.intValue(); } @@ -325,7 +343,8 @@ prevRevContent.done(); prevRevContent = new ByteArrayDataAccess(content); } catch (IOException ex) { - throw new HgBadStateException(ex); + String m = String.format("Failed to write revision %s of file %s", ge.node().shortNotation(), filename); + throw new HgInvalidControlFileException(m, ex, new File(filename)); } return true; } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgDataStreamException.java --- a/src/org/tmatesoft/hg/core/HgDataStreamException.java Thu Jun 21 21:36:06 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2011 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.core; - -import org.tmatesoft.hg.repo.HgDataFile; -import org.tmatesoft.hg.util.Path; - -/** - * Any erroneous state with @link {@link HgDataFile} input/output, read/write operations - * FIXME/REVISIT if HgInvalidControlFileExceptio and HgInvalidFileException is not sufficient? Is there real need for all 3? - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -@SuppressWarnings("serial") -public class HgDataStreamException extends HgException { - - public HgDataStreamException(Path file, String message, Throwable cause) { - super(message, cause); - setFileName(file); - } - - public HgDataStreamException(Path file, Throwable cause) { - super(cause); - setFileName(file); - } - - @Override - public HgDataStreamException setRevision(Nodeid r) { - return (HgDataStreamException) super.setRevision(r); - } - - @Override - public HgDataStreamException setRevisionIndex(int rev) { - return (HgDataStreamException) super.setRevisionIndex(rev); - } - @Override - public HgDataStreamException setFileName(Path name) { - return (HgDataStreamException) super.setFileName(name); - } -} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgException.java --- a/src/org/tmatesoft/hg/core/HgException.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgException.java Wed Jul 11 20:40:47 2012 +0200 @@ -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,33 +16,30 @@ */ package org.tmatesoft.hg.core; -import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; - +import org.tmatesoft.hg.internal.ExceptionInfo; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.util.Path; /** - * Root class for all hg4j exceptions. + * Root class for all Hg4J checked exceptions. * * @author Artem Tikhomirov * @author TMate Software Ltd. */ @SuppressWarnings("serial") -public class HgException extends Exception { +public abstract class HgException extends Exception { + + protected final ExceptionInfo extras = new ExceptionInfo(this); - protected int revNumber = BAD_REVISION; - protected Nodeid revision; - protected Path filename; - - public HgException(String reason) { + protected HgException(String reason) { super(reason); } - public HgException(String reason, Throwable cause) { + protected HgException(String reason, Throwable cause) { super(reason, cause); } - public HgException(Throwable cause) { + protected HgException(Throwable cause) { super(cause); } @@ -50,73 +47,41 @@ * @return not {@link HgRepository#BAD_REVISION} only when revision index was supplied at the construction time */ public int getRevisionIndex() { - return revNumber; + return extras.getRevisionIndex(); } - /** - * @deprecated use {@link #getRevisionIndex()} - */ - @Deprecated - public int getRevisionNumber() { - return getRevisionIndex(); + public HgException setRevisionIndex(int rev) { + return extras.setRevisionIndex(rev); } - - public HgException setRevisionIndex(int rev) { - revNumber = rev; - return this; - } - - /** - * @deprecated use {@link #setRevisionIndex(int)} - */ - @Deprecated - public final HgException setRevisionNumber(int rev) { - return setRevisionIndex(rev); + public boolean isRevisionIndexSet() { + return extras.isRevisionIndexSet(); } /** * @return non-null only when revision was supplied at construction time */ public Nodeid getRevision() { - return revision; + return extras.getRevision(); } public HgException setRevision(Nodeid r) { - revision = r; - return this; + return extras.setRevision(r); + } + + public boolean isRevisionSet() { + return extras.isRevisionSet(); } /** * @return non-null only if file name was set at construction time */ public Path getFileName() { - return filename; + return extras.getFileName(); } public HgException setFileName(Path name) { - filename = name; - return this; - } - - protected void appendDetails(StringBuilder sb) { - if (filename != null) { - sb.append("file:'"); - sb.append(filename); - sb.append('\''); - sb.append(';'); - sb.append(' '); - } - sb.append("rev:"); - if (revNumber != BAD_REVISION) { - sb.append(revNumber); - if (revision != null) { - sb.append(':'); - } - } - if (revision != null) { - sb.append(revision.shortNotation()); - } + return extras.setFileName(name); } @Override @@ -124,7 +89,7 @@ StringBuilder sb = new StringBuilder(super.toString()); sb.append(' '); sb.append('('); - appendDetails(sb); + extras.appendDetails(sb); sb.append(')'); return sb.toString(); } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgFileInformer.java --- a/src/org/tmatesoft/hg/core/HgFileInformer.java Thu Jun 21 21:36:06 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,215 +0,0 @@ -/* - * Copyright (c) 2011 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.core; - -import org.tmatesoft.hg.internal.ManifestRevision; -import org.tmatesoft.hg.repo.HgDataFile; -import org.tmatesoft.hg.repo.HgInternals; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.Status; - -/** - * Primary purpose is to provide information about file revisions at specific changeset. Multiple {@link #check(Path)} calls - * are possible once {@link #changeset(Nodeid)} (and optionally, {@link #followRenames(boolean)}) were set. - * - *

Sample: - *


- *   HgFileInformer i = new HgFileInformer(hgRepo).changeset(Nodeid.fromString("<40 digits>")).followRenames(true);
- *   if (i.check(file)) {
- *   	HgCatCommand catCmd = new HgCatCommand(hgRepo).revision(i.getFileRevision());
- *   	catCmd.execute(...);
- *   	...
- *   }
- * 
- * - * FIXME need better name. It's more about manifest of specific changeset, rather than informing (about) files - * TODO may add #manifest(Nodeid) to select manifest according to its revision (not only changeset revision as it's now) - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgFileInformer { - - private final HgRepository repo; - private boolean followRenames; - private Nodeid cset; - private ManifestRevision cachedManifest; - private HgFileRevision fileRevision; - private boolean renamed; - private Status checkResult; - - public HgFileInformer(HgRepository hgRepo) { - repo = hgRepo; - } - - /** - * Select specific changelog revision - * - * @param nid changeset identifier - * @return this for convenience - */ - public HgFileInformer changeset(Nodeid nid) { - if (nid == null || nid.isNull()) { - throw new IllegalArgumentException(); - } - cset = nid; - cachedManifest = null; - fileRevision = null; - return this; - } - - /** - * Whether to check file origins, default is false (look up only the name supplied) - * - * @param follow true to check copy/rename origin of the file if it is a copy. - * @return this for convenience - */ - public HgFileInformer followRenames(boolean follow) { - followRenames = follow; - fileRevision = null; - return this; - } - - /** - * Shortcut to perform {@link #check(Path)} and {@link #exists()}. Result of the check may be accessed via {@link #getCheckStatus()}. - * - * @param file name of the file in question - * @return true if file is known at the selected changeset. - * @throws IllegalArgumentException if {@link #changeset(Nodeid)} not specified or file argument is bad. - * @throws HgInvalidControlFileException if access to revlog index/data entry failed - */ - public boolean checkExists(Path file) throws HgInvalidControlFileException { - check(file); - if (!checkResult.isOk() && checkResult.getException() instanceof HgInvalidControlFileException) { - throw (HgInvalidControlFileException) checkResult.getException(); - } - return checkResult.isOk() && exists(); - } - - /** - * Find file (or its origin, if {@link #followRenames(boolean)} was set to true) among files known at specified {@link #changeset(Nodeid)}. - * - * @param file name of the file in question - * @return status object that describes outcome, {@link Status#isOk() Ok} status indicates successful completion of the operation, but doesn't imply - * file existence, use {@link #exists()} for that purpose. Message of the status may provide further hints on what exactly had happened. - * @throws IllegalArgumentException if {@link #changeset(Nodeid)} not specified or file argument is bad. - */ - public Status check(Path file) { - fileRevision = null; - checkResult = null; - renamed = false; - if (cset == null || file == null || file.isDirectory()) { - throw new IllegalArgumentException(); - } - HgDataFile dataFile = repo.getFileNode(file); - if (!dataFile.exists()) { - checkResult = new Status(Status.Kind.OK, String.format("File named %s is not known in the repository", file)); - return checkResult; - } - Nodeid toExtract = null; - try { - if (cachedManifest == null) { - int csetRev = repo.getChangelog().getRevisionIndex(cset); - cachedManifest = new ManifestRevision(null, null); // XXX how about context and cached manifest revisions - repo.getManifest().walk(csetRev, csetRev, cachedManifest); - // cachedManifest shall be meaningful - changelog.getRevisionIndex() above ensures we've got version that exists. - } - toExtract = cachedManifest.nodeid(file); - if (toExtract == null && followRenames) { - while (toExtract == null && dataFile.isCopy()) { - renamed = true; - file = dataFile.getCopySourceName(); - dataFile = repo.getFileNode(file); - toExtract = cachedManifest.nodeid(file); - } - } - } catch (HgInvalidControlFileException ex) { - checkResult = new Status(Status.Kind.ERROR, "", ex); - return checkResult; - } catch (HgDataStreamException ex) { - checkResult = new Status(Status.Kind.ERROR, "Follow copy/rename failed", ex); - HgInternals.getContext(repo).getLog().warn(getClass(), ex, checkResult.getMessage()); - return checkResult; - } - if (toExtract != null) { - fileRevision = new HgFileRevision(repo, toExtract, dataFile.getPath()); - checkResult = new Status(Status.Kind.OK, String.format("File %s, revision %s found at changeset %s", dataFile.getPath(), toExtract.shortNotation(), cset.shortNotation())); - return checkResult; - } - checkResult = new Status(Status.Kind.OK, String.format("File %s nor its origins were known at repository %s revision", file, cset.shortNotation())); - return checkResult; - } - - /** - * Re-get latest check status object - */ - public Status getCheckStatus() { - assertCheckRan(); - return checkResult; - } - - /** - * @return result of the last {@link #check(Path)} call. - */ - public boolean exists() { - assertCheckRan(); - return fileRevision != null; - } - - /** - * @return true if checked file was known by another name at the time of specified changeset. - */ - public boolean hasAnotherName() { - assertCheckRan(); - return renamed; - } - - /** - * @return holder for file revision information - */ - public HgFileRevision getFileRevision() { - assertCheckRan(); - return fileRevision; - } - - /** - * Name of the checked file as it was known at the time of the specified changeset. - * - * @return handy shortcut for getFileRevision().getPath() - */ - public Path filename() { - assertCheckRan(); - return fileRevision.getPath(); - } - - /** - * Revision of the checked file - * - * @return handy shortcut for getFileRevision().getRevision() - */ - public Nodeid revision() { - assertCheckRan(); - return fileRevision.getRevision(); - } - - private void assertCheckRan() { - if (checkResult == null) { - throw new HgBadStateException("Shall invoke #check(Path) first"); - } - } -} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgFileRevision.java --- a/src/org/tmatesoft/hg/core/HgFileRevision.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgFileRevision.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -17,7 +17,10 @@ package org.tmatesoft.hg.core; import org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgManifest; +import org.tmatesoft.hg.repo.HgManifest.Flags; import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgRuntimeException; import org.tmatesoft.hg.util.ByteChannel; import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.Pair; @@ -36,20 +39,32 @@ private Path origin; private Boolean isCopy = null; // null means not yet known private Pair parents; + private Flags flags; // null unless set/extracted - public HgFileRevision(HgRepository hgRepo, Nodeid rev, Path p) { + /** + * New description of a file revision from a specific repository. + * + *

Although this constructor is public, and clients can use it to construct own file revisions to pass e.g. to commands, its use is discouraged. + * + * @param hgRepo repository + * @param rev file revision + * @param manifestEntryFlags file flags at this revision (optional, may be null) + * @param p path of the file at the given revision + */ + public HgFileRevision(HgRepository hgRepo, Nodeid rev, HgManifest.Flags manifestEntryFlags, Path p) { if (hgRepo == null || rev == null || p == null) { // since it's package local, it is our code to blame for non validated arguments throw new IllegalArgumentException(); } repo = hgRepo; revision = rev; + flags = manifestEntryFlags; path = p; } // this cons shall be used when we know whether p was a copy. Perhaps, shall pass Map instead to stress orig argument is not optional - HgFileRevision(HgRepository hgRepo, Nodeid rev, Path p, Path orig) { - this(hgRepo, rev, p); + HgFileRevision(HgRepository hgRepo, Nodeid rev, HgManifest.Flags flags, Path p, Path orig) { + this(hgRepo, rev, flags, p); isCopy = Boolean.valueOf(orig == null); origin = orig; } @@ -61,6 +76,28 @@ public Nodeid getRevision() { return revision; } + + /** + * Extract flags of the file as recorded in the manifest for this file revision + * @return whether regular file, executable or a symbolic link + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public HgManifest.Flags getFileFlags() throws HgRuntimeException { + if (flags == null) { + /* + * Note, for uses other than HgManifestCommand or HgChangesetFileSneaker, when no flags come through the cons, + * it's possible to face next shortcoming: + * Imagine csetA and csetB, with corresponding manifestA and manifestB, the file didn't change (revision/nodeid is the same) + * but flag of the file has changed (e.g. became executable). Since HgFileRevision doesn't keep reference to + * an actual manifest revision, but only file's, and it's likely the flags returned from this method would + * yield result as from manifestA (i.e. no flag change in manifestB ever noticed). + */ + HgDataFile df = repo.getFileNode(path); + int revIdx = df.getRevisionIndex(revision); + flags = df.getFlags(revIdx); + } + return flags; + } public boolean wasCopied() throws HgException { if (isCopy == null) { @@ -84,8 +121,9 @@ * In most cases, only one parent revision would be present, only for merge revisions one can expect both. * * @return parent revisions of this file revision, with {@link Nodeid#NULL} for missing values. + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public Pair getParents() throws HgInvalidControlFileException { + public Pair getParents() throws HgRuntimeException { if (parents == null) { HgDataFile fn = repo.getFileNode(path); int revisionIndex = fn.getRevisionIndex(revision); @@ -99,13 +137,13 @@ return parents; } - public void putContentTo(ByteChannel sink) throws HgDataStreamException, HgInvalidControlFileException, CancelledException { + public void putContentTo(ByteChannel sink) throws HgException, CancelledException { HgDataFile fn = repo.getFileNode(path); int revisionIndex = fn.getRevisionIndex(revision); fn.contentWithFilters(revisionIndex, sink); } - private void checkCopy() throws HgInvalidControlFileException, HgDataStreamException { + private void checkCopy() throws HgException { HgDataFile fn = repo.getFileNode(path); if (fn.isCopy()) { if (fn.getRevision(0).equals(revision)) { diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgIncomingCommand.java --- a/src/org/tmatesoft/hg/core/HgIncomingCommand.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgIncomingCommand.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -30,8 +30,12 @@ import org.tmatesoft.hg.repo.HgBundle; import org.tmatesoft.hg.repo.HgChangelog; import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; +import org.tmatesoft.hg.repo.HgInvalidControlFileException; +import org.tmatesoft.hg.repo.HgInvalidStateException; import org.tmatesoft.hg.repo.HgRemoteRepository; import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgRuntimeException; +import org.tmatesoft.hg.repo.HgParentChildMap; import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.ProgressSupport; @@ -49,7 +53,7 @@ private boolean includeSubrepo; private RepositoryComparator comparator; private List missingBranches; - private HgChangelog.ParentWalker parentHelper; + private HgParentChildMap parentHelper; private Set branches; public HgIncomingCommand(HgRepository hgRepo) { @@ -66,7 +70,7 @@ /** * Select specific branch to push. * Multiple branch specification possible (changeset from any of these would be included in result). - * Note, {@link #executeLite(Object)} does not respect this setting. + * Note, {@link #executeLite()} does not respect this setting. * * @param branch - branch name, case-sensitive, non-null. * @return this for convenience @@ -100,34 +104,36 @@ * * @return list of nodes present at remote and missing locally * @throws HgRemoteConnectionException when failed to communicate with remote repository - * @throws HgInvalidControlFileException if access to revlog index/data entry failed + * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state * @throws CancelledException if execution of the command was cancelled */ - public List executeLite() throws HgRemoteConnectionException, HgInvalidControlFileException, CancelledException { - LinkedHashSet result = new LinkedHashSet(); - RepositoryComparator repoCompare = getComparator(); - for (BranchChain bc : getMissingBranches()) { - List missing = repoCompare.visitBranches(bc); - HashSet common = new HashSet(); // ordering is irrelevant - repoCompare.collectKnownRoots(bc, common); - // missing could only start with common elements. Once non-common, rest is just distinct branch revision trails. - for (Iterator it = missing.iterator(); it.hasNext() && common.contains(it.next()); it.remove()) ; - result.addAll(missing); + public List executeLite() throws HgException, CancelledException { + try { + LinkedHashSet result = new LinkedHashSet(); + RepositoryComparator repoCompare = getComparator(); + for (BranchChain bc : getMissingBranches()) { + List missing = repoCompare.visitBranches(bc); + HashSet common = new HashSet(); // ordering is irrelevant + repoCompare.collectKnownRoots(bc, common); + // missing could only start with common elements. Once non-common, rest is just distinct branch revision trails. + for (Iterator it = missing.iterator(); it.hasNext() && common.contains(it.next()); it.remove()) ; + result.addAll(missing); + } + ArrayList rv = new ArrayList(result); + return rv; + } catch (HgRuntimeException ex) { + throw new HgLibraryFailureException(ex); } - ArrayList rv = new ArrayList(result); - return rv; } /** * Full information about incoming changes * - * @throws HgRemoteConnectionException when failed to communicate with remote repository - * @throws HgInvalidControlFileException if access to revlog index/data entry failed - * @throws HgInvalidFileException to indicate failure working with locally downloaded changes in a bundle file * @throws HgCallbackTargetException to re-throw exception from the handler + * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state * @throws CancelledException if execution of the command was cancelled */ - public void executeFull(final HgChangesetHandler handler) throws HgRemoteConnectionException, HgInvalidControlFileException, HgInvalidFileException, HgCallbackTargetException, CancelledException { + public void executeFull(final HgChangesetHandler handler) throws HgCallbackTargetException, HgException, CancelledException { if (handler == null) { throw new IllegalArgumentException("Delegate can't be null"); } @@ -139,7 +145,7 @@ transformer.limitBranches(branches); changegroup.changes(localRepo, new HgChangelog.Inspector() { private int localIndex; - private final HgChangelog.ParentWalker parentHelper; + private final HgParentChildMap parentHelper; { parentHelper = getParentHelper(); @@ -150,7 +156,7 @@ public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { if (parentHelper.knownNode(nodeid)) { if (!common.contains(nodeid)) { - throw new HgBadStateException("Bundle shall not report known nodes other than roots we've supplied"); + throw new HgInvalidStateException("Bundle shall not report known nodes other than roots we've supplied"); } return; } @@ -158,6 +164,8 @@ } }); transformer.checkFailure(); + } catch (HgRuntimeException ex) { + throw new HgLibraryFailureException(ex); } finally { ps.done(); } @@ -174,9 +182,9 @@ return comparator; } - private HgChangelog.ParentWalker getParentHelper() throws HgInvalidControlFileException { + private HgParentChildMap getParentHelper() throws HgInvalidControlFileException { if (parentHelper == null) { - parentHelper = localRepo.getChangelog().new ParentWalker(); + parentHelper = new HgParentChildMap(localRepo.getChangelog()); parentHelper.init(); } return parentHelper; diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgInvalidControlFileException.java --- a/src/org/tmatesoft/hg/core/HgInvalidControlFileException.java Thu Jun 21 21:36:06 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2011 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.core; - -import java.io.File; - -import org.tmatesoft.hg.internal.Experimental; - -/** - * WORK IN PROGRESS - * - * Subclass of {@link HgInvalidFileException} to indicate failure to deal with one of Mercurial control files - * (most likely those under .hg/, but also those residing in the repository, with special meaning to the Mercurial, like .hgtags or .hgignore) - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -@SuppressWarnings("serial") -@Experimental(reason="WORK IN PROGRESS. Name is likely to change. Path argument to be added?") -public class HgInvalidControlFileException extends HgInvalidFileException { - - public HgInvalidControlFileException(String message, Throwable th, File file) { - super(message, th, file); - } - - @Override - public HgInvalidControlFileException setFile(File file) { - super.setFile(file); - return this; - } - - @Override - public HgInvalidControlFileException setRevision(Nodeid r) { - super.setRevision(r); - return this; - } -} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgInvalidFileException.java --- a/src/org/tmatesoft/hg/core/HgInvalidFileException.java Thu Jun 21 21:36:06 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2011 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.core; - -import java.io.File; -import java.io.IOException; - -/** - * Thrown when there are troubles working with local file. Most likely (but not necessarily) wraps IOException. Might be - * perceived as specialized IOException with optional File and other repository information. - * - * Hg4J tries to minimize chances for IOException to occur (i.e. {@link File#canRead()} is checked before attempt to - * read a file that might not exist, and doesn't use this exception to wrap each and any {@link IOException} source (e.g. - * #close() calls are unlikely to yield it), hence it is likely to address real cases when I/O error occurs. - * - * On the other hand, when a file is supposed to exist and be readable, this exception might get thrown as well to indicate - * that's not true. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -@SuppressWarnings("serial") -public class HgInvalidFileException extends HgException { - - private File localFile; - - public HgInvalidFileException(String message, Throwable th) { - super(message, th); - } - - public HgInvalidFileException(String message, Throwable th, File file) { - super(message, th); - localFile = file; - } - - public HgInvalidFileException setFile(File file) { - assert file != null; - localFile = file; - return this; - } - - /** - * @return file object that causes troubles, or null if specific file is unknown - */ - public File getFile() { - return localFile; - } - - @Override - protected void appendDetails(StringBuilder sb) { - super.appendDetails(sb); - if (localFile != null) { - sb.append(" file:"); - sb.append(localFile.getPath()); - sb.append(','); - if (localFile.exists()) { - sb.append("EXISTS"); - } else { - sb.append("DOESN'T EXIST"); - } - } - } -} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgInvalidRevisionException.java --- a/src/org/tmatesoft/hg/core/HgInvalidRevisionException.java Thu Jun 21 21:36:06 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2011 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.core; - -import org.tmatesoft.hg.internal.Experimental; -import org.tmatesoft.hg.repo.HgRepository; - -/** - * Use of revision or revision local index that is not valid for a given revlog. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -@SuppressWarnings("serial") -@Experimental(reason="1) Whether to use checked or runtime exception is not yet decided. 2) Perhaps, its use not bound to wrong arguments") -public class HgInvalidRevisionException extends IllegalArgumentException { - private Nodeid rev; - private Integer revIdx; - // next two make sense only when revIdx is present - private int rangeLeftBoundary = -1, rangeRightBoundary = -1; - - /** - * - * this exception is not expected to be initialized with another exception, although those who need to, - * may still use {@link #initCause(Throwable)} - * @param message optional description of the issue - * @param revision invalid revision, may be null if revisionIndex is used - * @param revisionIndex invalid revision index, may be null if not known and revision is supplied - */ - public HgInvalidRevisionException(String message, Nodeid revision, Integer revisionIndex) { - super(message); - assert revision != null || revisionIndex != null; - rev = revision; - revIdx = revisionIndex; - } - - public HgInvalidRevisionException(Nodeid revision) { - this(null, revision, null); - } - - public HgInvalidRevisionException(int revisionIndex) { - this(null, null, revisionIndex); - } - - public Nodeid getRevision() { - return rev; - } - - public Integer getRevisionIndex() { - return revIdx; - } - - public HgInvalidRevisionException setRevision(Nodeid revision) { - assert revision != null; - rev = revision; - return this; - } - - // int, not Integer is on purpose, not to clear exception completely - public HgInvalidRevisionException setRevisionIndex(int revisionIndex) { - revIdx = revisionIndex; - return this; - } - - public HgInvalidRevisionException setRevisionIndex(int revisionIndex, int rangeLeft, int rangeRight) { - revIdx = revisionIndex; - rangeLeftBoundary = rangeLeft; - rangeRightBoundary = rangeRight; - return this; - } - - @Override - public String getMessage() { - String msg = super.getMessage(); - if (msg != null) { - return msg; - } - StringBuilder sb = new StringBuilder(); - if (rev != null) { - sb.append("Revision:"); - sb.append(rev.shortNotation()); - sb.append(' '); - } - if (revIdx != null) { - String sr; - switch (revIdx) { - case HgRepository.BAD_REVISION : sr = "UNKNOWN"; break; - case HgRepository.TIP : sr = "TIP"; break; - case HgRepository.WORKING_COPY: sr = "WORKING-COPY"; break; - default : sr = revIdx.toString(); - } - if (rangeLeftBoundary != -1 || rangeRightBoundary != -1) { - sb.append(String.format("%s is not from [%d..%d]", sr, rangeLeftBoundary, rangeRightBoundary)); - } else { - sb.append(sr); - } - } - return sb.toString(); - } -} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgLibraryFailureException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/core/HgLibraryFailureException.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,40 @@ +/* + * Copyright (c) 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 + * 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.core; + +import org.tmatesoft.hg.repo.HgRuntimeException; + +/** + * Sole purpose of this exception is to wrap unexpected errors from the library implementation and + * propagate them to clients of hi-level API for graceful (and explicit) processing. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@SuppressWarnings("serial") +public class HgLibraryFailureException extends HgException { + + public HgLibraryFailureException(HgRuntimeException cause) { + super(cause); + assert cause != null; + } + + @Override + public HgRuntimeException getCause() { + return (HgRuntimeException) super.getCause(); + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgLogCommand.java --- a/src/org/tmatesoft/hg/core/HgLogCommand.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgLogCommand.java Wed Jul 11 20:40:47 2012 +0200 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 TMate Software Ltd +s * 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 @@ -17,6 +17,7 @@ package org.tmatesoft.hg.core; import static org.tmatesoft.hg.repo.HgRepository.TIP; +import static org.tmatesoft.hg.util.LogFacility.Severity.Error; import java.util.ArrayList; import java.util.Arrays; @@ -35,7 +36,12 @@ import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; import org.tmatesoft.hg.repo.HgDataFile; import org.tmatesoft.hg.repo.HgInternals; +import org.tmatesoft.hg.repo.HgInvalidControlFileException; +import org.tmatesoft.hg.repo.HgInvalidRevisionException; +import org.tmatesoft.hg.repo.HgInvalidStateException; +import org.tmatesoft.hg.repo.HgParentChildMap; import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgRuntimeException; import org.tmatesoft.hg.repo.HgStatusCollector; import org.tmatesoft.hg.util.CancelSupport; import org.tmatesoft.hg.util.CancelledException; @@ -67,7 +73,7 @@ private Path file; private boolean followHistory; // makes sense only when file != null private ChangesetTransformer csetTransform; - private HgChangelog.ParentWalker parentHelper; + private HgParentChildMap parentHelper; public HgLogCommand(HgRepository hgRepo) { repo = hgRepo; @@ -114,7 +120,7 @@ // multiple? public HgLogCommand date(Calendar date) { this.date = date; - // FIXME implement + // TODO post-1.0 implement // isSet(field) - false => don't use in detection of 'same date' throw HgRepository.notImplemented(); } @@ -132,8 +138,9 @@ /** * Limit to specified subset of Changelog, [min(rev1,rev2), max(rev1,rev2)], inclusive. * Revision may be specified with {@link HgRepository#TIP} - * @param rev1 - revision local index - * @param rev2 - revision local index + * + * @param rev1 - local index of start changeset revision + * @param rev2 - index of end changeset revision * @return this instance for convenience */ public HgLogCommand range(int rev1, int rev2) { @@ -155,13 +162,16 @@ * * @param nid changeset revision * @return this for convenience - * @throws HgInvalidRevisionException if supplied nodeid doesn't identify any revision from this revlog - * @throws HgInvalidControlFileException if access to revlog index/data entry failed + * @throws HgBadArgumentException if failed to find supplied changeset revision */ - public HgLogCommand changeset(Nodeid nid) throws HgInvalidControlFileException, HgInvalidRevisionException { + public HgLogCommand changeset(Nodeid nid) throws HgBadArgumentException { // XXX perhaps, shall support multiple (...) arguments and extend #execute to handle not only range, but also set of revisions. - final int csetRevIndex = repo.getChangelog().getRevisionIndex(nid); - return range(csetRevIndex, csetRevIndex); + try { + final int csetRevIndex = repo.getChangelog().getRevisionIndex(nid); + return range(csetRevIndex, csetRevIndex); + } catch (HgInvalidRevisionException ex) { + throw new HgBadArgumentException("Can't find revision", ex).setRevision(nid); + } } /** @@ -184,27 +194,35 @@ } /** - * Similar to {@link #execute(org.tmatesoft.hg.repo.RawChangeset.Inspector)}, collects and return result as a list. + * Similar to {@link #execute(HgChangesetHandler)}, collects and return result as a list. + * + * @see #execute(HgChangesetHandler) + * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state */ - public List execute() throws HgDataStreamException { + public List execute() throws HgException { CollectHandler collector = new CollectHandler(); try { execute(collector); - } catch (HgException ex) { + } catch (HgCallbackTargetException ex) { + // see below for CanceledException + HgInvalidStateException t = new HgInvalidStateException("Internal error"); + t.initCause(ex); + throw t; + } catch (CancelledException ex) { // can't happen as long as our CollectHandler doesn't throw any exception - throw new HgBadStateException(ex.getCause()); - } catch (CancelledException ex) { - // can't happen, see above - throw new HgBadStateException(ex); + HgInvalidStateException t = new HgInvalidStateException("Internal error"); + t.initCause(ex); + throw t; } return collector.getChanges(); } /** + * Iterate over range of changesets configured in the command. * * @param handler callback to process changesets. - * @throws HgCallbackTargetException to re-throw exception from the handler - * @throws HgException FIXME EXCEPTIONS + * @throws HgCallbackTargetException propagated exception from the handler + * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state * @throws CancelledException if execution of the command was cancelled * @throws IllegalArgumentException when inspector argument is null * @throws ConcurrentModificationException if this log command instance is already running @@ -219,7 +237,7 @@ final ProgressSupport progressHelper = getProgressSupport(handler); try { count = 0; - HgChangelog.ParentWalker pw = getParentHelper(file == null); // leave it uninitialized unless we iterate whole repo + HgParentChildMap pw = getParentHelper(file == null); // leave it uninitialized unless we iterate whole repo // ChangesetTransfrom creates a blank PathPool, and #file(String, boolean) above // may utilize it as well. CommandContext? How about StatusCollector there as well? csetTransform = new ChangesetTransformer(repo, handler, pw, progressHelper, getCancelSupport(handler, true)); @@ -230,19 +248,18 @@ } else { progressHelper.start(-1/*XXX enum const, or a dedicated method startUnspecified(). How about startAtLeast(int)?*/); HgDataFile fileNode = repo.getFileNode(file); + if (!fileNode.exists()) { + throw new HgPathNotFoundException(String.format("File %s not found in the repository", file), file); + } fileNode.history(startRev, endRev, this); csetTransform.checkFailure(); if (fileNode.isCopy()) { // even if we do not follow history, report file rename do { - if (handler instanceof FileHistoryHandler) { - HgFileRevision src = new HgFileRevision(repo, fileNode.getCopySourceRevision(), fileNode.getCopySourceName()); - HgFileRevision dst = new HgFileRevision(repo, fileNode.getRevision(0), fileNode.getPath(), src.getPath()); - try { - ((FileHistoryHandler) handler).copy(src, dst); - } catch (HgCallbackTargetException.Wrap ex) { - throw new HgCallbackTargetException(ex).setRevision(fileNode.getCopySourceRevision()).setFileName(fileNode.getCopySourceName()); - } + if (handler instanceof HgChangesetHandler.WithCopyHistory) { + HgFileRevision src = new HgFileRevision(repo, fileNode.getCopySourceRevision(), null, fileNode.getCopySourceName()); + HgFileRevision dst = new HgFileRevision(repo, fileNode.getRevision(0), null, fileNode.getPath(), src.getPath()); + ((HgChangesetHandler.WithCopyHistory) handler).copy(src, dst); } if (limit > 0 && count >= limit) { // if limit reach, follow is useless. @@ -256,6 +273,8 @@ } while (followHistory && fileNode.isCopy()); } } + } catch (HgRuntimeException ex) { + throw new HgLibraryFailureException(ex); } finally { csetTransform = null; progressHelper.done(); @@ -263,11 +282,14 @@ } /** - * TODO documentation - * @param handler - * @throws HgCallbackTargetException to re-throw exception from the handler - * @throws HgException FIXME EXCEPTIONS + * Tree-wise iteration of a file history, with handy access to parent-child relations between changesets. + * + * @param handler callback to process changesets. + * @throws HgCallbackTargetException propagated exception from the handler + * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state * @throws CancelledException if execution of the command was cancelled + * @throws IllegalArgumentException if command is not satisfied with its arguments + * @throws ConcurrentModificationException if this log command instance is already running */ public void execute(HgChangesetTreeHandler handler) throws HgCallbackTargetException, HgException, CancelledException { if (handler == null) { @@ -304,7 +326,7 @@ HistoryNode[] go(HgDataFile fileNode) throws HgInvalidControlFileException { completeHistory = new HistoryNode[fileNode.getRevisionCount()]; commitRevisions = new int[completeHistory.length]; - fileNode.walk(0, TIP, this); + fileNode.indexWalk(0, TIP, this); return completeHistory; } }; @@ -328,17 +350,13 @@ } else { ph2 = new ProgressSupport.Sub(progressHelper, 3); } - try { - ph2.start(completeHistory.length); - // XXX shall sort completeHistory according to changeset numbers? - for (int i = 0; i < completeHistory.length; i++ ) { - final HistoryNode n = completeHistory[i]; - handler.next(ei.init(n)); - ph2.worked(1); - cancelHelper.checkCancelled(); - } - } catch (HgCallbackTargetException.Wrap ex) { - throw new HgCallbackTargetException(ex); + ph2.start(completeHistory.length); + // XXX shall sort completeHistory according to changeset numbers? + for (int i = 0; i < completeHistory.length; i++ ) { + final HistoryNode n = completeHistory[i]; + handler.treeElement(ei.init(n)); + ph2.worked(1); + cancelHelper.checkCancelled(); } progressHelper.done(); } @@ -366,41 +384,21 @@ } } if (date != null) { - // FIXME implement date support for log + // TODO post-1.0 implement date support for log } count++; csetTransform.next(revisionNumber, nodeid, cset); } - private HgChangelog.ParentWalker getParentHelper(boolean create) throws HgInvalidControlFileException { + private HgParentChildMap getParentHelper(boolean create) throws HgInvalidControlFileException { if (parentHelper == null && create) { - parentHelper = repo.getChangelog().new ParentWalker(); + parentHelper = new HgParentChildMap(repo.getChangelog()); parentHelper.init(); } return parentHelper; } - /** - * When {@link HgLogCommand} is executed against file, handler passed to {@link HgLogCommand#execute(HgChangesetHandler)} may optionally - * implement this interface to get information about file renames. Method {@link #copy(HgFileRevision, HgFileRevision)} would - * get invoked prior any changeset of the original file (if file history being followed) is reported via {@link #next(HgChangeset)}. - * - * For {@link HgLogCommand#file(Path, boolean)} with renamed file path and follow argument set to false, - * {@link #copy(HgFileRevision, HgFileRevision)} would be invoked for the first copy/rename in the history of the file, but not - * followed by any changesets. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ - public interface FileHistoryHandler extends HgChangesetHandler { // FIXME move to stanalone class file, perhaps? - // XXX perhaps, should distinguish copy from rename? And what about merged revisions and following them? - /** - * @throws HgCallbackTargetException.Wrap wrapper object for any exception user code may produce. Wrapped exception would get re-thrown with {@link HgCallbackTargetException} - */ - void copy(HgFileRevision from, HgFileRevision to) throws HgCallbackTargetException.Wrap; - } - public static class CollectHandler implements HgChangesetHandler { private final List result = new LinkedList(); @@ -408,7 +406,7 @@ return Collections.unmodifiableList(result); } - public void next(HgChangeset changeset) { + public void cset(HgChangeset changeset) { result.add(changeset.clone()); } } @@ -468,11 +466,11 @@ return historyNode.fileRevision; } - public HgChangeset changeset() throws HgException { + public HgChangeset changeset() { return get(historyNode.changeset)[0]; } - public Pair parents() throws HgException { + public Pair parents() { if (parents != null) { return parents; } @@ -492,7 +490,7 @@ return parents = new Pair(r[0], r[1]); } - public Collection children() throws HgException { + public Collection children() { if (children != null) { return children; } @@ -510,22 +508,22 @@ } void populate(HgChangeset cs) { - cachedChangesets.put(cs.getRevision(), cs); + cachedChangesets.put(cs.getRevisionIndex(), cs); } - private HgChangeset[] get(int... changelogRevisionNumber) throws HgInvalidControlFileException { - HgChangeset[] rv = new HgChangeset[changelogRevisionNumber.length]; - IntVector misses = new IntVector(changelogRevisionNumber.length, -1); - for (int i = 0; i < changelogRevisionNumber.length; i++) { - if (changelogRevisionNumber[i] == -1) { + private HgChangeset[] get(int... changelogRevisionIndex) { + HgChangeset[] rv = new HgChangeset[changelogRevisionIndex.length]; + IntVector misses = new IntVector(changelogRevisionIndex.length, -1); + for (int i = 0; i < changelogRevisionIndex.length; i++) { + if (changelogRevisionIndex[i] == -1) { rv[i] = null; continue; } - HgChangeset cached = cachedChangesets.get(changelogRevisionNumber[i]); + HgChangeset cached = cachedChangesets.get(changelogRevisionIndex[i]); if (cached != null) { rv[i] = cached; } else { - misses.add(changelogRevisionNumber[i]); + misses.add(changelogRevisionIndex[i]); } } if (misses.size() > 0) { @@ -534,27 +532,31 @@ repo.getChangelog().range(this, changesets2read); for (int changeset2read : changesets2read) { HgChangeset cs = cachedChangesets.get(changeset2read); - if (cs == null) { - throw new HgBadStateException(); + if (cs == null) { + HgInvalidStateException t = new HgInvalidStateException(String.format("Can't get changeset for revision %d", changeset2read)); + throw t.setRevisionIndex(changeset2read); + } + // HgChangelog.range may reorder changesets according to their order in the changelog + // thus need to find original index + boolean sanity = false; + for (int i = 0; i < changelogRevisionIndex.length; i++) { + if (changelogRevisionIndex[i] == cs.getRevisionIndex()) { + rv[i] = cs; + sanity = true; + break; } - // HgChangelog.range may reorder changesets according to their order in the changelog - // thus need to find original index - boolean sanity = false; - for (int i = 0; i < changelogRevisionNumber.length; i++) { - if (changelogRevisionNumber[i] == cs.getRevision()) { - rv[i] = cs; - sanity = true; - break; - } - } - assert sanity; + } + if (!sanity) { + HgInternals.getContext(repo).getLog().dump(getClass(), Error, "Index of revision %d:%s doesn't match any of requested", cs.getRevisionIndex(), cs.getNodeid().shortNotation()); + } + assert sanity; } } return rv; } // init only when needed - void initTransform() throws HgInvalidControlFileException { + void initTransform() throws HgRuntimeException { if (transform == null) { transform = new ChangesetTransformer.Transformation(new HgStatusCollector(repo)/*XXX try to reuse from context?*/, getParentHelper(false)); } @@ -609,18 +611,12 @@ // reading nodeid involves reading index only, guess, can afford not to optimize multiple reads private Nodeid getRevision(int changelogRevisionNumber) { - // XXX pipe through pool + // TODO post-1.0 pipe through pool HgChangeset cs = cachedChangesets.get(changelogRevisionNumber); if (cs != null) { return cs.getNodeid(); } else { - try { - return repo.getChangelog().getRevision(changelogRevisionNumber); - } catch (HgException ex) { - HgInternals.getContext(repo).getLog().error(getClass(), ex, null); - // FIXME propagate, perhaps? - return Nodeid.NULL; // FIXME this is quick-n-dirty hack to move forward with introducing exceptions - } + return repo.getChangelog().getRevision(changelogRevisionNumber); } } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgManifestCommand.java --- a/src/org/tmatesoft/hg/core/HgManifestCommand.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgManifestCommand.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -25,11 +25,15 @@ import java.util.LinkedList; import java.util.List; +import org.tmatesoft.hg.internal.PathPool; +import org.tmatesoft.hg.repo.HgInvalidRevisionException; import org.tmatesoft.hg.repo.HgManifest; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.repo.HgManifest.Flags; +import org.tmatesoft.hg.repo.HgRuntimeException; +import org.tmatesoft.hg.util.CancelSupport; +import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.PathPool; import org.tmatesoft.hg.util.PathRewrite; @@ -44,7 +48,7 @@ private final HgRepository repo; private Path.Matcher matcher; private int startRev = 0, endRev = TIP; - private Handler visitor; + private HgManifestHandler visitor; private boolean needDirs = false; private final Mediator mediator = new Mediator(); @@ -73,11 +77,32 @@ return this; } - public HgManifestCommand revision(int rev) { - startRev = endRev = rev; - return this; + /** + * Select changeset for the command using revision index + * @param csetRevisionIndex index of changeset revision + * @return this for convenience. + */ + public HgManifestCommand changeset(int csetRevisionIndex) { + return range(csetRevisionIndex, csetRevisionIndex); } + /** + * Select changeset for the command + * + * @param nid changeset revision + * @return this for convenience + * @throws HgBadArgumentException if failed to find supplied changeset revision + */ + public HgManifestCommand changeset(Nodeid nid) throws HgBadArgumentException { + // XXX also see HgLogCommand#changeset(Nodeid) + try { + final int csetRevIndex = repo.getChangelog().getRevisionIndex(nid); + return range(csetRevIndex, csetRevIndex); + } catch (HgInvalidRevisionException ex) { + throw new HgBadArgumentException("Can't find revision", ex).setRevision(nid); + } + } + public HgManifestCommand dirs(boolean include) { // XXX whether directories with directories only are include or not // now lists only directories with files @@ -96,12 +121,16 @@ } /** - * Runs the command. + * With all parameters set, execute the command. + * * @param handler - callback to get the outcome + * @throws HgCallbackTargetException propagated exception from the handler + * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state + * @throws CancelledException if execution of the command was cancelled * @throws IllegalArgumentException if handler is null * @throws ConcurrentModificationException if this command is already in use (running) */ - public void execute(Handler handler) throws HgException { + public void execute(HgManifestHandler handler) throws HgCallbackTargetException, HgException, CancelledException { if (handler == null) { throw new IllegalArgumentException(); } @@ -110,90 +139,130 @@ } try { visitor = handler; - mediator.start(); + mediator.start(getCancelSupport(handler, true)); repo.getManifest().walk(startRev, endRev, mediator); + mediator.checkFailure(); + } catch (HgRuntimeException ex) { + throw new HgLibraryFailureException(ex); } finally { mediator.done(); visitor = null; } } - /** - * Callback to walk file/directory tree of a revision - */ - public interface Handler { - void begin(Nodeid manifestRevision); - void dir(Path p); // optionally invoked (if walker was configured to spit out directories) prior to any files from this dir and subdirs - void file(HgFileRevision fileRevision); // XXX allow to check p is invalid (df.exists()) - void end(Nodeid manifestRevision); - } - // I'd rather let HgManifestCommand implement HgManifest.Inspector directly, but this pollutes API alot - private class Mediator implements HgManifest.Inspector2 { + private class Mediator implements HgManifest.Inspector { // file names are likely to repeat in each revision, hence caching of Paths. // However, once HgManifest.Inspector switches to Path objects, perhaps global Path pool // might be more effective? private PathPool pathPool; private List manifestContent; private Nodeid manifestNodeid; + private Exception failure; + private CancelSupport cancelHelper; - public void start() { + public void start(CancelSupport cs) { + assert cs != null; // Manifest keeps normalized paths pathPool = new PathPool(new PathRewrite.Empty()); + cancelHelper = cs; } public void done() { manifestContent = null; pathPool = null; } + + private void recordFailure(HgCallbackTargetException ex) { + failure = ex; + } + private void recordCancel(CancelledException ex) { + failure = ex; + } + + public void checkFailure() throws HgCallbackTargetException, CancelledException { + // TODO post-1.0 perhaps, can combine this code (record/checkFailure) for reuse in more classes (e.g. in Revlog) + if (failure instanceof HgCallbackTargetException) { + HgCallbackTargetException ex = (HgCallbackTargetException) failure; + failure = null; + throw ex; + } + if (failure instanceof CancelledException) { + CancelledException ex = (CancelledException) failure; + failure = null; + throw ex; + } + } public boolean begin(int manifestRevision, Nodeid nid, int changelogRevision) { if (needDirs && manifestContent == null) { manifestContent = new LinkedList(); } - visitor.begin(manifestNodeid = nid); - return true; + try { + visitor.begin(manifestNodeid = nid); + cancelHelper.checkCancelled(); + return true; + } catch (HgCallbackTargetException ex) { + recordFailure(ex); + return false; + } catch (CancelledException ex) { + recordCancel(ex); + return false; + } } public boolean end(int revision) { - if (needDirs) { - LinkedHashMap> breakDown = new LinkedHashMap>(); - for (HgFileRevision fr : manifestContent) { - Path filePath = fr.getPath(); - Path dirPath = pathPool.parent(filePath); - LinkedList revs = breakDown.get(dirPath); - if (revs == null) { - revs = new LinkedList(); - breakDown.put(dirPath, revs); + try { + if (needDirs) { + LinkedHashMap> breakDown = new LinkedHashMap>(); + for (HgFileRevision fr : manifestContent) { + Path filePath = fr.getPath(); + Path dirPath = pathPool.parent(filePath); + LinkedList revs = breakDown.get(dirPath); + if (revs == null) { + revs = new LinkedList(); + breakDown.put(dirPath, revs); + } + revs.addLast(fr); } - revs.addLast(fr); + for (Path dir : breakDown.keySet()) { + visitor.dir(dir); + cancelHelper.checkCancelled(); + for (HgFileRevision fr : breakDown.get(dir)) { + visitor.file(fr); + } + } + manifestContent.clear(); } - for (Path dir : breakDown.keySet()) { - visitor.dir(dir); - for (HgFileRevision fr : breakDown.get(dir)) { - visitor.file(fr); - } - } - manifestContent.clear(); + visitor.end(manifestNodeid); + cancelHelper.checkCancelled(); + return true; + } catch (HgCallbackTargetException ex) { + recordFailure(ex); + return false; + } catch (CancelledException ex) { + recordCancel(ex); + return false; + } finally { + manifestNodeid = null; } - visitor.end(manifestNodeid); - manifestNodeid = null; - return true; - } - public boolean next(Nodeid nid, String fname, String flags) { - throw new HgBadStateException(HgManifest.Inspector2.class.getName()); } public boolean next(Nodeid nid, Path fname, Flags flags) { if (matcher != null && !matcher.accept(fname)) { return true; } - HgFileRevision fr = new HgFileRevision(repo, nid, fname); - if (needDirs) { - manifestContent.add(fr); - } else { - visitor.file(fr); + try { + HgFileRevision fr = new HgFileRevision(repo, nid, flags, fname); + if (needDirs) { + manifestContent.add(fr); + } else { + visitor.file(fr); + } + return true; + } catch (HgCallbackTargetException ex) { + recordFailure(ex); + return false; } - return true; } } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgManifestHandler.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/core/HgManifestHandler.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,64 @@ +/* + * 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 + * 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.core; + +import org.tmatesoft.hg.internal.Callback; +import org.tmatesoft.hg.util.Path; + +/** + * Callback to walk file/directory tree of a revision + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@Callback +public interface HgManifestHandler { + /** + * Indicates start of manifest revision. Subsequent {@link #file(HgFileRevision)} and {@link #dir(Path)} come + * from the specified manifest revision until {@link #end(Nodeid)} with the matching revision is invoked. + * + * @param manifestRevision unique identifier of the manifest revision + * @throws HgCallbackTargetException wrapper for any exception user code may produce + */ + void begin(Nodeid manifestRevision) throws HgCallbackTargetException; + + /** + * If walker is configured to spit out directories, indicates files from specified directories are about to be reported. + * Comes prior to any files from this directory and subdirectories + * + * @param path directory known in the manifest + * @throws HgCallbackTargetException wrapper for any exception user code may produce + */ + void dir(Path path) throws HgCallbackTargetException; + + /** + * Reports a file revision entry in the manifest + * + * @param fileRevision description of the file revision + * @throws HgCallbackTargetException wrapper for any exception user code may produce + */ + void file(HgFileRevision fileRevision) throws HgCallbackTargetException; + + /** + * Indicates all files from the manifest revision have been reported. + * Closes {@link #begin(Nodeid)} with the same revision that came before. + * + * @param manifestRevision unique identifier of the manifest revision + * @throws HgCallbackTargetException wrapper for any exception user code may produce + */ + void end(Nodeid manifestRevision) throws HgCallbackTargetException; +} \ No newline at end of file diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgOutgoingCommand.java --- a/src/org/tmatesoft/hg/core/HgOutgoingCommand.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgOutgoingCommand.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -22,8 +22,11 @@ import org.tmatesoft.hg.internal.RepositoryComparator; import org.tmatesoft.hg.repo.HgChangelog; +import org.tmatesoft.hg.repo.HgInvalidControlFileException; import org.tmatesoft.hg.repo.HgRemoteRepository; import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgRuntimeException; +import org.tmatesoft.hg.repo.HgParentChildMap; import org.tmatesoft.hg.util.CancelSupport; import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.ProgressSupport; @@ -41,7 +44,7 @@ @SuppressWarnings("unused") private boolean includeSubrepo; private RepositoryComparator comparator; - private HgChangelog.ParentWalker parentHelper; + private HgParentChildMap parentHelper; private Set branches; public HgOutgoingCommand(HgRepository hgRepo) { @@ -61,7 +64,7 @@ /** * Select specific branch to pull. * Multiple branch specification possible (changeset from any of these would be included in result). - * Note, {@link #executeLite(Object)} does not respect this setting. + * Note, {@link #executeLite()} does not respect this setting. * * @param branch - branch name, case-sensitive, non-null. * @return this for convenience @@ -94,14 +97,16 @@ * * @return list on local nodes known to be missing at remote server * @throws HgRemoteConnectionException when failed to communicate with remote repository - * @throws HgInvalidControlFileException if access to revlog index/data entry failed + * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state * @throws CancelledException if execution of the command was cancelled */ - public List executeLite() throws HgRemoteConnectionException, HgInvalidControlFileException, CancelledException { + public List executeLite() throws HgRemoteConnectionException, HgException, CancelledException { final ProgressSupport ps = getProgressSupport(null); try { ps.start(10); return getComparator(new ProgressSupport.Sub(ps, 5), getCancelSupport(null, true)).getLocalOnlyRevisions(); + } catch (HgRuntimeException ex) { + throw new HgLibraryFailureException(ex); } finally { ps.done(); } @@ -111,12 +116,12 @@ * Complete information about outgoing changes * * @param handler delegate to process changes + * @throws HgCallbackTargetException propagated exception from the handler * @throws HgRemoteConnectionException when failed to communicate with remote repository - * @throws HgInvalidControlFileException if access to revlog index/data entry failed - * @throws HgCallbackTargetException to re-throw exception from the handler + * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state * @throws CancelledException if execution of the command was cancelled */ - public void executeFull(final HgChangesetHandler handler) throws HgRemoteConnectionException, HgInvalidControlFileException, HgCallbackTargetException, CancelledException { + public void executeFull(final HgChangesetHandler handler) throws HgCallbackTargetException, HgException, CancelledException { if (handler == null) { throw new IllegalArgumentException("Delegate can't be null"); } @@ -128,6 +133,8 @@ inspector.limitBranches(branches); getComparator(new ProgressSupport.Sub(ps, 1), cs).visitLocalOnlyRevisions(inspector); inspector.checkFailure(); + } catch (HgRuntimeException ex) { + throw new HgLibraryFailureException(ex); } finally { ps.done(); } @@ -144,9 +151,9 @@ return comparator; } - private HgChangelog.ParentWalker getParentHelper() throws HgInvalidControlFileException { + private HgParentChildMap getParentHelper() throws HgInvalidControlFileException { if (parentHelper == null) { - parentHelper = localRepo.getChangelog().new ParentWalker(); + parentHelper = new HgParentChildMap(localRepo.getChangelog()); parentHelper.init(); } return parentHelper; diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgPathNotFoundException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/core/HgPathNotFoundException.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,36 @@ +/* + * Copyright (c) 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 + * 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.core; + +import org.tmatesoft.hg.util.Path; + +/** + * Indicates supplied path/location was is missing in the repository or specific revision. + *

Use {@link #getFileName()} to access name of the missing file + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@SuppressWarnings("serial") +public class HgPathNotFoundException extends HgException { + + public HgPathNotFoundException(String message, Path missingPath) { + super(message, null); + assert missingPath != null; + setFileName(missingPath); + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgRepoFacade.java --- a/src/org/tmatesoft/hg/core/HgRepoFacade.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgRepoFacade.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -41,7 +41,7 @@ private final SessionContext context; public HgRepoFacade() { - this(new BasicSessionContext(null, null)); + this(new BasicSessionContext(null)); } public HgRepoFacade(SessionContext ctx) { @@ -66,10 +66,11 @@ /** * Tries to find repository starting from the current working directory. + * * @return true if found valid repository - * @throws HgInvalidFileException in case of errors during repository initialization + * @throws HgRepositoryNotFoundException if no repository found in working directory */ - public boolean init() throws HgInvalidFileException { + public boolean init() throws HgRepositoryNotFoundException { repo = new HgLookup(context).detectFromWorkingDir(); return repo != null && !repo.isInvalid(); } @@ -79,10 +80,10 @@ * * @param repoLocation path to any folder within structure of a Mercurial repository. * @return true if found valid repository - * @throws HgInvalidFileException if there are errors accessing specified location + * @throws HgRepositoryNotFoundException if there's no repository at specified location * @throws IllegalArgumentException if argument is null */ - public boolean initFrom(File repoLocation) throws HgInvalidFileException { + public boolean initFrom(File repoLocation) throws HgRepositoryNotFoundException { if (repoLocation == null) { throw new IllegalArgumentException(); } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgRepositoryNotFoundException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/core/HgRepositoryNotFoundException.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,43 @@ +/* + * Copyright (c) 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 + * 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.core; + +/** + * Indicates failure to find repository at specified location + * XXX may provide information about alternatives tried + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@SuppressWarnings("serial") +public class HgRepositoryNotFoundException extends HgException { + + private String location; + + public HgRepositoryNotFoundException(String message) { + super(message); + } + + public HgRepositoryNotFoundException setLocation(String location) { + this.location = location; + return this; + } + + public String getLocation() { + return this.location; + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgStatus.java --- a/src/org/tmatesoft/hg/core/HgStatus.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgStatus.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -71,7 +71,7 @@ /** * @return null if author for the change can't be deduced (e.g. for clean files it's senseless) */ - public String getModificationAuthor() throws HgInvalidControlFileException { + public String getModificationAuthor() { RawChangeset cset = logHelper.findLatestChangeWith(path); if (cset == null) { if (kind == Kind.Modified || kind == Kind.Added || kind == Kind.Removed /*&& RightBoundary is TIP*/) { @@ -84,15 +84,16 @@ return null; } - public Date getModificationDate() throws HgInvalidControlFileException { + public Date getModificationDate() { RawChangeset cset = logHelper.findLatestChangeWith(path); if (cset == null) { File localFile = new File(logHelper.getRepo().getWorkingDir(), path.toString()); if (localFile.canRead()) { return new Date(localFile.lastModified()); } - // FIXME check dirstate and/or local file for tstamp - return new Date(); // what's correct + // TODO post-1.0 find out what to do in this case, perhaps, throw an exception? + // perhaps check dirstate and/or local file for tstamp + return new Date(); // what's correct? } else { return cset.date(); } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgStatusCommand.java --- a/src/org/tmatesoft/hg/core/HgStatusCommand.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgStatusCommand.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -22,15 +22,17 @@ import java.io.IOException; import java.util.ConcurrentModificationException; -import java.util.concurrent.CancellationException; import org.tmatesoft.hg.internal.ChangelogHelper; import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgRuntimeException; import org.tmatesoft.hg.repo.HgStatusCollector; import org.tmatesoft.hg.repo.HgStatusInspector; import org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector; +import org.tmatesoft.hg.util.CancelSupport; +import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.Status; +import org.tmatesoft.hg.util.Outcome; /** * Command to obtain file status information, 'hg status' counterpart. @@ -134,18 +136,18 @@ /** * Shorthand for {@link #base(int) cmd.base(BAD_REVISION)}{@link #change(int) .revision(revision)} * - * @param revision compare given revision against its parent + * @param changesetRevisionIndex compare given revision against its parent * @return this for convenience */ - public HgStatusCommand change(int revision) { + public HgStatusCommand change(int changesetRevisionIndex) { base(BAD_REVISION); - return revision(revision); + return revision(changesetRevisionIndex); } /** * Limit status operation to certain sub-tree. * - * @param pathMatcher - matcher to use, pass null/ to reset + * @param scopeMatcher - matcher to use, pass null/ to reset * @return this for convenience */ public HgStatusCommand match(Path.Matcher scopeMatcher) { @@ -160,12 +162,15 @@ /** * Perform status operation according to parameters set. * - * @param handler callback to get status information + * @param statusHandler callback to get status information + * @throws HgCallbackTargetException propagated exception from the handler + * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state * @throws IOException if there are (further unspecified) errors while walking working copy + * @throws CancelledException if execution of the command was cancelled * @throws IllegalArgumentException if handler is null * @throws ConcurrentModificationException if this command already runs (i.e. being used from another thread) */ - public void execute(HgStatusHandler statusHandler) throws CancellationException, HgException, IOException { + public void execute(HgStatusHandler statusHandler) throws HgCallbackTargetException, HgException, IOException, CancelledException { if (statusHandler == null) { throw new IllegalArgumentException(); } @@ -177,7 +182,7 @@ try { // XXX if I need a rough estimation (for ProgressMonitor) of number of work units, // I may use number of files in either rev1 or rev2 manifest edition - mediator.start(statusHandler, new ChangelogHelper(repo, startRevision)); + mediator.start(statusHandler, getCancelSupport(statusHandler, true), new ChangelogHelper(repo, startRevision)); if (endRevision == WORKING_COPY) { HgWorkingCopyStatusCollector wcsc = scope != null ? HgWorkingCopyStatusCollector.create(repo, scope) : new HgWorkingCopyStatusCollector(repo); wcsc.setBaseRevisionCollector(sc); @@ -190,25 +195,18 @@ sc.walk(startRevision, endRevision, mediator); } } - } catch (HgCallbackTargetException.Wrap ex) { - // seems too general to catch RuntimeException, i.e. - // unless catch is for very narrow piece of code, it's better not to catch any RTE (which may happen elsewhere, not only in handler) - // XXX Perhaps, need more detailed explanation in handlers that are expected to throw Wrap/RTE (i.e. HgChangesetHandler) - throw new HgCallbackTargetException(ex).setRevisionIndex(endRevision); + } catch (CancelledException ex) { + // this is our exception, thrown from Mediator. + // next check shall throw original cause of the stop - either HgCallbackTargetException or original CancelledException + mediator.checkFailure(); + } catch (HgRuntimeException ex) { + throw new HgLibraryFailureException(ex); } finally { mediator.done(); } } - /** - * @deprecated replaced with {@link HgStatusHandler} - */ - @Deprecated - public interface Handler extends HgStatusHandler{ - void handleStatus(HgStatus s); - } - - private class Mediator implements HgStatusInspector { + private class Mediator implements HgStatusInspector, CancelSupport { boolean needModified; boolean needAdded; boolean needRemoved; @@ -219,68 +217,117 @@ boolean needCopies; HgStatusHandler handler; private ChangelogHelper logHelper; + private CancelSupport handlerCancelSupport; + private HgCallbackTargetException failure; + private CancelledException cancellation; Mediator() { } - public void start(HgStatusHandler h, ChangelogHelper changelogHelper) { + public void start(HgStatusHandler h, CancelSupport hcs, ChangelogHelper changelogHelper) { handler = h; + handlerCancelSupport = hcs; logHelper = changelogHelper; } public void done() { handler = null; + handlerCancelSupport = null; logHelper = null; + failure = null; + cancellation = null; } public boolean busy() { return handler != null; } + // XXX copy from ChangesetTransformer. Perhaps, can share the code? + public void checkFailure() throws HgCallbackTargetException, CancelledException { + // do not forget to clear exceptions for reuse of this instance + if (failure != null) { + HgCallbackTargetException toThrow = failure; + failure = null; + throw toThrow; + } + if (cancellation != null) { + CancelledException toThrow = cancellation; + cancellation = null; + throw toThrow; + } + } + + // XXX copy from ChangesetTransformer. code sharing note above applies + public void checkCancelled() throws CancelledException { + if (failure != null || cancellation != null) { + // stop status iteration. Our exception is for the purposes of cancellation only, + // the one we have stored (this.cancellation) is for user + throw new CancelledException(); + } + } + + private void dispatch(HgStatus s) { + try { + handler.status(s); + handlerCancelSupport.checkCancelled(); + } catch (HgCallbackTargetException ex) { + failure = ex; + } catch (CancelledException ex) { + cancellation = ex; + } + } + public void modified(Path fname) { if (needModified) { - handler.handleStatus(new HgStatus(Modified, fname, logHelper)); + dispatch(new HgStatus(Modified, fname, logHelper)); } } public void added(Path fname) { if (needAdded) { - handler.handleStatus(new HgStatus(Added, fname, logHelper)); + dispatch(new HgStatus(Added, fname, logHelper)); } } public void removed(Path fname) { if (needRemoved) { - handler.handleStatus(new HgStatus(Removed, fname, logHelper)); + dispatch(new HgStatus(Removed, fname, logHelper)); } } public void copied(Path fnameOrigin, Path fnameAdded) { if (needCopies) { - // FIXME in fact, merged files may report 'copied from' as well, correct status kind thus may differ from Added - handler.handleStatus(new HgStatus(Added, fnameAdded, fnameOrigin, logHelper)); + // TODO post-1.0 in fact, merged files may report 'copied from' as well, correct status kind thus may differ from Added + dispatch(new HgStatus(Added, fnameAdded, fnameOrigin, logHelper)); } } public void missing(Path fname) { if (needMissing) { - handler.handleStatus(new HgStatus(Missing, fname, logHelper)); + dispatch(new HgStatus(Missing, fname, logHelper)); } } public void unknown(Path fname) { if (needUnknown) { - handler.handleStatus(new HgStatus(Unknown, fname, logHelper)); + dispatch(new HgStatus(Unknown, fname, logHelper)); } } public void clean(Path fname) { if (needClean) { - handler.handleStatus(new HgStatus(Clean, fname, logHelper)); + dispatch(new HgStatus(Clean, fname, logHelper)); } } public void ignored(Path fname) { if (needIgnored) { - handler.handleStatus(new HgStatus(Ignored, fname, logHelper)); + dispatch(new HgStatus(Ignored, fname, logHelper)); } } - public void invalid(Path fname, Exception ex) { - handler.handleError(fname, new Status(Status.Kind.ERROR, "Failed to get file status", ex)); + public void invalid(Path fname, Exception err) { + try { + handler.error(fname, new Outcome(Outcome.Kind.Failure, "Failed to get file status", err)); + handlerCancelSupport.checkCancelled(); + } catch (HgCallbackTargetException ex) { + failure = ex; + } catch (CancelledException ex) { + cancellation = ex; + } } } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgStatusHandler.java --- a/src/org/tmatesoft/hg/core/HgStatusHandler.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgStatusHandler.java Wed Jul 11 20:40:47 2012 +0200 @@ -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,27 +16,32 @@ */ package org.tmatesoft.hg.core; +import org.tmatesoft.hg.internal.Callback; import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.Status; +import org.tmatesoft.hg.util.Outcome; /** * Callback to process {@link HgStatus} objects. * @author Artem Tikhomirov * @author TMate Software Ltd. */ +@Callback public interface HgStatusHandler { - /* XXX #next() as in HgChangesetHandler? - * perhaps, handle() is better name? If yes, rename method in HgChangesetHandler, too, to make them similar. - * void next(HgStatus s); - * XXX describe RTE and HgCallbackTargetException + /** + * Report status of the next file + * + * @param s file status descriptor + * @throws HgCallbackTargetException wrapper for any exception user code may produce */ - void handleStatus(HgStatus s); + void status(HgStatus s) throws HgCallbackTargetException; /** * Report non-critical error processing single file during status operation + * * @param file name of the file that caused the trouble * @param s error description object + * @throws HgCallbackTargetException wrapper for any exception user code may produce */ - void handleError(Path file, Status s); + void error(Path file, Outcome s) throws HgCallbackTargetException; } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/HgUpdateConfigCommand.java --- a/src/org/tmatesoft/hg/core/HgUpdateConfigCommand.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/HgUpdateConfigCommand.java Wed Jul 11 20:40:47 2012 +0200 @@ -111,6 +111,11 @@ throw new UnsupportedOperationException(); } + /** + * Perform config file update + * + * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state + */ public void execute() throws HgException { try { ConfigFile cfg = new ConfigFile(); @@ -131,7 +136,8 @@ } cfg.writeTo(configFile); } catch (IOException ex) { - throw new HgInvalidFileException("Failed to update configuration file", ex, configFile); + String m = String.format("Failed to update configuration file %s", configFile); + throw new HgBadArgumentException(m, ex); // TODO [post-1.0] better exception, it's not bad argument case } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/SessionContext.java --- a/src/org/tmatesoft/hg/core/SessionContext.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/SessionContext.java Wed Jul 11 20:40:47 2012 +0200 @@ -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,24 +16,33 @@ */ package org.tmatesoft.hg.core; -import org.tmatesoft.hg.internal.Experimental; import org.tmatesoft.hg.util.LogFacility; /** - * WORK IN PROGRESS + * Access to objects that might need to be shared between various distinct operations ran during the same working session + * (i.e. caches, log, etc.). It's unspecified whether session context is per repository or can span multiple repositories * - * Access to objects that might need to be shared between various distinct operations ran during the same working session - * (i.e. caches, log, etc.). It's unspecified whether session context is per repository or can span multiple repositories + *

Note, API is likely to be extended in future versions, adding more object to share. * * @author Artem Tikhomirov * @author TMate Software Ltd. */ -@Experimental(reason="Work in progress") -public interface SessionContext { - LogFacility getLog(); +public abstract class SessionContext { + // abstract class to facilitate adding more functionality without API break /** - * LIKELY TO CHANGE TO STANDALONE CONFIGURATION OBJECT + * Access wrapper for a system log facility. + * @return facility to direct dumps to, never null */ - Object getProperty(String name, Object defaultValue); + public abstract LogFacility getLog(); + + /** + * Access configuration parameters of the session. + * @param name name of the session configuration parameter + * @param defaultValue value to return if parameter is not configured + * @return value of the session parameter, defaultValue if none found + */ + public abstract Object getConfigurationProperty(String name, Object defaultValue); + // perhaps, later may add Configuration object, with PropertyMarshal's helpers + // e.g. when there's standalone Caches and WritableSessionProperties objects } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/core/package.html --- a/src/org/tmatesoft/hg/core/package.html Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/core/package.html Wed Jul 11 20:40:47 2012 +0200 @@ -1,5 +1,6 @@ - -Hi-level API - + +

Hi-level API

+

Hi-level API to deal with Mercurial repositories using task-oriented commands. Start with {@link org.tmatesoft.hg.core.HgRepoFacade} class

+ \ No newline at end of file diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/BasicSessionContext.java --- a/src/org/tmatesoft/hg/internal/BasicSessionContext.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/BasicSessionContext.java Wed Jul 11 20:40:47 2012 +0200 @@ -21,57 +21,45 @@ import org.tmatesoft.hg.core.SessionContext; import org.tmatesoft.hg.util.LogFacility; -import org.tmatesoft.hg.util.PathPool; -import org.tmatesoft.hg.util.PathRewrite; +import org.tmatesoft.hg.util.LogFacility.Severity; /** * * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class BasicSessionContext implements SessionContext { +public class BasicSessionContext extends SessionContext { - private PathPool pathPool; private LogFacility logFacility; private final Map properties; - public BasicSessionContext(PathPool pathFactory, LogFacility log) { - this(null, pathFactory, log); + public BasicSessionContext(LogFacility log) { + this(null, log); } @SuppressWarnings("unchecked") - public BasicSessionContext(Map propertyOverrides, PathPool pathFactory, LogFacility log) { - pathPool = pathFactory; + public BasicSessionContext(Map propertyOverrides, LogFacility log) { logFacility = log; properties = propertyOverrides == null ? Collections.emptyMap() : (Map) propertyOverrides; } - public PathPool getPathPool() { - if (pathPool == null) { - pathPool = new PathPool(new PathRewrite.Empty()); - } - return pathPool; - } - + @Override public LogFacility getLog() { // e.g. for exceptions that we can't handle but log (e.g. FileNotFoundException when we've checked beforehand file.canRead() if (logFacility == null) { - boolean needDebug = _getBooleanProperty("hg.consolelog.debug", false); - boolean needInfo = needDebug || _getBooleanProperty("hg.consolelog.info", false); - logFacility = new StreamLogFacility(needDebug, needInfo, true, System.out); + PropertyMarshal pm = new PropertyMarshal(this); + boolean needDebug = pm.getBoolean("hg4j.consolelog.debug", false); + boolean needInfo = pm.getBoolean("hg4j.consolelog.info", false); + boolean needTime = pm.getBoolean("hg4j.consolelog.tstamp", true); + Severity l = needDebug ? Severity.Debug : (needInfo ? Severity.Info : Severity.Warn); + logFacility = new StreamLogFacility(l, needTime, System.out); } return logFacility; } - private boolean _getBooleanProperty(String name, boolean defaultValue) { - // can't use and unchecked cast because got no confidence passed properties are strictly of the kind of my default values, - // i.e. if boolean from outside comes as "true", while I pass default as Boolean or vice versa. - Object p = getProperty(name, defaultValue); - return p instanceof Boolean ? ((Boolean) p).booleanValue() : Boolean.parseBoolean(String.valueOf(p)); - } - - // TODO specific helpers for boolean and int values - public Object getProperty(String name, Object defaultValue) { + // specific helpers for boolean and int values are available from PropertyMarshal + @Override + public Object getConfigurationProperty(String name, Object defaultValue) { // NOTE, this method is invoked from getLog(), hence do not call getLog from here unless changed appropriately Object value = properties.get(name); if (value != null) { diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/ByteArrayDataAccess.java --- a/src/org/tmatesoft/hg/internal/ByteArrayDataAccess.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/ByteArrayDataAccess.java Wed Jul 11 20:40:47 2012 +0200 @@ -69,7 +69,7 @@ } @Override public void seek(int offset) { - pos = (int) offset; + pos = offset; } @Override public void skip(int bytes) throws IOException { diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/Callback.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/Callback.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,38 @@ +/* + * Copyright (c) 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 + * 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 java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.tmatesoft.hg.core.HgCallbackTargetException; + +/** + * Marker to ease location of callback interfaces in the API. + * + * All classes/interfaces supposed to be subclassed/implemented by users, with methods throwing {@link HgCallbackTargetException} shall bear the mark. + * Besides, classes that are low-level callbacks (from {@link org.tmatesoft.hg.repo}) shall bear it, too. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ ElementType.TYPE }) +public @interface Callback { +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/ChangelogHelper.java --- a/src/org/tmatesoft/hg/internal/ChangelogHelper.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/ChangelogHelper.java Wed Jul 11 20:40:47 2012 +0200 @@ -16,10 +16,10 @@ */ package org.tmatesoft.hg.internal; -import org.tmatesoft.hg.core.HgInvalidControlFileException; import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; import org.tmatesoft.hg.repo.HgDataFile; import org.tmatesoft.hg.repo.HgInternals; +import org.tmatesoft.hg.repo.HgInvalidControlFileException; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.util.Path; diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/DataAccess.java --- a/src/org/tmatesoft/hg/internal/DataAccess.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/DataAccess.java Wed Jul 11 20:40:47 2012 +0200 @@ -29,13 +29,26 @@ * @author TMate Software Ltd. */ public class DataAccess { - public boolean isEmpty() { + public boolean isEmpty() throws IOException { return true; } - // TODO throws IOException (few subclasses have non-trivial length() operation) - public int length() { + // IOException due to few subclasses that have non-trivial length() operation + // long length and offset are needed only in RevlogStream, makes no sense elsewhere + // because chunks Mercurial operates with fit into int (4 bytes actualLength field) + // For those that may face large pieces of data (actual data streams) there are #longLength + // and #longSeek() to implement + public int length() throws IOException { return 0; } + + public long longLength() throws IOException { + return length(); + } + + public void longSeek(long offset) throws IOException { + seek(Internals.ltoi(offset)); + } + /** * get this instance into initial state * @throws IOException @@ -87,7 +100,8 @@ throw new IOException(String.format("No data, can't read %d bytes", length)); } // reads bytes into ByteBuffer, up to its limit or total data length, whichever smaller - // FIXME perhaps, in DataAccess paradigm (when we read known number of bytes, we shall pass specific byte count to read) + // TODO post-1.0 perhaps, in DataAccess paradigm (when we read known number of bytes, we shall pass specific byte count to read) + // for 1.0, it's ok as it's our internal class public void readBytes(ByteBuffer buf) throws IOException { // int toRead = Math.min(buf.remaining(), (int) length()); // if (buf.hasArray()) { @@ -97,7 +111,7 @@ // readBytes(bb, 0, bb.length); // buf.put(bb); // } - // FIXME optimize to read as much as possible at once + // TODO post-1.0 optimize to read as much as possible at once while (!isEmpty() && buf.hasRemaining()) { buf.put(readByte()); } @@ -107,7 +121,7 @@ } // XXX decide whether may or may not change position in the DataAccess - // FIXME exception handling is not right, just for the sake of quick test + // TODO REVISIT exception handling may not be right, initially just for the sake of quick test public byte[] byteArray() throws IOException { reset(); byte[] rv = new byte[length()]; diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/DataAccessProvider.java --- a/src/org/tmatesoft/hg/internal/DataAccessProvider.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/DataAccessProvider.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -16,6 +16,9 @@ */ package org.tmatesoft.hg.internal; +import static org.tmatesoft.hg.util.LogFacility.Severity.Error; +import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; + import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -23,8 +26,8 @@ import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; -import org.tmatesoft.hg.core.HgBadStateException; import org.tmatesoft.hg.core.SessionContext; +import org.tmatesoft.hg.util.LogFacility; /** * @@ -40,27 +43,28 @@ public static final String CFG_PROPERTY_MAPIO_LIMIT = "hg4j.dap.mapio_limit"; public static final String CFG_PROPERTY_MAPIO_BUFFER_SIZE = "hg4j.dap.mapio_buffer"; public static final String CFG_PROPERTY_FILE_BUFFER_SIZE = "hg4j.dap.file_buffer"; + + private static final int DEFAULT_MAPIO_LIMIT = 100 * 1024; // 100 kB + private static final int DEFAULT_FILE_BUFFER = 8 * 1024; // 8 kB + private static final int DEFAULT_MAPIO_BUFFER = DEFAULT_MAPIO_LIMIT; // same as default boundary private final int mapioMagicBoundary; - private final int bufferSize; + private final int bufferSize, mapioBufSize; private final SessionContext context; public DataAccessProvider(SessionContext ctx) { - this(ctx, getConfigOption(ctx, CFG_PROPERTY_MAPIO_LIMIT, 100 * 1024), getConfigOption(ctx, CFG_PROPERTY_FILE_BUFFER_SIZE, 8 * 1024)); + context = ctx; + PropertyMarshal pm = new PropertyMarshal(ctx); + mapioMagicBoundary = pm.getInt(CFG_PROPERTY_MAPIO_LIMIT, DEFAULT_MAPIO_LIMIT); + bufferSize = pm.getInt(CFG_PROPERTY_FILE_BUFFER_SIZE, DEFAULT_FILE_BUFFER); + mapioBufSize = pm.getInt(CFG_PROPERTY_MAPIO_BUFFER_SIZE, DEFAULT_MAPIO_BUFFER); } - private static int getConfigOption(SessionContext ctx, String optName, int defaultValue) { - Object v = ctx.getProperty(optName, defaultValue); - if (false == v instanceof Number) { - v = Integer.parseInt(v.toString()); - } - return ((Number) v).intValue(); - } - - public DataAccessProvider(SessionContext ctx, int mapioBoundary, int regularBufferSize) { + public DataAccessProvider(SessionContext ctx, int mapioBoundary, int regularBufferSize, int mapioBufferSize) { context = ctx; mapioMagicBoundary = mapioBoundary == 0 ? Integer.MAX_VALUE : mapioBoundary; bufferSize = regularBufferSize; + mapioBufSize = mapioBufferSize; } public DataAccess create(File f) { @@ -69,40 +73,38 @@ } try { FileChannel fc = new FileInputStream(f).getChannel(); - int flen = (int) fc.size(); - if (fc.size() - flen != 0) { - throw new HgBadStateException("Files greater than 2Gb are not yet supported"); - } + long flen = fc.size(); if (flen > mapioMagicBoundary) { // TESTS: bufLen of 1024 was used to test MemMapFileAccess - return new MemoryMapFileAccess(fc, flen, getConfigOption(context, CFG_PROPERTY_MAPIO_BUFFER_SIZE, 100*1024 /*same as default boundary*/)); + return new MemoryMapFileAccess(fc, flen, mapioBufSize, context.getLog()); } else { // XXX once implementation is more or less stable, // may want to try ByteBuffer.allocateDirect() to see // if there's any performance gain. boolean useDirectBuffer = false; // XXX might be another config option // TESTS: bufferSize of 100 was used to check buffer underflow states when readBytes reads chunks bigger than bufSize - return new FileAccess(fc, flen, bufferSize, useDirectBuffer); + return new FileAccess(fc, flen, bufferSize, useDirectBuffer, context.getLog()); } } catch (IOException ex) { // unlikely to happen, we've made sure file exists. - context.getLog().error(getClass(), ex, null); + context.getLog().dump(getClass(), Error, ex, null); } return new DataAccess(); // non-null, empty. } - // DOESN'T WORK YET private static class MemoryMapFileAccess extends DataAccess { private FileChannel fileChannel; - private final int size; private long position = 0; // always points to buffer's absolute position in the file + private MappedByteBuffer buffer; + private final long size; private final int memBufferSize; - private MappedByteBuffer buffer; + private final LogFacility logFacility; - public MemoryMapFileAccess(FileChannel fc, int channelSize, int bufferSize) { + public MemoryMapFileAccess(FileChannel fc, long channelSize, int bufferSize, LogFacility log) { fileChannel = fc; size = channelSize; - memBufferSize = bufferSize > channelSize ? channelSize : bufferSize; // no reason to waste memory more than there's data + logFacility = log; + memBufferSize = bufferSize > channelSize ? (int) channelSize : bufferSize; // no reason to waste memory more than there's data } @Override @@ -111,22 +113,28 @@ } @Override + public DataAccess reset() throws IOException { + longSeek(0); + return this; + } + + @Override public int length() { + return Internals.ltoi(longLength()); + } + + @Override + public long longLength() { return size; } @Override - public DataAccess reset() throws IOException { - seek(0); - return this; - } - - @Override - public void seek(int offset) { + public void longSeek(long offset) { assert offset >= 0; // offset may not necessarily be further than current position in the file (e.g. rewind) if (buffer != null && /*offset is within buffer*/ offset >= position && (offset - position) < buffer.limit()) { - buffer.position((int) (offset - position)); + // cast is ok according to check above + buffer.position(Internals.ltoi(offset - position)); } else { position = offset; buffer = null; @@ -134,6 +142,11 @@ } @Override + public void seek(int offset) { + longSeek(offset); + } + + @Override public void skip(int bytes) throws IOException { assert bytes >= 0; if (buffer == null) { @@ -153,7 +166,27 @@ position += buffer.position(); } long left = size - position; - buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, position, left < memBufferSize ? left : memBufferSize); + for (int i = 0; i < 3; i++) { + try { + buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, position, left < memBufferSize ? left : memBufferSize); + return; + } catch (IOException ex) { + if (i == 2) { + throw ex; + } + if (i == 0) { + // if first attempt failed, try to free some virtual memory, see Issue 30 for details + logFacility.dump(getClass(), Warn, ex, "Memory-map failed, gonna try gc() to free virtual memory"); + } + try { + buffer = null; + System.gc(); + Thread.sleep((1+i) * 1000); + } catch (Throwable t) { + logFacility.dump(getClass(), Error, t, "Bad luck"); + } + } + } } @Override @@ -196,7 +229,7 @@ try { fileChannel.close(); } catch (IOException ex) { - StreamLogFacility.newDefault().debug(getClass(), ex, null); + logFacility.dump(getClass(), Warn, ex, null); } fileChannel = null; } @@ -206,14 +239,16 @@ // (almost) regular file access - FileChannel and buffers. private static class FileAccess extends DataAccess { private FileChannel fileChannel; - private final int size; private ByteBuffer buffer; - private int bufferStartInFile = 0; // offset of this.buffer in the file. + private long bufferStartInFile = 0; // offset of this.buffer in the file. + private final long size; + private final LogFacility logFacility; - public FileAccess(FileChannel fc, int channelSize, int bufferSizeHint, boolean useDirect) { + public FileAccess(FileChannel fc, long channelSize, int bufferSizeHint, boolean useDirect, LogFacility log) { fileChannel = fc; size = channelSize; - final int capacity = size < bufferSizeHint ? size : bufferSizeHint; + logFacility = log; + final int capacity = size < bufferSizeHint ? (int) size : bufferSizeHint; buffer = useDirect ? ByteBuffer.allocateDirect(capacity) : ByteBuffer.allocate(capacity); buffer.flip(); // or .limit(0) to indicate it's empty } @@ -224,23 +259,29 @@ } @Override + public DataAccess reset() throws IOException { + longSeek(0); + return this; + } + + @Override public int length() { - return size; + return Internals.ltoi(longLength()); } @Override - public DataAccess reset() throws IOException { - seek(0); - return this; + public long longLength() { + return size; } - + @Override - public void seek(int offset) throws IOException { + public void longSeek(long offset) throws IOException { if (offset > size) { throw new IllegalArgumentException(String.format("Can't seek to %d for the file of size %d (buffer start:%d)", offset, size, bufferStartInFile)); } if (offset < bufferStartInFile + buffer.limit() && offset >= bufferStartInFile) { - buffer.position((int) (offset - bufferStartInFile)); + // cast to int is safe, we've checked we fit into buffer + buffer.position(Internals.ltoi(offset - bufferStartInFile)); } else { // out of current buffer, invalidate it (force re-read) // XXX or ever re-read it right away? @@ -252,6 +293,11 @@ } @Override + public void seek(int offset) throws IOException { + longSeek(offset); + } + + @Override public void skip(int bytes) throws IOException { final int newPos = buffer.position() + bytes; if (newPos >= 0 && newPos < buffer.limit()) { @@ -259,7 +305,7 @@ buffer.position(newPos); } else { // - seek(bufferStartInFile + newPos); + longSeek(bufferStartInFile + newPos); } } @@ -317,7 +363,7 @@ try { fileChannel.close(); } catch (IOException ex) { - StreamLogFacility.newDefault().debug(getClass(), ex, null); + logFacility.dump(getClass(), Warn, ex, null); } fileChannel = null; } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/DigestHelper.java --- a/src/org/tmatesoft/hg/internal/DigestHelper.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/DigestHelper.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -21,8 +21,8 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import org.tmatesoft.hg.core.HgBadStateException; import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.HgInvalidStateException; /** @@ -50,7 +50,9 @@ sha1 = MessageDigest.getInstance("SHA-1"); } catch (NoSuchAlgorithmException ex) { // could hardly happen, JDK from Sun always has sha1. - throw new HgBadStateException(ex); + HgInvalidStateException t = new HgInvalidStateException("Need SHA-1 algorithm for nodeid calculation"); + t.initCause(ex); + throw t; } } return sha1; diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/EncodingHelper.java --- a/src/org/tmatesoft/hg/internal/EncodingHelper.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/EncodingHelper.java Wed Jul 11 20:40:47 2012 +0200 @@ -16,14 +16,20 @@ */ package org.tmatesoft.hg.internal; +import static org.tmatesoft.hg.util.LogFacility.Severity.Error; + import java.nio.ByteBuffer; +import java.nio.CharBuffer; import java.nio.charset.CharacterCodingException; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CharsetEncoder; +import org.tmatesoft.hg.core.SessionContext; + /** * Keep all encoding-related issues in the single place + * NOT thread-safe (encoder and decoder requires synchronized access) * @author Artem Tikhomirov * @author TMate Software Ltd. */ @@ -34,28 +40,64 @@ * http://mercurial.808500.n3.nabble.com/Unicode-support-request-td3430704.html */ + private final SessionContext sessionContext; private final CharsetEncoder encoder; private final CharsetDecoder decoder; - EncodingHelper(Charset fsEncoding) { + EncodingHelper(Charset fsEncoding, SessionContext ctx) { + sessionContext = ctx; decoder = fsEncoding.newDecoder(); encoder = fsEncoding.newEncoder(); } + /** + * Translate file names from manifest to amazing Unicode string + */ public String fromManifest(byte[] data, int start, int length) { + return decodeWithSystemDefaultFallback(data, start, length); + } + + /** + * @return byte representation of the string directly comparable to bytes in manifest + */ + public byte[] toManifest(String s) { + if (s == null) { + // perhaps, can return byte[0] in this case? + throw new IllegalArgumentException(); + } + try { + // synchonized(encoder) { + ByteBuffer bb = encoder.encode(CharBuffer.wrap(s)); + // } + byte[] rv = new byte[bb.remaining()]; + bb.get(rv, 0, rv.length); + return rv; + } catch (CharacterCodingException ex) { + sessionContext.getLog().dump(getClass(), Error, ex, String.format("Use of charset %s failed, resort to system default", charset().name())); + // resort to system-default + return s.getBytes(); + } + } + + /** + * Translate file names from dirstate to amazing Unicode string + */ + public String fromDirstate(byte[] data, int start, int length) { + return decodeWithSystemDefaultFallback(data, start, length); + } + + private String decodeWithSystemDefaultFallback(byte[] data, int start, int length) { try { return decoder.decode(ByteBuffer.wrap(data, start, length)).toString(); } catch (CharacterCodingException ex) { + sessionContext.getLog().dump(getClass(), Error, ex, String.format("Use of charset %s failed, resort to system default", charset().name())); // resort to system-default return new String(data, start, length); } } - public String fromDirstate(byte[] data, int start, int length) throws CharacterCodingException { - return decoder.decode(ByteBuffer.wrap(data, start, length)).toString(); + private Charset charset() { + return encoder.charset(); } - public Charset charset() { - return encoder.charset(); - } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/ExceptionInfo.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/ExceptionInfo.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,168 @@ +/* + * Copyright (c) 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 + * 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.repo.HgRepository.BAD_REVISION; +import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION; +import static org.tmatesoft.hg.repo.HgRepository.TIP; +import static org.tmatesoft.hg.repo.HgRepository.WORKING_COPY; + +import java.io.File; + +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.Path; + +/** + * Extras to record with exception to describe it better. + * XXX perhaps, not only with exception, may utilize it with status object? + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class ExceptionInfo { + protected final T owner; + protected Integer revNumber = null; + protected Nodeid revision; + protected Path filename; + protected File localFile; + // next two make sense only when revNumber was set + private int rangeLeftBoundary = BAD_REVISION, rangeRightBoundary = BAD_REVISION; + + /** + * @param owner instance to return from setters + */ + public ExceptionInfo(T owner) { + this.owner = owner; + } + + /** + * @return not {@link HgRepository#BAD_REVISION} only when revision index was supplied at the construction time + */ + public int getRevisionIndex() { + return revNumber == null ? HgRepository.BAD_REVISION : revNumber; + } + + public T setRevisionIndex(int rev) { + revNumber = rev; + return owner; + } + + public boolean isRevisionIndexSet() { + return revNumber != null; + } + + /** + * @return non-null only when revision was supplied at construction time + */ + public Nodeid getRevision() { + return revision; + } + + public T setRevision(Nodeid r) { + revision = r; + return owner; + } + + public boolean isRevisionSet() { + return revision != null; + } + + /** + * @return non-null only if file name was set at construction time + */ + public Path getFileName() { + return filename; + } + + public T setFileName(Path name) { + filename = name; + return owner; + } + + public T setFile(File file) { + localFile = file; + return owner; + } + + /** + * @return file object that causes troubles, or null if specific file is unknown + */ + public File getFile() { + return localFile; + } + + public T setRevisionIndexBoundary(int revisionIndex, int rangeLeft, int rangeRight) { + setRevisionIndex(revisionIndex); + rangeLeftBoundary = rangeLeft; + rangeRightBoundary = rangeRight; + return owner; + } + + public StringBuilder appendDetails(StringBuilder sb) { + if (filename != null) { + sb.append("path:'"); + sb.append(filename); + sb.append('\''); + sb.append(';'); + sb.append(' '); + } + sb.append("rev:"); + boolean needNodeid = true; + if (isRevisionIndexSet()) { + if (rangeLeftBoundary != BAD_REVISION || rangeRightBoundary != BAD_REVISION) { + String sr; + switch (getRevisionIndex()) { + case BAD_REVISION: + sr = "UNKNOWN"; break; + case TIP: + sr = "TIP"; break; + case WORKING_COPY: + sr = "WORKING-COPY"; break; + case NO_REVISION: + sr = "NO REVISION"; break; + default: + sr = String.valueOf(getRevisionIndex()); + } + sb.append(String.format("%s is not from [%d..%d]", sr, rangeLeftBoundary, rangeRightBoundary)); + } else { + sb.append(getRevisionIndex()); + if (isRevisionSet()) { + sb.append(':'); + sb.append(getRevision().shortNotation()); + needNodeid = false; + } + } + } + if (isRevisionSet() && needNodeid) { + sb.append(getRevision().shortNotation()); + } + if (localFile != null) { + sb.append(';'); + sb.append(' '); + sb.append(" file:"); + sb.append(localFile.getPath()); + sb.append(','); + if (localFile.exists()) { + sb.append("EXISTS"); + } else { + sb.append("DOESN'T EXIST"); + } + } + return sb; + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/FilterDataAccess.java --- a/src/org/tmatesoft/hg/internal/FilterDataAccess.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/FilterDataAccess.java Wed Jul 11 20:40:47 2012 +0200 @@ -28,18 +28,18 @@ */ public class FilterDataAccess extends DataAccess { private final DataAccess dataAccess; - private final int offset; + private final long offset; private final int length; private int count; - public FilterDataAccess(DataAccess dataAccess, int offset, int length) { + public FilterDataAccess(DataAccess dataAccess, long offset, int length) { this.dataAccess = dataAccess; this.offset = offset; this.length = length; count = length; } - protected int available() { + protected int available() throws IOException { return count; } @@ -50,12 +50,12 @@ } @Override - public boolean isEmpty() { + public boolean isEmpty() throws IOException { return count <= 0; } @Override - public int length() { + public int length() throws IOException { return length; } @@ -64,8 +64,8 @@ if (localOffset < 0 || localOffset > length) { throw new IllegalArgumentException(); } - dataAccess.seek(offset + localOffset); - count = (int) (length - localOffset); + dataAccess.longSeek(offset + localOffset); + count = length - localOffset; } @Override @@ -91,7 +91,7 @@ throw new IOException(String.format("Underflow. Bytes left: %d. FilterDA[offset: %d, length: %d]", count, offset, length)); } if (count == length) { - dataAccess.seek(offset); + dataAccess.longSeek(offset); } count--; return dataAccess.readByte(); @@ -106,7 +106,7 @@ throw new IOException(String.format("Underflow. Bytes left: %d, asked to read %d. FilterDA[offset: %d, length: %d]", count, len, offset, length)); } if (count == length) { - dataAccess.seek(offset); + dataAccess.longSeek(offset); } dataAccess.readBytes(b, off, len); count -= len; diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/IdentityPool.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/IdentityPool.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2011 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 org.tmatesoft.hg.util.Convertor; +import org.tmatesoft.hg.util.DirectHashSet; + + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class IdentityPool implements Convertor { + private final DirectHashSet unify = new DirectHashSet(); + + public IdentityPool() { + } + + public IdentityPool(int sizeHint) { + } + + public T mangle(T t) { + return unify(t); + } + + public T unify(T t) { + T rv = unify.get(t); + if (rv == null) { + // first time we see a new value + unify.put(t); + rv = t; + } + return rv; + } + + public boolean contains(T t) { + return unify.get(t) != null; + } + + public void record(T t) { + unify.put(t); + } + + public void clear() { + unify.clear(); + } + + public int size() { + return unify.size(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(IdentityPool.class.getSimpleName()); + sb.append('@'); + sb.append(Integer.toString(System.identityHashCode(this))); + sb.append(' '); + sb.append(unify.toString()); + return sb.toString(); + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/InflaterDataAccess.java --- a/src/org/tmatesoft/hg/internal/InflaterDataAccess.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/InflaterDataAccess.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -22,9 +22,6 @@ import java.util.zip.Inflater; import java.util.zip.ZipException; -import org.tmatesoft.hg.core.HgBadStateException; - - /** * DataAccess counterpart for InflaterInputStream. * XXX is it really needed to be subclass of FilterDataAccess? @@ -40,15 +37,15 @@ private int decompressedPos = 0; private int decompressedLength; - public InflaterDataAccess(DataAccess dataAccess, int offset, int compressedLength) { + public InflaterDataAccess(DataAccess dataAccess, long offset, int compressedLength) { this(dataAccess, offset, compressedLength, -1, new Inflater(), new byte[512]); } - public InflaterDataAccess(DataAccess dataAccess, int offset, int compressedLength, int actualLength) { + public InflaterDataAccess(DataAccess dataAccess, long offset, int compressedLength, int actualLength) { this(dataAccess, offset, compressedLength, actualLength, new Inflater(), new byte[512]); } - public InflaterDataAccess(DataAccess dataAccess, int offset, int compressedLength, int actualLength, Inflater inflater, byte[] buf) { + public InflaterDataAccess(DataAccess dataAccess, long offset, int compressedLength, int actualLength, Inflater inflater, byte[] buf) { super(dataAccess, offset, compressedLength); if (inflater == null || buf == null) { throw new IllegalArgumentException(); @@ -67,50 +64,45 @@ } @Override - protected int available() { + protected int available() throws IOException { return length() - decompressedPos; } @Override - public boolean isEmpty() { + public boolean isEmpty() throws IOException { // can't use super.available() <= 0 because even when 0 < super.count < 6(?) // decompressedPos might be already == length() return available() <= 0; } @Override - public int length() { + public int length() throws IOException { if (decompressedLength != -1) { return decompressedLength; } decompressedLength = 0; // guard to avoid endless loop in case length() would get invoked from below. int c = 0; - try { - int oldPos = decompressedPos; - byte[] dummy = new byte[buffer.length]; - int toRead; - while ((toRead = super.available()) > 0) { - if (toRead > buffer.length) { - toRead = buffer.length; + int oldPos = decompressedPos; + byte[] dummy = new byte[buffer.length]; + int toRead; + while ((toRead = super.available()) > 0) { + if (toRead > buffer.length) { + toRead = buffer.length; + } + super.readBytes(buffer, 0, toRead); + inflater.setInput(buffer, 0, toRead); + try { + while (!inflater.needsInput()) { + c += inflater.inflate(dummy, 0, dummy.length); } - super.readBytes(buffer, 0, toRead); - inflater.setInput(buffer, 0, toRead); - try { - while (!inflater.needsInput()) { - c += inflater.inflate(dummy, 0, dummy.length); - } - } catch (DataFormatException ex) { - throw new HgBadStateException(ex); - } + } catch (DataFormatException ex) { + throw new IOException(ex.toString()); } - decompressedLength = c + oldPos; - reset(); - seek(oldPos); - return decompressedLength; - } catch (IOException ex) { - decompressedLength = -1; // better luck next time? - throw new HgBadStateException(ex); // XXX perhaps, checked exception } + decompressedLength = c + oldPos; + reset(); + seek(oldPos); + return decompressedLength; } @Override @@ -119,10 +111,10 @@ throw new IllegalArgumentException(); } if (localOffset >= decompressedPos) { - skip((int) (localOffset - decompressedPos)); + skip(localOffset - decompressedPos); } else { reset(); - skip((int) localOffset); + skip(localOffset); } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/IntMap.java --- a/src/org/tmatesoft/hg/internal/IntMap.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/IntMap.java Wed Jul 11 20:40:47 2012 +0200 @@ -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.Iterator; +import java.util.Map; +import java.util.Map.Entry; import java.util.NoSuchElementException; @@ -141,8 +144,73 @@ size -= count; } } + + // document iterator is non-modifying (neither remove() nor setValue() works) + // perhaps, may also implement Iterable to use nice for() + public Iterator> entryIterator() { + class E implements Map.Entry { + private Integer key; + private V value; + + public Integer getKey() { + return key; + } + + public V getValue() { + return value; + } + + public V setValue(V value) { + throw new UnsupportedOperationException(); + } + + void init(Integer k, V v) { + key = k; + value = v; + } + } + + return new Iterator>() { + private int i = 0; + private final E entry = new E(); + private final int _size; + private final int[] _keys; + private final Object[] _values; + + { + _size = IntMap.this.size; + _keys = IntMap.this.keys; + _values = IntMap.this.values; + } + + public boolean hasNext() { + return i < _size; + } + + public Entry next() { + if (i >= _size) { + throw new NoSuchElementException(); + } + @SuppressWarnings("unchecked") + V val = (V) _values[i]; + entry.init(_keys[i], val); + i++; + return entry; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } - + public Map fill(Map map) { + for (Iterator> it = entryIterator(); it.hasNext(); ) { + Map.Entry next = it.next(); + map.put(next.getKey(), next.getValue()); + } + return map; + } // copy of Arrays.binarySearch, with upper search limit as argument private static int binarySearch(int[] a, int high, int key) { diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/Internals.java --- a/src/org/tmatesoft/hg/internal/Internals.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/Internals.java Wed Jul 11 20:40:47 2012 +0200 @@ -17,6 +17,7 @@ package org.tmatesoft.hg.internal; import static org.tmatesoft.hg.internal.RequiresFile.*; +import static org.tmatesoft.hg.util.LogFacility.Severity.Error; import java.io.File; import java.io.FileOutputStream; @@ -32,6 +33,7 @@ import org.tmatesoft.hg.core.SessionContext; import org.tmatesoft.hg.repo.HgInternals; +import org.tmatesoft.hg.repo.HgInvalidControlFileException; import org.tmatesoft.hg.repo.HgRepoConfig.ExtensionsSection; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.util.PathRewrite; @@ -80,18 +82,16 @@ private final boolean shallCacheRevlogsInRepo; public Internals(SessionContext ctx) { - this.sessionContext = ctx; + 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)); + shallCacheRevlogsInRepo = new PropertyMarshal(ctx).getBoolean(CFG_PROPERTY_REVLOG_STREAM_CACHE, true); } - public void parseRequires(HgRepository hgRepo, File requiresFile) { + public void parseRequires(HgRepository hgRepo, File requiresFile) throws HgInvalidControlFileException { try { new RequiresFile().parse(this, requiresFile); } catch (IOException ex) { - // FIXME not quite sure error reading requires file shall be silently logged only. - HgInternals.getContext(hgRepo).getLog().error(getClass(), ex, null); + throw new HgInvalidControlFileException("Parse failed", ex, requiresFile); } } @@ -169,11 +169,11 @@ } public EncodingHelper buildFileNameEncodingHelper() { - return new EncodingHelper(getFileEncoding()); + return new EncodingHelper(getFileEncoding(), sessionContext); } private Charset getFileEncoding() { - Object altEncoding = sessionContext.getProperty(CFG_PROPERTY_FS_FILENAME_ENCODING, null); + Object altEncoding = sessionContext.getConfigurationProperty(CFG_PROPERTY_FS_FILENAME_ENCODING, null); Charset cs; if (altEncoding == null) { cs = Charset.defaultCharset(); @@ -183,7 +183,7 @@ } 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 - sessionContext.getLog().error(Internals.class, ex, String.format("Bad configuration value for filename encoding %s", altEncoding)); + sessionContext.getLog().dump(Internals.class, Error, ex, String.format("Bad configuration value for filename encoding %s", altEncoding)); cs = Charset.defaultCharset(); } } @@ -195,13 +195,36 @@ } /** + * @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 true 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 true 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.getProperty(CFG_PROPERTY_HG_INSTALL_ROOT, null); + String p = (String) ctx.getConfigurationProperty(CFG_PROPERTY_HG_INSTALL_ROOT, null); if (p != null) { return new File(p); } @@ -276,7 +299,7 @@ if (f.canRead() && f.isDirectory()) { return listConfigFiles(f); } - // FIXME query registry, e.g. with + // TODO post-1.0 query registry, e.g. with // Runtime.exec("reg query HKLM\Software\Mercurial") // f = new File("C:\\Mercurial\\Mercurial.ini"); @@ -371,4 +394,15 @@ } 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; + } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/KeywordFilter.java --- a/src/org/tmatesoft/hg/internal/KeywordFilter.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/KeywordFilter.java Wed Jul 11 20:40:47 2012 +0200 @@ -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,17 +16,18 @@ */ package org.tmatesoft.hg.internal; +import static org.tmatesoft.hg.util.LogFacility.Severity.Error; + import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Date; import java.util.TreeMap; -import org.tmatesoft.hg.core.HgException; -import org.tmatesoft.hg.core.HgInvalidControlFileException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; import org.tmatesoft.hg.repo.HgInternals; import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgRuntimeException; import org.tmatesoft.hg.util.Pair; import org.tmatesoft.hg.util.Path; @@ -71,7 +72,7 @@ l = s.length(); } } - // FIXME later may implement #filter() not to read full kw value (just "$kw:"). However, limit of maxLen + 2 would keep valid. + // TODO post-1.0 later may implement #filter() not to read full kw value (just "$kw:"). However, limit of maxLen + 2 would keep valid. // for buffers less then minBufferLen, there are chances #filter() implementation would never end // (i.e. for input "$LongestKey"$ minBufferLen = l + 2 + (isExpanding ? 0 : 120 /*any reasonable constant for max possible kw value length*/); @@ -113,7 +114,7 @@ // end of buffer reached if (rv == null) { if (keywordStart == x) { - // FIXME in fact, x might be equal to keywordStart and to src.position() here ('$' is first character in the buffer, + // TODO post-1.0 in fact, x might be equal to keywordStart and to src.position() here ('$' is first character in the buffer, // and there are no other '$' not eols till the end of the buffer). This would lead to deadlock (filter won't consume any // bytes). To prevent this, either shall copy bytes [keywordStart..buffer.limit()) to local buffer and use it on the next invocation, // or add lookup of the keywords right after first '$' is found (do not wait for closing '$'). For now, large enough src buffer would be sufficient @@ -259,11 +260,12 @@ private String revision() { try { - // FIXME add cset's nodeid into Changeset class + // TODO post-1.0 Either add cset's nodeid into Changeset class or use own inspector + // when accessing changelog, see below, #getChangeset int csetRev = repo.getFileNode(path).getChangesetRevisionIndex(HgRepository.TIP); return repo.getChangelog().getRevision(csetRev).shortNotation(); - } catch (HgException ex) { - HgInternals.getContext(repo).getLog().error(getClass(), ex, null); + } catch (HgRuntimeException ex) { + HgInternals.getContext(repo).getLog().dump(getClass(), Error, ex, null); return Nodeid.NULL.shortNotation(); // XXX perhaps, might return anything better? Not sure how hg approaches this. } } @@ -271,8 +273,8 @@ private String username() { try { return getChangeset().user(); - } catch (HgException ex) { - HgInternals.getContext(repo).getLog().error(getClass(), ex, null); + } catch (HgRuntimeException ex) { + HgInternals.getContext(repo).getLog().dump(getClass(), Error, ex, null); return ""; } } @@ -281,16 +283,19 @@ Date d; try { d = getChangeset().date(); - } catch (HgException ex) { - HgInternals.getContext(repo).getLog().error(getClass(), ex, null); + } catch (HgRuntimeException ex) { + HgInternals.getContext(repo).getLog().dump(getClass(), Error, ex, null); d = new Date(0l); } return String.format("%tY/% idsMap; private final TreeMap flagsMap; - private final Pool idsPool; - private final Pool namesPool; + private final Convertor idsPool; + private final Convertor namesPool; private Nodeid changeset; private int changelogRev; // optional pools for effective management of nodeids and filenames (they are likely // to be duplicated among different manifest revisions - public ManifestRevision(Pool nodeidPool, Pool filenamePool) { + public ManifestRevision(Pool nodeidPool, Convertor filenamePool) { idsPool = nodeidPool; namesPool = filenamePool; idsMap = new TreeMap(); @@ -57,7 +57,8 @@ } public HgManifest.Flags flags(Path fname) { - return flagsMap.get(fname); + HgManifest.Flags f = flagsMap.get(fname); + return f == null ? HgManifest.Flags.RegularFile : f; } /** @@ -73,20 +74,17 @@ // - public boolean next(Nodeid nid, String fname, String flags) { - throw new HgBadStateException(HgManifest.Inspector2.class.getName()); - } - public boolean next(Nodeid nid, Path fname, HgManifest.Flags flags) { if (namesPool != null) { - fname = namesPool.unify(fname); + fname = namesPool.mangle(fname); } if (idsPool != null) { - nid = idsPool.unify(nid); + nid = idsPool.mangle(nid); } idsMap.put(fname, nid); - if (flags != null) { - // TreeMap$Entry takes 32 bytes. No reason to keep null for such price + if (flags != HgManifest.Flags.RegularFile) { + // TreeMap$Entry takes 32 bytes. No reason to keep regular file attribute (in fact, no flags state) + // for such price // Alternatively, Map> might come as a solution // however, with low rate of elements with flags this would consume more memory // than two distinct maps (sizeof(Pair) == 16). diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/NewlineFilter.java --- a/src/org/tmatesoft/hg/internal/NewlineFilter.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/NewlineFilter.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -21,6 +21,7 @@ import static org.tmatesoft.hg.internal.Filter.Direction.FromRepo; import static org.tmatesoft.hg.internal.Filter.Direction.ToRepo; import static org.tmatesoft.hg.internal.KeywordFilter.copySlice; +import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; import java.io.File; import java.io.IOException; @@ -28,8 +29,8 @@ import java.util.ArrayList; import java.util.Map; -import org.tmatesoft.hg.core.HgBadStateException; import org.tmatesoft.hg.repo.HgInternals; +import org.tmatesoft.hg.repo.HgInvalidStateException; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.util.Adaptable; import org.tmatesoft.hg.util.Path; @@ -67,7 +68,7 @@ public ByteBuffer filter(ByteBuffer src) { if (!processInconsistent && !previewDone) { - throw new HgBadStateException("This filter requires preview operation prior to actual filtering when eol.only-consistent is true"); + throw new HgInvalidStateException("This filter requires preview operation prior to actual filtering when eol.only-consistent is true"); } if (!processInconsistent && foundLoneLF && foundCRLF) { // do not process inconsistent newlines @@ -270,7 +271,9 @@ for (int i = max(pos-10, 0), x = min(pos + 10, b.limit()); i < x; i++) { sb.append(String.format("%02x ", b.get(i))); } - throw new HgBadStateException(String.format("Inconsistent newline characters in the stream %s (char 0x%x, local index:%d)", sb.toString(), b.get(pos), pos)); + // TODO post-1.0 need HgBadDataException (not InvalidState but smth closer to data stream error) + // but don't want to add class for the single use now + throw new HgInvalidStateException(String.format("Inconsistent newline characters in the stream %s (char 0x%x, local index:%d)", sb.toString(), b.get(pos), pos)); } private static int indexOf(byte ch, ByteBuffer b, int from) { @@ -313,7 +316,7 @@ try { hgeol.addLocation(cfgFile); } catch (IOException ex) { - HgInternals.getContext(hgRepo).getLog().warn(getClass(), ex, null); + HgInternals.getContext(hgRepo).getLog().dump(getClass(), Warn, ex, null); } nativeRepoFormat = hgeol.getSection("repository").get("native"); if (nativeRepoFormat == null) { @@ -336,7 +339,7 @@ } else if ("BIN".equals(e.getValue())) { binPatterns.add(e.getKey()); } else { - HgInternals.getContext(hgRepo).getLog().warn(getClass(), "Can't recognize .hgeol entry: %s for %s", e.getValue(), e.getKey()); + HgInternals.getContext(hgRepo).getLog().dump(getClass(), Warn, "Can't recognize .hgeol entry: %s for %s", e.getValue(), e.getKey()); } } if (!crlfPatterns.isEmpty()) { diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/Patch.java --- a/src/org/tmatesoft/hg/internal/Patch.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/Patch.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -21,7 +21,8 @@ import java.util.Formatter; /** - * @see http://mercurial.selenic.com/wiki/BundleFormat, in Changelog group description + * @see http://mercurial.selenic.com/wiki/BundleFormat + * in Changelog group description * * range [start..end] in original source gets replaced with data of length (do not keep, use data.length instead) * range [end(i)..start(i+1)] is copied from the source @@ -119,7 +120,7 @@ } baseRevisionContent.seek(prevEnd); // copy everything in the source past last record's end - baseRevisionContent.readBytes(rv, destIndex, (int) (baseRevisionContent.length() - prevEnd)); + baseRevisionContent.readBytes(rv, destIndex, (baseRevisionContent.length() - prevEnd)); return rv; } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/PathGlobMatcher.java --- a/src/org/tmatesoft/hg/internal/PathGlobMatcher.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/PathGlobMatcher.java Wed Jul 11 20:40:47 2012 +0200 @@ -52,7 +52,7 @@ // HgIgnore.glob2regex is similar, but IsIgnore solves slightly different task // (need to match partial paths, e.g. for glob 'bin' shall match not only 'bin' folder, but also any path below it, // which is not generally the case - private static String glob2regexp(String glob) { // FIXME TESTS NEEDED!!! + private static String glob2regexp(String glob) { // TODO TESTS NEEDED!!! int end = glob.length() - 1; if (glob.length() > 2 && glob.charAt(end) == '*' && glob.charAt(end - 1) == '.') { end-=2; diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/PathPool.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/PathPool.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,109 @@ +/* + * 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 + * 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 java.lang.ref.SoftReference; +import java.util.WeakHashMap; + +import org.tmatesoft.hg.util.Convertor; +import org.tmatesoft.hg.util.Path; +import org.tmatesoft.hg.util.PathRewrite; + + +/** + * Produces path from strings and caches (weakly) result for reuse + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class PathPool implements Path.Source, Convertor { + private final WeakHashMap> cache; + private final PathRewrite pathRewrite; + + public PathPool(PathRewrite rewrite) { + pathRewrite = rewrite; + cache = new WeakHashMap>(); + } + + /* + * (non-Javadoc) + * @see org.tmatesoft.hg.util.Path.Source#path(java.lang.CharSequence) + */ + public Path path(CharSequence p) { + if (p instanceof Path) { + return asPath((Path) p); + } + p = pathRewrite.rewrite(p); + return get(p, true); + } + + /* + * (non-Javadoc) + * @see org.tmatesoft.hg.util.Convertor#mangle(java.lang.Object) + */ + public Path mangle(Path p) { + return asPath(p); + } + + // pipes path object through cache to reuse instance, if possible + // TODO unify with Pool + private Path asPath(Path p) { + CharSequence s = pathRewrite.rewrite(p.toString()); + // rewrite string, not path to avoid use of Path object as key + // in case pathRewrite does nothing and returns supplied instance + // + Path cached = get(s, false); + if (cached == null) { + cache.put(s, new SoftReference(cached = p)); + } + return cached; + } + + // XXX what would be parent of an empty path? + // Path shall have similar functionality + public Path parent(Path path) { + if (path.length() == 0) { + throw new IllegalArgumentException(); + } + for (int i = path.length() - 2 /*if path represents a dir, trailing char is slash, skip*/; i >= 0; i--) { + if (path.charAt(i) == '/') { + return get(path.subSequence(0, i+1).toString(), true); + } + } + return get("", true); + } + + // invoke when path pool is no longer in use, to ease gc work + public void clear() { + cache.clear(); + } + + private Path get(CharSequence p, boolean create) { + SoftReference sr = cache.get(p); + Path path = sr == null ? null : sr.get(); + if (path == null) { + if (create) { + path = Path.create(p); + cache.put(p, new SoftReference(path)); + } else if (sr != null) { + // cached path no longer used, clear cache entry - do not wait for RefQueue to step in + cache.remove(p); + } + } + return path; + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/PathScope.java --- a/src/org/tmatesoft/hg/internal/PathScope.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/PathScope.java Wed Jul 11 20:40:47 2012 +0200 @@ -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,24 +16,66 @@ */ package org.tmatesoft.hg.internal; +import static org.tmatesoft.hg.util.Path.CompareResult.*; + import java.util.ArrayList; +import org.tmatesoft.hg.util.FileIterator; import org.tmatesoft.hg.util.Path; +import org.tmatesoft.hg.util.Path.CompareResult; /** + *
    + *
  • Specify folder to get all files in there included, but no subdirs + *
  • Specify folder to get all files and files in subdirectories included + *
  • Specify exact set files (with option to accept or not paths leading to them) + *
* @author Artem Tikhomirov * @author TMate Software Ltd. */ public class PathScope implements Path.Matcher { private final Path[] files; private final Path[] dirs; - private final boolean recursiveDirs; + private final boolean includeNestedDirs; + private final boolean includeParentDirs; + private final boolean includeDirContent; + + /** + * See {@link PathScope#PathScope(boolean, boolean, Path...)} + */ + public PathScope(boolean recursiveDirs, Path... paths) { + this(true, recursiveDirs, true, paths); + } - public PathScope(boolean recursiveDirs, Path... paths) { + /** + * With matchParentDirs, recursiveDirs and matchDirContent set to false, + * this scope matches only exact paths specified. + *

+ * With matchParentDirs set to true, parent directories for files and folders listed in + * the paths would get accepted as well (handy for {@link FileIterator FileIterators}). + * Note, if supplied path lists a file, parent directory for the file is not matched unless matchParentDirs + * is true. To match file's immediate parent without matching all other parents up to the root, just add file parent + * along with the file to paths. + *

+ * With recursiveDirs set to true, subdirectories (with files) of directories listed in paths would + * be matched as well. Similar to `a/b/**` + *

+ * With matchDirContent set to true, files right under any directory listed in path would be matched. + * Similar to `a/b/*`. Makes little sense to set to false when recursiceDirs is true, although may still + * be useful in certain scenarios, e.g. PathScope(false, true, false, "a/") matches files under "a/b/*" and "a/b/c/*", but not files "a/*". + * + * @param matchParentDirs true to accept parent dirs of supplied paths + * @param recursiveDirs true to include subdirectories and files of supplied paths + * @param includeDirContent + * @param paths files and folders to match + */ + public PathScope(boolean matchParentDirs, boolean recursiveDirs, boolean matchDirContent, Path... paths) { if (paths == null) { throw new IllegalArgumentException(); } - this.recursiveDirs = recursiveDirs; + includeParentDirs = matchParentDirs; + includeNestedDirs = recursiveDirs; + includeDirContent = matchDirContent; ArrayList f = new ArrayList(5); ArrayList d = new ArrayList(5); for (Path p : paths) { @@ -49,36 +91,53 @@ public boolean accept(Path path) { if (path.isDirectory()) { - // either equals to or parent of a directory we know about. - // If recursiveDirs, accept also if nested to one of our directories. - // If one of configured files is nested under the path, accept. + // either equals to or a parent of a directory we know about (i.e. configured dir is *nested* in supplied arg). + // Also, accept arg if it happened to be nested into configured dir (i.e. one of them is *parent* for the arg), + // and recursiveDirs is true. for (Path d : dirs) { switch(d.compareWith(path)) { case Same : return true; - case Nested : return true; - case Parent : return recursiveDirs; + case ImmediateChild : + case Nested : return includeParentDirs; // path is parent to one of our locations + case ImmediateParent : + case Parent : return includeNestedDirs; // path is nested in one of our locations } } + if (!includeParentDirs) { + return false; + } + // If one of configured files is nested under the path, and we shall report parents, accept. + // Note, I don't respect includeDirContent here as with file it's easy to add parent to paths explicitly, if needed. + // (if includeDirContent == .f and includeParentDirs == .f, directory than owns a scope file won't get reported) for (Path f : files) { - if (f.compareWith(path) == Path.CompareResult.Nested) { + CompareResult cr = f.compareWith(path); + if (cr == Nested || cr == ImmediateChild) { return true; } } } else { - for (Path d : dirs) { - if (d.compareWith(path) == Path.CompareResult.Parent) { - return true; - } - } for (Path f : files) { if (f.equals(path)) { return true; } } - // either lives in a directory in out scope - // or there's a file that matches the path + // if interested in nested/recursive dirs, shall check if supplied file is under any of our configured locations + if (!includeNestedDirs && !includeDirContent) { + return false; + } + for (Path d : dirs) { + CompareResult cr = d.compareWith(path); + if (includeNestedDirs && cr == Parent) { + // file is nested in one of our locations + return true; + } + if (includeDirContent && cr == ImmediateParent) { + // file is right under one of our directories, and includeDirContents is .t + return true; + } + // try another directory + } } - // TODO Auto-generated method stub return false; } } \ No newline at end of file diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/PhasesHelper.java --- a/src/org/tmatesoft/hg/internal/PhasesHelper.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/PhasesHelper.java Wed Jul 11 20:40:47 2012 +0200 @@ -18,6 +18,8 @@ import static org.tmatesoft.hg.repo.HgPhase.Draft; import static org.tmatesoft.hg.repo.HgPhase.Secret; +import static org.tmatesoft.hg.util.LogFacility.Severity.Info; +import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; import java.io.BufferedReader; import java.io.File; @@ -29,10 +31,11 @@ import java.util.List; import org.tmatesoft.hg.core.HgChangeset; -import org.tmatesoft.hg.core.HgInvalidControlFileException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.repo.HgChangelog; import org.tmatesoft.hg.repo.HgInternals; +import org.tmatesoft.hg.repo.HgInvalidControlFileException; +import org.tmatesoft.hg.repo.HgParentChildMap; import org.tmatesoft.hg.repo.HgPhase; import org.tmatesoft.hg.repo.HgRepository; @@ -45,7 +48,7 @@ public final class PhasesHelper { private final HgRepository repo; - private final HgChangelog.ParentWalker parentHelper; + private final HgParentChildMap parentHelper; private Boolean repoSupporsPhases; private List draftPhaseRoots; private List secretPhaseRoots; @@ -55,7 +58,7 @@ this(hgRepo, null); } - public PhasesHelper(HgRepository hgRepo, HgChangelog.ParentWalker pw) { + public PhasesHelper(HgRepository hgRepo, HgParentChildMap pw) { repo = hgRepo; parentHelper = pw; } @@ -70,7 +73,7 @@ public HgPhase getPhase(HgChangeset cset) throws HgInvalidControlFileException { final Nodeid csetRev = cset.getNodeid(); - final int csetRevIndex = cset.getRevision(); + final int csetRevIndex = cset.getRevisionIndex(); return getPhase(csetRevIndex, csetRev); } @@ -127,13 +130,13 @@ continue; } if (lc.length != 2) { - HgInternals.getContext(repo).getLog().warn(getClass(), "Bad line in phaseroots:%s", line); + HgInternals.getContext(repo).getLog().dump(getClass(), Warn, "Bad line in phaseroots:%s", line); continue; } int phaseIndex = Integer.parseInt(lc[0]); Nodeid rootRev = Nodeid.fromAscii(lc[1]); if (!repo.getChangelog().isKnown(rootRev)) { - HgInternals.getContext(repo).getLog().warn(getClass(), "Phase(%d) root node %s doesn't exist in the repository, ignored.", phaseIndex, rootRev); + HgInternals.getContext(repo).getLog().dump(getClass(), Warn, "Phase(%d) root node %s doesn't exist in the repository, ignored.", phaseIndex, rootRev); continue; } HgPhase phase = HgPhase.parse(phaseIndex); @@ -152,7 +155,7 @@ try { br.close(); } catch (IOException ex) { - HgInternals.getContext(repo).getLog().info(getClass(), ex, null); + HgInternals.getContext(repo).getLog().dump(getClass(), Info, ex, null); // ignore the exception otherwise } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/Pool.java --- a/src/org/tmatesoft/hg/internal/Pool.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/Pool.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -18,13 +18,15 @@ import java.util.HashMap; +import org.tmatesoft.hg.util.Convertor; + /** * Instance pooling. * * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class Pool { +public class Pool implements Convertor { private final HashMap unify; public Pool() { @@ -38,7 +40,11 @@ unify = new HashMap(sizeHint * 4 / 3, 0.75f); } } - + + public T mangle(T t) { + return unify(t); + } + public T unify(T t) { T rv = unify.get(t); if (rv == null) { diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/Pool2.java --- a/src/org/tmatesoft/hg/internal/Pool2.java Thu Jun 21 21:36:06 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,72 +0,0 @@ -/* - * Copyright (c) 2011 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 org.tmatesoft.hg.util.DirectHashSet; - - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class Pool2 { - private final DirectHashSet unify = new DirectHashSet(); - - public Pool2() { - } - - public Pool2(int sizeHint) { - } - - public T unify(T t) { - T rv = unify.get(t); - if (rv == null) { - // first time we see a new value - unify.put(t); - rv = t; - } - return rv; - } - - public boolean contains(T t) { - return unify.get(t) != null; - } - - public void record(T t) { - unify.put(t); - } - - public void clear() { - unify.clear(); - } - - public int size() { - return unify.size(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(Pool2.class.getSimpleName()); - sb.append('@'); - sb.append(Integer.toString(System.identityHashCode(this))); - sb.append(' '); - sb.append(unify.toString()); - return sb.toString(); - } -} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/ProcessExecHelper.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/ProcessExecHelper.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,96 @@ +/* + * Copyright (c) 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 + * 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 java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.CharBuffer; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +/** + * Utility to run shell commands. Not thread-safe. + * Beware of memory overcommitment issue on Linux - suprocess get allocated virtual memory of parent process size + * @see http://developers.sun.com/solaris/articles/subprocess/subprocess.html + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class ProcessExecHelper { + private File dir; + private int exitValue; + private ProcessBuilder pb; + + public ProcessExecHelper() { + } + + protected List prepareCommand(List cmd) { + return cmd; + } + + public CharSequence exec(String... command) throws IOException, InterruptedException { + return exec(Arrays.asList(command)); + } + + public CharSequence exec(List command) throws IOException, InterruptedException { + List cmd = prepareCommand(command); + if (pb == null) { + pb = new ProcessBuilder(cmd).directory(dir).redirectErrorStream(true); + } else { + pb.command(cmd); // dir and redirect are set + } + Process p = pb.start(); + InputStreamReader stdOut = new InputStreamReader(p.getInputStream()); + LinkedList l = new LinkedList(); + int r = -1; + CharBuffer b = null; + do { + if (b == null || b.remaining() < b.capacity() / 3) { + b = CharBuffer.allocate(512); + l.add(b); + } + r = stdOut.read(b); + } while (r != -1); + int total = 0; + for (CharBuffer cb : l) { + total += cb.position(); + cb.flip(); + } + CharBuffer res = CharBuffer.allocate(total); + for (CharBuffer cb : l) { + res.put(cb); + } + res.flip(); + p.waitFor(); + exitValue = p.exitValue(); + return res; + } + + public int exitValue() { + return exitValue; + } + + public ProcessExecHelper cwd(File wd) { + dir = wd; + if (pb != null) { + pb.directory(dir); + } + return this; + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/PropertyMarshal.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/PropertyMarshal.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,48 @@ +/* + * Copyright (c) 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 + * 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 org.tmatesoft.hg.core.SessionContext; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class PropertyMarshal { + + private final SessionContext sessionContext; + + public PropertyMarshal(SessionContext ctx) { + sessionContext = ctx; + } + + public boolean getBoolean(String propertyName, boolean defaultValue) { + // can't use and unchecked cast because got no confidence passed properties are strictly of the kind of my default values, + // i.e. if boolean from outside comes as "true", while I pass default as Boolean or vice versa. + Object p = sessionContext.getConfigurationProperty(propertyName, defaultValue); + return p instanceof Boolean ? ((Boolean) p).booleanValue() : Boolean.parseBoolean(String.valueOf(p)); + } + + public int getInt(String propertyName, int defaultValue) { + Object v = sessionContext.getConfigurationProperty(propertyName, defaultValue); + if (false == v instanceof Number) { + v = Integer.parseInt(String.valueOf(v)); + } + return ((Number) v).intValue(); + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/RepositoryComparator.java --- a/src/org/tmatesoft/hg/internal/RepositoryComparator.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/RepositoryComparator.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -19,6 +19,7 @@ import static org.tmatesoft.hg.core.Nodeid.NULL; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -29,14 +30,15 @@ import java.util.Map.Entry; import java.util.Set; -import org.tmatesoft.hg.core.HgBadStateException; -import org.tmatesoft.hg.core.HgInvalidControlFileException; import org.tmatesoft.hg.core.HgRemoteConnectionException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.repo.HgChangelog; +import org.tmatesoft.hg.repo.HgInvalidControlFileException; +import org.tmatesoft.hg.repo.HgInvalidStateException; import org.tmatesoft.hg.repo.HgRemoteRepository; import org.tmatesoft.hg.repo.HgRemoteRepository.Range; import org.tmatesoft.hg.repo.HgRemoteRepository.RemoteBranch; +import org.tmatesoft.hg.repo.HgParentChildMap; import org.tmatesoft.hg.util.CancelSupport; import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.ProgressSupport; @@ -49,11 +51,11 @@ public class RepositoryComparator { private final boolean debug = Boolean.parseBoolean(System.getProperty("hg4j.remote.debug")); - private final HgChangelog.ParentWalker localRepo; + private final HgParentChildMap localRepo; private final HgRemoteRepository remoteRepo; private List common; - public RepositoryComparator(HgChangelog.ParentWalker pwLocal, HgRemoteRepository hgRemote) { + public RepositoryComparator(HgParentChildMap pwLocal, HgRemoteRepository hgRemote) { localRepo = pwLocal; remoteRepo = hgRemote; } @@ -65,7 +67,7 @@ // sanity check for (Nodeid n : common) { if (!localRepo.knownNode(n)) { - throw new HgBadStateException("Unknown node reported as common:" + n); + throw new HgInvalidStateException("Unknown node reported as common:" + n); } } progressSupport.done(); @@ -74,7 +76,7 @@ public List getCommon() { if (common == null) { - throw new HgBadStateException("Call #compare(Object) first"); + throw new HgInvalidStateException("Call #compare(Object) first"); } return common; } @@ -120,7 +122,7 @@ return; } if (earliestRevision < 0 || earliestRevision >= changelog.getLastRevision()) { - throw new HgBadStateException(String.format("Invalid index of common known revision: %d in total of %d", earliestRevision, 1+changelog.getLastRevision())); + throw new HgInvalidStateException(String.format("Invalid index of common known revision: %d in total of %d", earliestRevision, 1+changelog.getLastRevision())); } changelog.range(earliestRevision+1, changelog.getLastRevision(), inspector); } @@ -317,7 +319,7 @@ } } while(--watchdog > 0); if (watchdog == 0) { - throw new HgBadStateException(String.format("Can't narrow down branch [%s, %s]", rb.head.shortNotation(), rb.root.shortNotation())); + throw new HgInvalidStateException(String.format("Can't narrow down branch [%s, %s]", rb.head.shortNotation(), rb.root.shortNotation())); } } if (debug) { @@ -486,15 +488,16 @@ toQuery.clear(); } if (rootIndex == -1) { - throw new HgBadStateException("Shall not happen, provided between output is correct"); // FIXME EXCEPTIONS + throw new HgInvalidStateException("Shall not happen, provided between output is correct"); } result[rootIndex] = branchRoot; boolean resultOk = true; LinkedList fromRootToHead = new LinkedList(); + IntVector missing = new IntVector(); for (int i = 0; i <= rootIndex; i++) { Nodeid n = result[i]; if (n == null) { - System.out.printf("ERROR: element %d wasn't found\n",i); + missing.add(i); resultOk = false; } fromRootToHead.addFirst(n); // reverse order @@ -503,7 +506,9 @@ System.out.println("Total queries:" + totalQueries); } if (!resultOk) { - throw new HgBadStateException("See console for details"); // FIXME EXCEPTIONS + assert missing.size() > 0; + // TODO post-1.0 perhaps, there's better alternative than HgInvalidStateException, e.g. HgDataFormatException? + throw new HgInvalidStateException(String.format("Missing elements with indexes: %s", Arrays.toString(missing.toArray()))); } return fromRootToHead; } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/RevisionDescendants.java --- a/src/org/tmatesoft/hg/internal/RevisionDescendants.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/RevisionDescendants.java Wed Jul 11 20:40:47 2012 +0200 @@ -18,10 +18,10 @@ import java.util.BitSet; -import org.tmatesoft.hg.core.HgBadStateException; -import org.tmatesoft.hg.core.HgInvalidControlFileException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.repo.HgChangelog; +import org.tmatesoft.hg.repo.HgInvalidControlFileException; +import org.tmatesoft.hg.repo.HgInvalidStateException; import org.tmatesoft.hg.repo.HgRepository; /** @@ -57,7 +57,7 @@ if (rootRevIndex == tipRevIndex) { return; } - repo.getChangelog().walk(rootRevIndex+1, tipRevIndex, new HgChangelog.ParentInspector() { + repo.getChangelog().indexWalk(rootRevIndex+1, tipRevIndex, new HgChangelog.ParentInspector() { // TODO ParentRevisionInspector, with no parent nodeids, just indexes? private int i = 1; // above we start with revision next to rootRevIndex, which is at offset 0 @@ -74,11 +74,11 @@ p2IsDescendant = result.get(p2x); } // - int rx = revisionIndex -rootRevIndex; + int rx = revisionIndex - rootRevIndex; if (rx != i) { - throw new HgBadStateException(); + throw new HgInvalidStateException(String.format("Sanity check failed. Revision %d. Expected:%d, was:%d", revisionIndex, rx, i)); } - // current revision is descentand if any of its parents is descendant + // current revision is descendant if any of its parents is descendant result.set(rx, p1IsDescendant || p2IsDescendant); i++; } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/RevlogDump.java --- a/src/org/tmatesoft/hg/internal/RevlogDump.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/RevlogDump.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -76,7 +76,7 @@ while (dis.available() > 0) { long l = di.readLong(); long offset = entryIndex == 0 ? 0 : (l >>> 16); - int flags = (int) (l & 0X0FFFF); + int flags = (int) (l & 0x0FFFF); int compressedLen = di.readInt(); int actualLen = di.readInt(); int baseRevision = di.readInt(); diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/RevlogStream.java --- a/src/org/tmatesoft/hg/internal/RevlogStream.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/RevlogStream.java Wed Jul 11 20:40:47 2012 +0200 @@ -23,12 +23,11 @@ import java.io.IOException; import java.util.zip.Inflater; -import org.tmatesoft.hg.core.HgBadStateException; -import org.tmatesoft.hg.core.HgException; -import org.tmatesoft.hg.core.HgInvalidControlFileException; -import org.tmatesoft.hg.core.HgInvalidRevisionException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.repo.HgInternals; +import org.tmatesoft.hg.repo.HgInvalidControlFileException; +import org.tmatesoft.hg.repo.HgInvalidRevisionException; +import org.tmatesoft.hg.repo.HgInvalidStateException; import org.tmatesoft.hg.repo.HgRepository; @@ -56,6 +55,7 @@ private int[] baseRevisions; private boolean inline = false; private final File indexFile; + private File dataFile; private final DataAccessProvider dataAccess; // if we need anything else from HgRepo, might replace DAP parameter with HgRepo and query it for DAP. @@ -65,17 +65,40 @@ } /*package*/ DataAccess getIndexStream() { - // XXX may supply a hint that I'll need really few bytes of data (perhaps, at some offset) + // TODO post 1.0 may supply a hint that I'll need really few bytes of data (perhaps, at some offset) // to avoid mmap files when only few bytes are to be read (i.e. #dataLength()) return dataAccess.create(indexFile); } /*package*/ DataAccess getDataStream() { - final String indexName = indexFile.getName(); - File dataFile = new File(indexFile.getParentFile(), indexName.substring(0, indexName.length() - 1) + "d"); - return dataAccess.create(dataFile); + return dataAccess.create(getDataFile()); } + /** + * Constructs file object that corresponds to .d revlog counterpart. + * Note, it's caller responsibility to ensure this file makes any sense (i.e. check {@link #inline} attribute) + */ + private File getDataFile() { + if (dataFile == null) { + final String indexName = indexFile.getName(); + dataFile = new File(indexFile.getParentFile(), indexName.substring(0, indexName.length() - 1) + "d"); + } + return dataFile; + } + + // initialize exception with the file where revlog structure information comes from + public HgInvalidControlFileException initWithIndexFile(HgInvalidControlFileException ex) { + return ex.setFile(indexFile); + } + + // initialize exception with the file where revlog data comes from + public HgInvalidControlFileException initWithDataFile(HgInvalidControlFileException ex) { + // exceptions are usually raised after read attepmt, hence inline shall be initialized + // although honest approach is to call #initOutline() first + return ex.setFile(inline ? indexFile : getDataFile()); + } + + public int revisionCount() { initOutline(); return baseRevisions.length; @@ -96,7 +119,7 @@ int actualLen = daIndex.readInt(); return actualLen; } catch (IOException ex) { - throw new HgInvalidControlFileException(null, ex, indexFile); + throw new HgInvalidControlFileException(null, ex, indexFile).setRevisionIndex(revisionIndex); } finally { daIndex.done(); } @@ -118,7 +141,7 @@ daIndex.readBytes(rv, 0, 20); return rv; } catch (IOException ex) { - throw new HgInvalidControlFileException(null, ex, indexFile); + throw new HgInvalidControlFileException("Revision lookup failed", ex, indexFile).setRevisionIndex(revisionIndex); } finally { daIndex.done(); } @@ -139,7 +162,7 @@ int linkRev = daIndex.readInt(); return linkRev; } catch (IOException ex) { - throw new HgInvalidControlFileException(null, ex, indexFile); + throw new HgInvalidControlFileException("Linked revision lookup failed", ex, indexFile).setRevisionIndex(revisionIndex); } finally { daIndex.done(); } @@ -170,7 +193,7 @@ daIndex.skip(inline ? 12 + compressedLen : 12); } } catch (IOException ex) { - throw new HgInvalidControlFileException("Failed", ex, indexFile).setRevision(nodeid); + throw new HgInvalidControlFileException("Revision lookup failed", ex, indexFile).setRevision(nodeid); } finally { daIndex.done(); } @@ -182,7 +205,7 @@ // should be possible to use TIP, ALL, or -1, -2, -n notation of Hg // ? boolean needsNodeid - public void iterate(int start, int end, boolean needData, Inspector inspector) throws HgInvalidRevisionException, HgInvalidControlFileException /*REVISIT - too general exception*/ { + public void iterate(int start, int end, boolean needData, Inspector inspector) throws HgInvalidRevisionException, HgInvalidControlFileException { initOutline(); final int indexSize = revisionCount(); if (indexSize == 0) { @@ -205,8 +228,6 @@ throw new HgInvalidControlFileException(String.format("Failed reading [%d..%d]", start, end), ex, indexFile); } catch (HgInvalidControlFileException ex) { throw ex; - } catch (HgException ex) { - throw new HgInvalidControlFileException(String.format("Failed reading [%d..%d]", start, end), ex, indexFile); } finally { r.finish(); } @@ -253,10 +274,8 @@ final int c = sortedRevisions.length; throw new HgInvalidControlFileException(String.format("Failed reading %d revisions in [%d; %d]",c, sortedRevisions[0], sortedRevisions[c-1]), ex, indexFile); } catch (HgInvalidControlFileException ex) { + // TODO post-1.0 fill HgRuntimeException with appropriate file (either index or data, depending on error source) throw ex; - } catch (HgException ex) { - final int c = sortedRevisions.length; - throw new HgInvalidControlFileException(String.format("Failed reading %d revisions in [%d; %d]",c, sortedRevisions[0], sortedRevisions[c-1]), ex, indexFile); } finally { r.finish(); } @@ -286,7 +305,7 @@ return revisionIndex; } - private void initOutline() { + private void initOutline() throws HgInvalidControlFileException { if (baseRevisions != null && baseRevisions.length > 0) { return; } @@ -302,7 +321,7 @@ final int INLINEDATA = 1 << 16; inline = (versionField & INLINEDATA) != 0; IntVector resBases, resOffsets = null; - int entryCountGuess = da.length() / REVLOGV1_RECORD_SIZE; + int entryCountGuess = Internals.ltoi(da.longLength() / REVLOGV1_RECORD_SIZE); if (inline) { entryCountGuess >>>= 2; // pure guess, assume useful data takes 3/4 of total space resOffsets = new IntVector(entryCountGuess, 5000); @@ -323,11 +342,11 @@ // byte[] nodeid = new byte[32]; resBases.add(baseRevision); if (inline) { - int o = (int) offset; + int o = Internals.ltoi(offset); if (o != offset) { // just in case, can't happen, ever, unless HG (or some other bad tool) produces index file // with inlined data of size greater than 2 Gb. - throw new HgBadStateException("Data too big, offset didn't fit to sizeof(int)"); + throw new HgInvalidStateException("Data too big, offset didn't fit to sizeof(int)"); } resOffsets.add(o + REVLOGV1_RECORD_SIZE * resOffsets.size()); da.skip(3*4 + 32 + compressedLen); // Check: 44 (skip) + 20 (read) = 64 (total RevlogNG record size) @@ -348,9 +367,7 @@ } } } catch (IOException ex) { - ex.printStackTrace(); // FIXME, log error is not enough - // too bad, no outline then, but don't fail with NPE - baseRevisions = new int[0]; + throw new HgInvalidControlFileException("Failed to analyze revlog index", ex, indexFile); } finally { da.done(); } @@ -404,7 +421,7 @@ // System.out.printf("applyTime:%d ms, inspectorTime: %d ms\n", applyTime, inspectorTime); // TIMING } - public boolean range(int start, int end) throws IOException, HgException { + public boolean range(int start, int end) throws IOException { byte[] nodeidBuf = new byte[20]; int i; // it (i.e. replace with i >= start) @@ -441,7 +458,7 @@ long l = daIndex.readLong(); // 0 long offset = i == 0 ? 0 : (l >>> 16); @SuppressWarnings("unused") - int flags = (int) (l & 0X0FFFF); + int flags = (int) (l & 0x0FFFF); int compressedLen = daIndex.readInt(); // +8 int actualLen = daIndex.readInt(); // +12 int baseRevision = daIndex.readInt(); // +16 @@ -453,15 +470,15 @@ daIndex.skip(12); DataAccess userDataAccess = null; if (needData) { - int streamOffset; + long streamOffset; DataAccess streamDataAccess; if (inline) { streamDataAccess = daIndex; streamOffset = getIndexOffsetInt(i) + REVLOGV1_RECORD_SIZE; // don't need to do seek as it's actual position in the index stream } else { - streamOffset = (int) offset; + streamOffset = offset; streamDataAccess = daData; - daData.seek(streamOffset); + daData.longSeek(streamOffset); } final boolean patchToPrevious = baseRevision != i; // the only way I found to tell if it's a patch if (streamDataAccess.isEmpty() || compressedLen == 0) { @@ -541,6 +558,6 @@ // XXX boolean retVal to indicate whether to continue? // TODO specify nodeid and data length, and reuse policy (i.e. if revlog stream doesn't reuse nodeid[] for each call) // implementers shall not invoke DataAccess.done(), it's accomplished by #iterate at appropraite moment - void next(int revisionIndex, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[/*20*/] nodeid, DataAccess data) throws HgException; + void next(int revisionIndex, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[/*20*/] nodeid, DataAccess data); } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/StoragePathHelper.java --- a/src/org/tmatesoft/hg/internal/StoragePathHelper.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/StoragePathHelper.java Wed Jul 11 20:40:47 2012 +0200 @@ -56,14 +56,15 @@ fncache = isFncache; dotencode = isDotencode; suffix2replace = Pattern.compile("\\.([id]|hg)/"); - csEncoder = fsEncoding.newEncoder(); // FIXME catch exception and rethrow as our's RT + csEncoder = fsEncoding.newEncoder(); byteEncodingBuf = ByteBuffer.allocate(Math.round(csEncoder.maxBytesPerChar()) + 1/*in fact, need ceil, hence +1*/); charEncodingBuf = CharBuffer.allocate(1); } - // FIXME document what path argument is, whether it includes .i or .d, and whether it's 'normalized' (slashes) or not. - // since .hg/store keeps both .i files and files without extension (e.g. fncache), guees, for data == false - // we shall assume path has extension + /** + * path argument is repository-relative name of the user's file. + * It has to be normalized (slashes) and shall not include extension .i or .d. + */ public CharSequence rewrite(CharSequence p) { final String STR_STORE = "store/"; final String STR_DATA = "data/"; diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/StreamLogFacility.java --- a/src/org/tmatesoft/hg/internal/StreamLogFacility.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/StreamLogFacility.java Wed Jul 11 20:40:47 2012 +0200 @@ -16,6 +16,8 @@ */ package org.tmatesoft.hg.internal; +import static org.tmatesoft.hg.util.LogFacility.Severity.Info; + import java.io.PrintStream; import org.tmatesoft.hg.util.LogFacility; @@ -29,13 +31,14 @@ public class StreamLogFacility implements LogFacility { private final boolean isDebug; - private final boolean isInfo; + private final Severity severity; protected final boolean timestamp; protected final PrintStream outStream; - public StreamLogFacility(boolean pringDebug, boolean printInfo, boolean needTimestamp, PrintStream out) { - isDebug = pringDebug; - isInfo = printInfo; + public StreamLogFacility(Severity level, boolean needTimestamp, PrintStream out) { + assert level != null; + severity = level; + isDebug = level == Severity.Debug; timestamp = needTimestamp; outStream = out; } @@ -43,56 +46,24 @@ public boolean isDebug() { return isDebug; } - - public boolean isInfo() { - return isInfo; - } - - public void debug(Class src, String format, Object... args) { - if (!isDebug) { - return; - } - printf("DEBUG", src, format, args); - } - - public void info(Class src, String format, Object... args) { - if (!isInfo) { - return; - } - printf("INFO", src, format, args); - } - - public void warn(Class src, String format, Object... args) { - printf("WARN", src, format, args); + + public Severity getLevel() { + return severity; } - public void error(Class src, String format, Object... args) { - printf("ERROR", src, format, args); - } - - public void debug(Class src, Throwable th, String message) { - if (!isDebug) { - return; + public void dump(Class src, Severity severity, String format, Object... args) { + if (severity.ordinal() >= getLevel().ordinal()) { + printf(severity, src, format, args); } - printf("DEBUG", src, th, message); } - public void info(Class src, Throwable th, String message) { - if (!isInfo) { - return; + public void dump(Class src, Severity severity, Throwable th, String message) { + if (severity.ordinal() >= getLevel().ordinal()) { + printf(severity, src, th, message); } - printf("INFO", src, th, message); } - public void warn(Class src, Throwable th, String message) { - printf("WARN", src, th, message); - } - - public void error(Class src, Throwable th, String message) { - printf("ERROR", src, th, message); - } - - protected void printf(String level, Class src, String format, Object... args) { + protected void printf(Severity level, Class src, String format, Object... args) { String msg = String.format(format, args); if (timestamp) { outStream.printf(isDebug ? "%tT.%1$tL " : "%tT ", System.currentTimeMillis()); @@ -104,17 +75,17 @@ } outStream.printf("(%s) ", cn); } - outStream.printf("%s: %s", level, msg); + outStream.printf("%s: %s", level.toString().toUpperCase(), msg); if (format.length() == 0 || format.charAt(format.length() - 1) != '\n') { outStream.println(); } } - protected void printf(String level, Class src, Throwable th, String msg) { + protected void printf(Severity level, Class src, Throwable th, String msg) { if (msg != null || timestamp || isDebug || th == null) { printf(level, src, msg == null ? "" : msg, (Object[]) null); } if (th != null) { - if (isDebug || isInfo) { + if (getLevel().ordinal() <= Info.ordinal()) { // full stack trace th.printStackTrace(outStream); } else { @@ -126,6 +97,6 @@ // alternative to hardcore System.out where SessionContext is not available now (or ever) public static LogFacility newDefault() { - return new StreamLogFacility(true, true, true, System.out); + return new StreamLogFacility(Severity.Debug, true, System.out); } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/SubrepoManager.java --- a/src/org/tmatesoft/hg/internal/SubrepoManager.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/SubrepoManager.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -27,12 +27,17 @@ import java.util.List; import java.util.Map; -import org.tmatesoft.hg.core.HgInvalidControlFileException; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.HgInternals; +import org.tmatesoft.hg.repo.HgInvalidControlFileException; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.repo.HgSubrepoLocation; +import org.tmatesoft.hg.util.Path; /** * + * @see http://mercurial.selenic.com/wiki/SubrepoWork + * @see http://mercurial.selenic.com/wiki/Subrepository * @author Artem Tikhomirov * @author TMate Software Ltd. */ @@ -75,6 +80,7 @@ try { String line; LinkedList res = new LinkedList(); + HgInternals hgRepoInternal = new HgInternals(repo); while ((line = br.readLine()) != null) { int sep = line.indexOf('='); if (sep == -1) { @@ -84,7 +90,7 @@ // to have separate String instances (new String(line.substring())) String key = line.substring(0, sep).trim(); String value = line.substring(sep + 1).trim(); - if (value.length() == 0) { + if (key.length() == 0 || value.length() == 0) { // XXX log bad line? continue; } @@ -100,7 +106,12 @@ } } // TODO respect paths mappings in config file - HgSubrepoLocation loc = new HgSubrepoLocation(repo, key, value, kind, substate.get(key)); + // + // apparently, key value can't end with '/', `hg commit` fails if it does: + // abort: path ends in directory separator: fourth/ + Path p = Path.create(key.charAt(key.length()-1) == '/' ? key : key + '/'); + String revValue = substate.get(key); + HgSubrepoLocation loc = hgRepoInternal.newSubrepo(p, value, kind, revValue == null ? null : Nodeid.fromAscii(revValue)); res.add(loc); } return Arrays.asList(res.toArray(new HgSubrepoLocation[res.size()])); @@ -110,6 +121,7 @@ } private Map readState(BufferedReader br) throws IOException { + // TODO reuse for other files with format, like .hgtags HashMap rv = new HashMap(); try { String line; diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/WinToNixPathRewrite.java --- a/src/org/tmatesoft/hg/internal/WinToNixPathRewrite.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/WinToNixPathRewrite.java Wed Jul 11 20:40:47 2012 +0200 @@ -22,7 +22,7 @@ * Translate windows path separators to Unix/POSIX-style * * @author Artem Tikhomirov - * @author Tmate Software Ltd. + * @author TMate Software Ltd. */ public final class WinToNixPathRewrite implements PathRewrite { public CharSequence rewrite(CharSequence p) { diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/internal/package.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/package.html Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,5 @@ + + +Intimate implementation peculiarities of the library. Classes in this package shall be deemed internal API and not referenced from outside unless you really known what you do and what are possible consequences. + + \ No newline at end of file diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgBranches.java --- a/src/org/tmatesoft/hg/repo/HgBranches.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgBranches.java Wed Jul 11 20:40:47 2012 +0200 @@ -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.repo; +import static org.tmatesoft.hg.util.LogFacility.Severity.Error; +import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; + import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; @@ -34,9 +37,6 @@ import java.util.TreeMap; import java.util.regex.Pattern; -import org.tmatesoft.hg.core.HgException; -import org.tmatesoft.hg.core.HgInvalidControlFileException; -import org.tmatesoft.hg.core.HgInvalidRevisionException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.internal.Experimental; import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; @@ -101,24 +101,24 @@ return lastInCache; } catch (IOException ex) { // log error, but otherwise do nothing - repo.getContext().getLog().warn(getClass(), ex, null); + repo.getContext().getLog().dump(getClass(), Warn, ex, null); // FALL THROUGH to return -1 indicating no cache information } catch (NumberFormatException ex) { - repo.getContext().getLog().warn(getClass(), ex, null); + repo.getContext().getLog().dump(getClass(), Warn, ex, null); // FALL THROUGH } catch (HgInvalidControlFileException ex) { // shall not happen, thus log as error - repo.getContext().getLog().error(getClass(), ex, null); + repo.getContext().getLog().dump(getClass(), Error, ex, null); // FALL THROUGH } catch (HgInvalidRevisionException ex) { - repo.getContext().getLog().error(getClass(), ex, null); + repo.getContext().getLog().dump(getClass(), Error, ex, null); // FALL THROUGH } finally { if (br != null) { try { br.close(); } catch (IOException ex) { - repo.getContext().getLog().info(getClass(), ex, null); // ignore + repo.getContext().getLog().dump(getClass(), Warn, ex, null); // ignore } } } @@ -132,7 +132,7 @@ int lastCached = readCache(); isCacheActual = lastCached == repo.getChangelog().getLastRevision(); if (!isCacheActual) { - final HgChangelog.ParentWalker pw = repo.getChangelog().new ParentWalker(); + final HgParentChildMap pw = new HgParentChildMap(repo.getChangelog()); pw.init(); ps.worked(repo.getChangelog().getRevisionCount()); // first revision branch found at @@ -220,7 +220,7 @@ } } final HgChangelog clog = repo.getChangelog(); - final HgChangelog.RevisionMap rmap = clog.new RevisionMap().init(); + final HgRevisionMap rmap = new HgRevisionMap(clog).init(); for (BranchInfo bi : branches.values()) { bi.validate(clog, rmap); } @@ -240,10 +240,10 @@ * Writes down information about repository branches in a format Mercurial native client can understand. * Cache file gets overwritten only if it is out of date (i.e. misses some branch information) * @throws IOException if write to cache file failed - * @throws HgException subclass of {@link HgException} in case of repository access issue + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ @Experimental(reason="Usage of cache isn't supposed to be public knowledge") - public void writeCache() throws IOException, HgException { + public void writeCache() throws IOException, HgRuntimeException { if (isCacheActual) { return; } @@ -299,7 +299,7 @@ this(branchName, Nodeid.NULL, branchHeads); } - void validate(HgChangelog clog, HgChangelog.RevisionMap rmap) throws HgInvalidControlFileException { + void validate(HgChangelog clog, HgRevisionMap rmap) throws HgInvalidControlFileException { int[] localCset = new int[heads.size()]; int i = 0; for (Nodeid h : heads) { diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgBundle.java --- a/src/org/tmatesoft/hg/repo/HgBundle.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgBundle.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -19,9 +19,6 @@ import java.io.File; import java.io.IOException; -import org.tmatesoft.hg.core.HgBadStateException; -import org.tmatesoft.hg.core.HgCallbackTargetException; -import org.tmatesoft.hg.core.HgInvalidFileException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.core.SessionContext; import org.tmatesoft.hg.internal.ByteArrayChannel; @@ -36,11 +33,14 @@ import org.tmatesoft.hg.util.CancelledException; /** + * WORK IN PROGRESS + * * @see http://mercurial.selenic.com/wiki/BundleFormat * * @author Artem Tikhomirov * @author TMate Software Ltd. */ +@Experimental(reason="API is not stable") public class HgBundle { private final File bundleFile; @@ -66,7 +66,7 @@ throw HgRepository.notImplemented(); } if (signature[4] != 'U' || signature[5] != 'N') { - throw new HgBadStateException("Bad bundle signature:" + new String(signature)); + throw new HgInvalidStateException(String.format("Bad bundle signature: %s", String.valueOf(signature))); } // "...UN", fall-through } else { @@ -96,7 +96,7 @@ * @param hgRepo repository that shall possess base revision for this bundle * @param inspector callback to get each changeset found */ - public void changes(final HgRepository hgRepo, final HgChangelog.Inspector inspector) throws HgCallbackTargetException, HgInvalidFileException { + public void changes(final HgRepository hgRepo, final HgChangelog.Inspector inspector) throws HgRuntimeException { Inspector bundleInsp = new Inspector() { DigestHelper dh = new DigestHelper(); boolean emptyChangelog = true; @@ -150,7 +150,7 @@ throw new IllegalStateException(String.format("Revision %s needs a parent %s, which is missing in the supplied repo %s", ge.node().shortNotation(), base.shortNotation(), hgRepo.toString())); } ByteArrayChannel bac = new ByteArrayChannel(); - changelog.rawContent(base, bac); // FIXME get DataAccess directly, to avoid + changelog.rawContent(base, bac); // TODO post-1.0 get DataAccess directly, to avoid // extra byte[] (inside ByteArrayChannel) duplication just for the sake of subsequent ByteArrayDataChannel wrap. prevRevContent = new ByteArrayDataAccess(bac.toArray()); } @@ -159,7 +159,7 @@ byte[] csetContent = ge.apply(prevRevContent); dh = dh.sha1(ge.firstParent(), ge.secondParent(), csetContent); // XXX ge may give me access to byte[] content of nodeid directly, perhaps, I don't need DH to be friend of Nodeid? if (!ge.node().equalsTo(dh.asBinary())) { - throw new IllegalStateException("Integrity check failed on " + bundleFile + ", node:" + ge.node()); + throw new HgInvalidStateException(String.format("Integrity check failed on %s, node: %s", bundleFile, ge.node().shortNotation())); } ByteArrayDataAccess csetDataAccess = new ByteArrayDataAccess(csetContent); RawChangeset cs = RawChangeset.parse(csetDataAccess); @@ -168,8 +168,10 @@ prevRevContent = csetDataAccess.reset(); } catch (CancelledException ex) { return false; - } catch (Exception ex) { - throw new HgBadStateException(ex); // FIXME EXCEPTIONS + } catch (IOException ex) { + throw new HgInvalidFileException("Invalid bundle file", ex, bundleFile); // TODO post-1.0 revisit exception handling + } catch (HgInvalidDataFormatException ex) { + throw new HgInvalidControlFileException("Invalid bundle file", ex, bundleFile); } return true; } @@ -180,11 +182,7 @@ public void fileEnd(String name) {} }; - try { - inspectChangelog(bundleInsp); - } catch (RuntimeException ex) { - throw new HgCallbackTargetException(ex); - } + inspectChangelog(bundleInsp); } // callback to minimize amount of Strings and Nodeids instantiated @@ -209,7 +207,12 @@ boolean element(GroupElement element); } - public void inspectChangelog(Inspector inspector) throws HgInvalidFileException { + /** + * @param inspector callback to visit changelog entries + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + * @throws IllegalArgumentException if inspector argument is null + */ + public void inspectChangelog(Inspector inspector) throws HgRuntimeException { if (inspector == null) { throw new IllegalArgumentException(); } @@ -226,7 +229,12 @@ } } - public void inspectManifest(Inspector inspector) throws HgInvalidFileException { + /** + * @param inspector callback to visit manifest entries + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + * @throws IllegalArgumentException if inspector argument is null + */ + public void inspectManifest(Inspector inspector) throws HgRuntimeException { if (inspector == null) { throw new IllegalArgumentException(); } @@ -247,7 +255,12 @@ } } - public void inspectFiles(Inspector inspector) throws HgInvalidFileException { + /** + * @param inspector callback to visit file entries + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + * @throws IllegalArgumentException if inspector argument is null + */ + public void inspectFiles(Inspector inspector) throws HgRuntimeException { if (inspector == null) { throw new IllegalArgumentException(); } @@ -272,7 +285,12 @@ } } - public void inspectAll(Inspector inspector) throws HgInvalidFileException { + /** + * @param inspector visit complete bundle (changelog, manifest and file entries) + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + * @throws IllegalArgumentException if inspector argument is null + */ + public void inspectAll(Inspector inspector) throws HgRuntimeException { if (inspector == null) { throw new IllegalArgumentException(); } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgChangelog.java --- a/src/org/tmatesoft/hg/repo/HgChangelog.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgChangelog.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -21,7 +21,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; -import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Formatter; @@ -31,18 +30,14 @@ import java.util.Map; import java.util.TimeZone; -import org.tmatesoft.hg.core.HgBadArgumentException; -import org.tmatesoft.hg.core.HgException; -import org.tmatesoft.hg.core.HgInvalidControlFileException; -import org.tmatesoft.hg.core.HgInvalidRevisionException; import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.Callback; import org.tmatesoft.hg.internal.DataAccess; import org.tmatesoft.hg.internal.IterateControlMediator; import org.tmatesoft.hg.internal.Lifecycle; import org.tmatesoft.hg.internal.Pool; import org.tmatesoft.hg.internal.RevlogStream; import org.tmatesoft.hg.util.CancelSupport; -import org.tmatesoft.hg.util.Pair; import org.tmatesoft.hg.util.ProgressSupport; /** @@ -51,7 +46,7 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class HgChangelog extends Revlog { +public final class HgChangelog extends Revlog { /* package-local */HgChangelog(HgRepository hgRepo, RevlogStream content) { super(hgRepo, content); @@ -107,25 +102,17 @@ return range(x, x).get(0); } + @Callback public interface Inspector { - // TODO describe whether cset is new instance each time - // describe what revisionNumber is when Inspector is used with HgBundle (BAD_REVISION or bundle's local order?) - void next(int revisionNumber, Nodeid nodeid, RawChangeset cset); - } - - /** - * Unlike regular {@link Inspector}, this one supplies changeset revision along with its parents and children according - * to parent information of the revlog this inspector visits. - * @see HgDataFile#history(TreeInspector) - * @deprecated use {@link HgChangesetTreeHandler} and HgLogCommand#execute(HgChangesetTreeHandler)} - */ - @Deprecated - public interface TreeInspector { - // the reason TreeInsector is in HgChangelog, not in Revlog, because despite the fact it can - // be applied to any revlog, it's not meant to provide revisions of any revlog it's beeing applied to, - // but changeset revisions always. - // TODO HgChangelog.walk(TreeInspector) - void next(Nodeid changesetRevision, Pair parentChangesets, Collection childChangesets); + /** + * Access next changeset + * TODO describe what revisionNumber is when Inspector is used with HgBundle (BAD_REVISION or bundle's local order?) + * + * @param revisionIndex index of revision being inspected, local to the inspected object + * @param nodeid revision being inspected + * @param cset changeset raw data + */ + void next(int revisionIndex, Nodeid nodeid, RawChangeset cset); } /** @@ -239,7 +226,7 @@ } } - /*package*/ static RawChangeset parse(DataAccess da) throws IOException, HgBadArgumentException { + /*package*/ static RawChangeset parse(DataAccess da) throws IOException, HgInvalidDataFormatException { byte[] data = da.byteArray(); RawChangeset rv = new RawChangeset(); rv.init(data, 0, data.length, null); @@ -247,18 +234,17 @@ } // @param usersPool - it's likely user names get repeated again and again throughout repository. can be null - // FIXME replace HgBadArgumentException with HgInvalidDataFormatException or HgInvalidControlFileException - /* package-local */void init(byte[] data, int offset, int length, Pool usersPool) throws HgBadArgumentException { + /* package-local */void init(byte[] data, int offset, int length, Pool usersPool) throws HgInvalidDataFormatException { final int bufferEndIndex = offset + length; final byte lineBreak = (byte) '\n'; int breakIndex1 = indexOf(data, lineBreak, offset, bufferEndIndex); if (breakIndex1 == -1) { - throw new HgBadArgumentException("Bad Changeset data", null); + throw new HgInvalidDataFormatException("Bad Changeset data"); } Nodeid _nodeid = Nodeid.fromAscii(data, 0, breakIndex1); int breakIndex2 = indexOf(data, lineBreak, breakIndex1 + 1, bufferEndIndex); if (breakIndex2 == -1) { - throw new HgBadArgumentException("Bad Changeset data", null); + throw new HgInvalidDataFormatException("Bad Changeset data"); } String _user = new String(data, breakIndex1 + 1, breakIndex2 - breakIndex1 - 1); if (usersPool != null) { @@ -266,12 +252,12 @@ } int breakIndex3 = indexOf(data, lineBreak, breakIndex2 + 1, bufferEndIndex); if (breakIndex3 == -1) { - throw new HgBadArgumentException("Bad Changeset data", null); + throw new HgInvalidDataFormatException("Bad Changeset data"); } String _timeString = new String(data, breakIndex2 + 1, breakIndex3 - breakIndex2 - 1); int space1 = _timeString.indexOf(' '); if (space1 == -1) { - throw new HgBadArgumentException(String.format("Bad Changeset data: %s in [%d..%d]", "time string", breakIndex2+1, breakIndex3), null); + throw new HgInvalidDataFormatException(String.format("Bad Changeset data: %s in [%d..%d]", "time string", breakIndex2+1, breakIndex3)); } int space2 = _timeString.indexOf(' ', space1 + 1); if (space2 == -1) { @@ -317,7 +303,7 @@ } } if (breakIndex4 == -1 || breakIndex4 >= bufferEndIndex) { - throw new HgBadArgumentException("Bad Changeset data", null); + throw new HgInvalidDataFormatException("Bad Changeset data"); } } else { breakIndex4--; @@ -325,11 +311,11 @@ String _comment; try { _comment = new String(data, breakIndex4 + 2, bufferEndIndex - breakIndex4 - 2, "UTF-8"); - // FIXME respect ui.fallbackencoding and try to decode if set + // TODO post-1.0 respect ui.fallbackencoding and try to decode if set; use EncodingHelper } catch (UnsupportedEncodingException ex) { _comment = ""; // Could hardly happen - throw new HgBadArgumentException("Bad Changeset data", ex); + throw new HgInvalidDataFormatException("Bad Changeset data", ex); } // change this instance at once, don't leave it partially changes in case of error this.manifest = _nodeid; @@ -386,15 +372,18 @@ progressHelper = ProgressSupport.Factory.get(delegate); } - public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) throws HgException { + public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) { try { byte[] data = da.byteArray(); cset.init(data, 0, data.length, usersPool); // XXX there's no guarantee for Changeset.Callback that distinct instance comes each time, consider instance reuse inspector.next(revisionNumber, Nodeid.fromBinary(nodeid, 0), cset); progressHelper.worked(1); + } catch (HgInvalidDataFormatException ex) { + throw ex.setRevisionIndex(revisionNumber); } catch (IOException ex) { - throw new HgException(ex); // XXX need better exception, perhaps smth like HgChangelogException (extends HgInvalidControlFileException) + // XXX need better exception, perhaps smth like HgChangelogException (extends HgInvalidControlFileException) + throw new HgInvalidControlFileException("Failed reading changelog", ex, null).setRevisionIndex(revisionNumber); } if (iterateControl != null) { iterateControl.checkCancelled(); diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgDataFile.java --- a/src/org/tmatesoft/hg/repo/HgDataFile.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgDataFile.java Wed Jul 11 20:40:47 2012 +0200 @@ -18,6 +18,7 @@ import static org.tmatesoft.hg.repo.HgInternals.wrongRevisionIndex; import static org.tmatesoft.hg.repo.HgRepository.*; +import static org.tmatesoft.hg.util.LogFacility.Severity.*; import java.io.ByteArrayOutputStream; import java.io.File; @@ -28,19 +29,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; -import java.util.Collections; -import java.util.List; -import org.tmatesoft.hg.core.HgDataStreamException; -import org.tmatesoft.hg.core.HgException; -import org.tmatesoft.hg.core.HgInvalidControlFileException; -import org.tmatesoft.hg.core.HgInvalidRevisionException; -import org.tmatesoft.hg.core.HgLogCommand; +import org.tmatesoft.hg.core.HgChangesetFileSneaker; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.internal.DataAccess; import org.tmatesoft.hg.internal.FilterByteChannel; import org.tmatesoft.hg.internal.FilterDataAccess; import org.tmatesoft.hg.internal.IntMap; +import org.tmatesoft.hg.internal.Internals; import org.tmatesoft.hg.internal.RevlogStream; import org.tmatesoft.hg.util.ByteChannel; import org.tmatesoft.hg.util.CancelSupport; @@ -53,12 +49,17 @@ /** - * ? name:HgFileNode? + * Regular user data file stored in the repository. + * + *

Note, most methods accept index in the file's revision history, not that of changelog. Easy way to obtain + * changeset revision index from file's is to use {@link #getChangesetRevisionIndex(int)}. To obtain file's revision + * index for a given changeset, {@link HgManifest#getFileRevision(int, Path)} or {@link HgChangesetFileSneaker} may + * come handy. * * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class HgDataFile extends Revlog { +public final class HgDataFile extends Revlog { // absolute from repo root? // slashes, unix-style? @@ -77,39 +78,60 @@ } // exists is not the best name possible. now it means no file with such name was ever known to the repo. - // it might be confused with files existed before but lately removed. + // it might be confused with files existed before but lately removed. TODO HgFileNode.exists makes more sense. + // or HgDataFile.known() public boolean exists() { return content != null; // XXX need better impl } - // human-readable (i.e. "COPYING", not "store/data/_c_o_p_y_i_n_g.i") + /** + * Human-readable file name, i.e. "COPYING", not "store/data/_c_o_p_y_i_n_g.i" + */ public Path getPath() { return path; // hgRepo.backresolve(this) -> name? In this case, what about hashed long names? } /** - * Handy shorthand for {@link #length(int) length(getRevisionIndex(nodeid))} + * Handy shorthand for {@link #getLength(int) length(getRevisionIndex(nodeid))} * * @param nodeid revision of the file * * @return size of the file content at the given revision - * @throws HgInvalidRevisionException if supplied nodeid doesn't identify any revision from this revlog - * @throws HgDataStreamException if attempt to access file metadata failed - * @throws HgInvalidControlFileException if access to revlog index/data entry failed + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public int length(Nodeid nodeid) throws HgDataStreamException, HgInvalidControlFileException, HgInvalidRevisionException { - return length(getRevisionIndex(nodeid)); + public int getLength(Nodeid nodeid) throws HgRuntimeException { + try { + return getLength(getRevisionIndex(nodeid)); + } catch (HgInvalidControlFileException ex) { + throw ex.isRevisionSet() ? ex : ex.setRevision(nodeid); + } catch (HgInvalidRevisionException ex) { + throw ex.isRevisionSet() ? ex : ex.setRevision(nodeid); + } } /** * @param fileRevisionIndex - revision local index, non-negative. From predefined constants, only {@link HgRepository#TIP} makes sense. * @return size of the file content at the revision identified by local revision number. - * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog - * @throws HgDataStreamException if attempt to access file metadata failed - * @throws HgInvalidControlFileException if access to revlog index/data entry failed + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public int length(int fileRevisionIndex) throws HgDataStreamException, HgInvalidControlFileException, HgInvalidRevisionException { - // TODO support WORKING_COPY constant + public int getLength(int fileRevisionIndex) throws HgRuntimeException { + if (wrongRevisionIndex(fileRevisionIndex) || fileRevisionIndex == BAD_REVISION) { + throw new HgInvalidRevisionException(fileRevisionIndex); + } + if (fileRevisionIndex == TIP) { + fileRevisionIndex = getLastRevision(); + } else if (fileRevisionIndex == WORKING_COPY) { + File f = getRepo().getFile(this); + if (f.exists()) { + // single revision can't be greater than 2^32, shall be safe to cast to int + return Internals.ltoi(f.length()); + } + Nodeid fileRev = getWorkingCopyRevision(); + if (fileRev == null) { + throw new HgInvalidRevisionException(String.format("File %s is not part of working copy", getPath()), null, fileRevisionIndex); + } + fileRevisionIndex = getRevisionIndex(fileRev); + } if (metadata == null || !metadata.checked(fileRevisionIndex)) { checkAndRecordMetadata(fileRevisionIndex); } @@ -119,18 +141,21 @@ } return dataLen; } - + /** * Reads content of the file from working directory. If file present in the working directory, its actual content without * any filters is supplied through the sink. If file does not exist in the working dir, this method provides content of a file - * as if it would be refreshed in the working copy, i.e. its corresponding revision - * (XXX according to dirstate? file tip?) is read from the repository, and filters repo -> working copy get applied. + * as if it would be refreshed in the working copy, i.e. its corresponding revision (according to dirstate) is read from the + * repository, and filters repo -> working copy get applied. + * + * NOTE, if file is missing from the working directory and is not part of the dirstate (but otherwise legal repository file, + * e.g. from another branch), no content would be supplied. * - * @param sink where to pipe content to - * @throws HgDataStreamException to indicate troubles reading repository file + * @param sink content consumer * @throws CancelledException if execution of the operation was cancelled + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public void workingCopy(ByteChannel sink) throws HgDataStreamException, HgInvalidControlFileException, CancelledException { + public void workingCopy(ByteChannel sink) throws CancelledException, HgRuntimeException { File f = getRepo().getFile(this); if (f.exists()) { final CancelSupport cs = CancelSupport.Factory.get(sink); @@ -150,83 +175,93 @@ buf.compact(); } } catch (IOException ex) { - throw new HgDataStreamException(getPath(), ex); + throw new HgInvalidFileException("Working copy read failed", ex, f); } finally { progress.done(); if (fc != null) { try { fc.close(); } catch (IOException ex) { - getRepo().getContext().getLog().info(getClass(), ex, null); + getRepo().getContext().getLog().dump(getClass(), Warn, ex, null); } } } } else { - final Pair wcParents = getRepo().getWorkingCopyParents(); - Nodeid p = wcParents.first().isNull() ? wcParents.second() : wcParents.first(); - if (p.isNull()) { - // no dirstate parents - no content + Nodeid fileRev = getWorkingCopyRevision(); + if (fileRev == null) { + // no content for this data file in the working copy - it is not part of the actual working state. + // XXX perhaps, shall report this to caller somehow, not silently pass no data? return; } - final HgChangelog clog = getRepo().getChangelog(); + final int fileRevIndex = getRevisionIndex(fileRev); + contentWithFilters(fileRevIndex, sink); + } + } + + /** + * @return file revision as recorded in repository manifest for dirstate parent, or null if no file revision can be found + */ + private Nodeid getWorkingCopyRevision() throws HgInvalidControlFileException { + final Pair wcParents = getRepo().getWorkingCopyParents(); + Nodeid p = wcParents.first().isNull() ? wcParents.second() : wcParents.first(); + final HgChangelog clog = getRepo().getChangelog(); + final int csetRevIndex; + if (p.isNull()) { + // no dirstate parents + getRepo().getContext().getLog().dump(getClass(), Info, "No dirstate parents, resort to TIP", getPath()); + // if it's a repository with no dirstate, use TIP then + csetRevIndex = clog.getLastRevision(); + if (csetRevIndex == -1) { + // shall not happen provided there's .i for this data file (hence at least one cset) + // and perhaps exception is better here. However, null as "can't find" indication seems reasonable. + return null; + } + } else { // 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); } + Nodeid fileRev = getRepo().getManifest().getFileRevision(csetRevIndex, getPath()); + // it's possible for a file to be in working dir and have store/.i but to belong e.g. to a different + // branch than the one from dirstate. Thus it's possible to get null fileRev + // which would serve as an indication this data file is not part of working copy + return fileRev; } -// public void content(int revision, ByteChannel sink, boolean applyFilters) throws HgDataStreamException, IOException, CancelledException { -// byte[] content = content(revision); -// final CancelSupport cancelSupport = CancelSupport.Factory.get(sink); -// final ProgressSupport progressSupport = ProgressSupport.Factory.get(sink); -// ByteBuffer buf = ByteBuffer.allocate(512); -// int left = content.length; -// progressSupport.start(left); -// int offset = 0; -// cancelSupport.checkCancelled(); -// ByteChannel _sink = applyFilters ? new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath())) : sink; -// do { -// buf.put(content, offset, Math.min(left, buf.remaining())); -// buf.flip(); -// cancelSupport.checkCancelled(); -// // XXX I may not rely on returned number of bytes but track change in buf position instead. -// int consumed = _sink.write(buf); -// buf.compact(); -// offset += consumed; -// left -= consumed; -// progressSupport.worked(consumed); -// } while (left > 0); -// progressSupport.done(); // XXX shall specify whether #done() is invoked always or only if completed successfully. -// } - - /*XXX not sure distinct method contentWithFilters() is the best way to do, perhaps, callers shall add filters themselves?*/ - public void contentWithFilters(int revision, ByteChannel sink) throws HgDataStreamException, HgInvalidControlFileException, CancelledException, HgInvalidRevisionException { - if (revision == WORKING_COPY) { + /** + * Access content of a file revision + * XXX not sure distinct method contentWithFilters() is the best way to do, perhaps, callers shall add filters themselves? + * + * @param fileRevisionIndex - revision local index, non-negative. From predefined constants, {@link HgRepository#TIP} and {@link HgRepository#WORKING_COPY} make sense. + * @param sink content consumer + * + * @throws CancelledException if execution of the operation was cancelled + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public void contentWithFilters(int fileRevisionIndex, ByteChannel sink) throws CancelledException, HgRuntimeException { + if (fileRevisionIndex == WORKING_COPY) { workingCopy(sink); // pass un-mangled sink } else { - content(revision, new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath()))); + content(fileRevisionIndex, new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath()))); } } /** + * Retrieve content of specific revision. Content is provided as is, without any filters (e.g. keywords, eol, etc.) applied. + * For filtered content, use {@link #contentWithFilters(int, ByteChannel)}. * * @param fileRevisionIndex - revision local index, non-negative. From predefined constants, {@link HgRepository#TIP} and {@link HgRepository#WORKING_COPY} make sense. - * @param sink - * @throws HgDataStreamException FIXME EXCEPTIONS - * @throws HgInvalidControlFileException if access to revlog index/data entry failed + * @param sink content consumer + * * @throws CancelledException if execution of the operation was cancelled - * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public void content(int fileRevisionIndex, ByteChannel sink) throws HgDataStreamException, HgInvalidControlFileException, CancelledException, HgInvalidRevisionException { + public void content(int fileRevisionIndex, ByteChannel sink) throws CancelledException, HgRuntimeException { // for data files need to check heading of the file content for possible metadata // @see http://mercurial.selenic.com/wiki/FileFormats#data.2BAC8- if (fileRevisionIndex == TIP) { @@ -255,144 +290,38 @@ insp = new ContentPipe(sink, metadata.dataOffset(fileRevisionIndex), lf); } else { // do not know if there's metadata - insp = new MetadataInspector(metadata, lf, getPath(), new ContentPipe(sink, 0, lf)); + insp = new MetadataInspector(metadata, lf, new ContentPipe(sink, 0, lf)); } insp.checkCancelled(); super.content.iterate(fileRevisionIndex, fileRevisionIndex, true, insp); try { - insp.checkFailed(); // XXX is there real need to throw IOException from ContentPipe? - } catch (HgDataStreamException ex) { - throw ex; + insp.checkFailed(); + } catch (HgInvalidControlFileException ex) { + ex = ex.setFileName(getPath()); + throw ex.isRevisionIndexSet() ? ex : ex.setRevisionIndex(fileRevisionIndex); } catch (IOException ex) { - throw new HgDataStreamException(getPath(), ex).setRevisionIndex(fileRevisionIndex); - } catch (HgException ex) { - // shall not happen, unless we changed ContentPipe or its subclass - throw new HgDataStreamException(getPath(), ex.getClass().getName(), ex); - } - } - - private static class HistoryNode { - int changeset; - Nodeid cset; - HistoryNode parent1, parent2; - List children; - - HistoryNode(int cs, HistoryNode p1, HistoryNode p2) { - changeset = cs; - parent1 = p1; - parent2 = p2; - if (p1 != null) { - p1.addChild(this); - } - if (p2 != null) { - p2.addChild(this); - } - } - - Nodeid changesetRevision() { - assert cset != null : "we initialize all csets prior to use"; - return cset; - } - - void addChild(HistoryNode child) { - if (children == null) { - children = new ArrayList(2); - } - children.add(child); + HgInvalidControlFileException e = new HgInvalidControlFileException("Revision content access failed", ex, null); + throw content.initWithIndexFile(e).setFileName(getPath()).setRevisionIndex(fileRevisionIndex); } } - + /** - * @deprecated use {@link HgLogCommand#execute(org.tmatesoft.hg.core.HgChangesetTreeHandler)} instead + * Walk complete change history of the file. + * @param inspector callback to visit changesets + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - @Deprecated - public void history(HgChangelog.TreeInspector inspector) throws HgInvalidControlFileException{ - final CancelSupport cancelSupport = CancelSupport.Factory.get(inspector); - try { - final boolean[] needsSorting = { false }; - final HistoryNode[] completeHistory = new HistoryNode[getRevisionCount()]; - final int[] commitRevisions = new int[completeHistory.length]; - RevlogStream.Inspector insp = new RevlogStream.Inspector() { - public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) { - if (revisionNumber > 0) { - if (commitRevisions[revisionNumber-1] > linkRevision) { - needsSorting[0] = true; - } - } - commitRevisions[revisionNumber] = linkRevision; - HistoryNode p1 = null, p2 = null; - if (parent1Revision != -1) { - p1 = completeHistory[parent1Revision]; - } - if (parent2Revision != -1) { - p2 = completeHistory[parent2Revision]; - } - completeHistory[revisionNumber] = new HistoryNode(linkRevision, p1, p2); - } - }; - content.iterate(0, getLastRevision(), false, insp); - cancelSupport.checkCancelled(); - if (needsSorting[0]) { - Arrays.sort(commitRevisions); - } - // read changeset revisions at once (to avoid numerous changelog.getRevision reads) - // but just nodeids, not RawChangeset (changelog.iterate(data=false) - ArrayList changesetRevisions = new ArrayList(commitRevisions.length); - getRepo().getChangelog().getRevisionsInternal(changesetRevisions, commitRevisions); - cancelSupport.checkCancelled(); - // assign them to corresponding HistoryNodes - for (int i = 0; i < completeHistory.length; i++ ) { - final HistoryNode n = completeHistory[i]; - if (needsSorting[0]) { - int x = Arrays.binarySearch(commitRevisions, n.changeset); - assert x >= 0; - n.cset = changesetRevisions.get(x); - } else { - // commit revisions were not sorted, may use original index directly - n.cset = changesetRevisions.get(i); - } - } - cancelSupport.checkCancelled(); - // XXX shall sort completeHistory according to changeset numbers? - for (int i = 0; i < completeHistory.length; i++ ) { - final HistoryNode n = completeHistory[i]; - HistoryNode p; - Nodeid p1, p2; - if ((p = n.parent1) != null) { - p1 = p.changesetRevision(); - } else { - p1 = Nodeid.NULL; - } - if ((p= n.parent2) != null) { - p2 = p.changesetRevision(); - } else { - p2 = Nodeid.NULL; - } - final Pair parentChangesets = new Pair(p1, p2); - final List childChangesets; - if (n.children == null) { - childChangesets = Collections.emptyList(); - } else { - Nodeid[] revisions = new Nodeid[n.children.size()]; - int j = 0; - for (HistoryNode hn : n.children) { - revisions[j++] = hn.changesetRevision(); - } - childChangesets = Arrays.asList(revisions); - } - inspector.next(n.changesetRevision(), parentChangesets, childChangesets); - cancelSupport.checkCancelled(); - } - } catch (CancelledException ex) { - return; - } - } - - public void history(HgChangelog.Inspector inspector) throws HgInvalidControlFileException { + public void history(HgChangelog.Inspector inspector) throws HgRuntimeException { history(0, getLastRevision(), inspector); } - public void history(int start, int end, HgChangelog.Inspector inspector) throws HgInvalidRevisionException, HgInvalidControlFileException { + /** + * Walk subset of the file's change history. + * @param start revision local index, inclusive; non-negative or {@link HgRepository#TIP} + * @param end revision local index, inclusive; non-negative or {@link HgRepository#TIP} + * @param inspector callback to visit changesets + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public void history(int start, int end, HgChangelog.Inspector inspector) throws HgRuntimeException { if (!exists()) { throw new IllegalStateException("Can't get history of invalid repository file node"); } @@ -432,18 +361,10 @@ * For a given revision of the file (identified with revision index), find out index of the corresponding changeset. * * @return changeset revision index - * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog - * @throws HgInvalidControlFileException if access to revlog index/data entry failed + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public int getChangesetRevisionIndex(int revision) throws HgInvalidControlFileException, HgInvalidRevisionException { - return content.linkRevision(revision); - } - /** - * @deprecated use {@link #getChangesetRevisionIndex(int)} instead - */ - @Deprecated - public int getChangesetLocalRevision(int revision) throws HgInvalidControlFileException, HgInvalidRevisionException { - return getChangesetRevisionIndex(revision); + public int getChangesetRevisionIndex(int fileRevisionIndex) throws HgRuntimeException { + return content.linkRevision(fileRevisionIndex); } /** @@ -451,20 +372,19 @@ * * @param nid revision of the file * @return changeset revision - * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog - * @throws HgInvalidControlFileException if access to revlog index/data entry failed + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public Nodeid getChangesetRevision(Nodeid nid) throws HgInvalidControlFileException, HgInvalidRevisionException { + public Nodeid getChangesetRevision(Nodeid nid) throws HgRuntimeException { int changelogRevision = getChangesetRevisionIndex(getRevisionIndex(nid)); return getRepo().getChangelog().getRevision(changelogRevision); } /** - * - * @return - * @throws HgDataStreamException if attempt to access file metadata failed + * Tells whether this file originates from another repository file + * @return true if this file is a copy of another from the repository + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public boolean isCopy() throws HgDataStreamException { + public boolean isCopy() throws HgRuntimeException { if (metadata == null || !metadata.checked(0)) { checkAndRecordMetadata(0); } @@ -478,22 +398,37 @@ * Get name of the file this one was copied from. * * @return name of the file origin - * @throws HgDataStreamException if attempt to access file metadata failed - * @throws UnsupportedOperationException if this file doesn't represent a copy ({@link #isCopy()} was false) + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public Path getCopySourceName() throws HgDataStreamException { + public Path getCopySourceName() throws HgRuntimeException { if (isCopy()) { return Path.create(metadata.find(0, "copy")); } throw new UnsupportedOperationException(); // XXX REVISIT, think over if Exception is good (clients would check isCopy() anyway, perhaps null is sufficient?) } - public Nodeid getCopySourceRevision() throws HgDataStreamException { + /** + * + * @return revision this file was copied from + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public Nodeid getCopySourceRevision() throws HgRuntimeException { if (isCopy()) { return Nodeid.fromAscii(metadata.find(0, "copyrev")); // XXX reuse/cache Nodeid } throw new UnsupportedOperationException(); } + + /** + * Get file flags recorded in the manifest + * @param fileRevisionIndex - revision local index, non-negative, or {@link HgRepository#TIP}. + * @see HgManifest#getFileFlags(int, Path) + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public HgManifest.Flags getFlags(int fileRevisionIndex) throws HgRuntimeException { + int changesetRevIndex = getChangesetRevisionIndex(fileRevisionIndex); + return getRepo().getManifest().getFileFlags(changesetRevIndex, getPath()); + } @Override public String toString() { @@ -504,9 +439,9 @@ return sb.toString(); } - private void checkAndRecordMetadata(int localRev) throws HgDataStreamException { + private void checkAndRecordMetadata(int localRev) throws HgInvalidControlFileException { // content() always initializes metadata. - // FIXME this is expensive way to find out metadata, distinct RevlogStream.Iterator would be better. + // TODO [post-1.0] this is expensive way to find out metadata, distinct RevlogStream.Iterator would be better. // Alternatively, may parameterize MetadataContentPipe to do prepare only. // For reference, when throwing CancelledException, hg status -A --rev 3:80 takes 70 ms // however, if we just consume buffer instead (buffer.position(buffer.limit()), same command takes ~320ms @@ -520,7 +455,7 @@ } catch (CancelledException ex) { // it's ok, we did that } catch (HgInvalidControlFileException ex) { - throw new HgDataStreamException(getPath(), ex); + throw ex.isRevisionIndexSet() ? ex : ex.setRevisionIndex(localRev); } } @@ -616,18 +551,16 @@ private static class MetadataInspector extends ErrorHandlingInspector implements RevlogStream.Inspector { private final Metadata metadata; private final RevlogStream.Inspector delegate; - private final Path fname; // need these only for error reporting private final LogFacility log; - public MetadataInspector(Metadata _metadata, LogFacility logFacility, Path file, RevlogStream.Inspector chain) { + public MetadataInspector(Metadata _metadata, LogFacility logFacility, RevlogStream.Inspector chain) { metadata = _metadata; - fname = file; log = logFacility; delegate = chain; setCancelSupport(CancelSupport.Factory.get(chain)); } - public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) throws HgException { + public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) { try { final int daLength = data.length(); if (daLength < 4 || data.readByte() != 1 || data.readByte() != 10) { @@ -648,12 +581,13 @@ } } catch (IOException ex) { recordFailure(ex); - } catch (HgDataStreamException ex) { - recordFailure(ex.setRevisionIndex(revisionNumber)); + } catch (HgInvalidControlFileException ex) { + // TODO RevlogStream, where this RevlogStream.Inspector goes, shall set File (as it's the only one having access to it) + recordFailure(ex.isRevisionIndexSet() ? ex : ex.setRevisionIndex(revisionNumber)); } } - private int parseMetadata(DataAccess data, final int daLength, ArrayList _metadata) throws IOException, HgDataStreamException { + private int parseMetadata(DataAccess data, final int daLength, ArrayList _metadata) throws IOException, HgInvalidControlFileException { int lastEntryStart = 2; int lastColon = -1; // XXX in fact, need smth like ByteArrayBuilder, similar to StringBuilder, @@ -674,7 +608,7 @@ break; } if (key == null || lastColon == -1 || i <= lastColon) { - log.error(getClass(), "Missing key in file revision metadata at index %d", i); + log.dump(getClass(), Error, "Missing key in file revision metadata at index %d", i); } value = new String(bos.toByteArray()).trim(); bos.reset(); @@ -705,16 +639,16 @@ // data.isEmpty is not reliable, renamed files of size==0 keep only metadata if (!metadataIsComplete) { // XXX perhaps, worth a testcase (empty file, renamed, read or ask ifCopy - throw new HgDataStreamException(fname, "Metadata is not closed properly", null); + throw new HgInvalidControlFileException("Metadata is not closed properly", null, null); } return lastEntryStart; } @Override - public void checkFailed() throws HgException, IOException, CancelledException { + public void checkFailed() throws HgRuntimeException, IOException, CancelledException { super.checkFailed(); if (delegate instanceof ErrorHandlingInspector) { - // XXX need to add ErrorDestination and pass it around (much like CancelSupport get passed) + // TODO need to add ErrorDestination (ErrorTarget/Acceptor?) and pass it around (much like CancelSupport get passed) // so that delegate would be able report its failures directly to caller without this hack ((ErrorHandlingInspector) delegate).checkFailed(); } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgDirstate.java --- a/src/org/tmatesoft/hg/repo/HgDirstate.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgDirstate.java Wed Jul 11 20:40:47 2012 +0200 @@ -17,27 +17,26 @@ package org.tmatesoft.hg.repo; import static org.tmatesoft.hg.core.Nodeid.NULL; +import static org.tmatesoft.hg.util.LogFacility.Severity.Debug; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; -import java.nio.charset.CharacterCodingException; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.TreeSet; -import org.tmatesoft.hg.core.HgInvalidControlFileException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.internal.DataAccess; import org.tmatesoft.hg.internal.EncodingHelper; import org.tmatesoft.hg.util.Pair; import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.PathPool; import org.tmatesoft.hg.util.PathRewrite; +import org.tmatesoft.hg.util.LogFacility.Severity; /** @@ -55,7 +54,7 @@ private final HgRepository repo; private final File dirstateFile; - private final PathPool pathPool; + private final Path.Source pathPool; private final PathRewrite canonicalPathRewrite; private Map normal; private Map added; @@ -66,13 +65,12 @@ */ private Map canonical2dirstateName; private Pair parents; - private String currentBranch; // canonicalPath may be null if we don't need to check for names other than in dirstate - /*package-local*/ HgDirstate(HgRepository hgRepo, File dirstate, PathPool pathPool, PathRewrite canonicalPath) { + /*package-local*/ HgDirstate(HgRepository hgRepo, File dirstate, Path.Source pathSource, PathRewrite canonicalPath) { repo = hgRepo; dirstateFile = dirstate; // XXX decide whether file names shall be kept local to reader (see #branches()) or passed from outside - this.pathPool = pathPool; + pathPool = pathSource; canonicalPathRewrite = canonicalPath; } @@ -88,15 +86,16 @@ return; } DataAccess da = repo.getDataAccess().create(dirstateFile); - if (da.isEmpty()) { - return; - } - // not sure linked is really needed here, just for ease of debug - normal = new LinkedHashMap(); - added = new LinkedHashMap(); - removed = new LinkedHashMap(); - merged = new LinkedHashMap(); try { + if (da.isEmpty()) { + return; + } + // not sure linked is really needed here, just for ease of debug + normal = new LinkedHashMap(); + added = new LinkedHashMap(); + removed = new LinkedHashMap(); + merged = new LinkedHashMap(); + parents = internalReadParents(da); // hg init; hg up produces an empty repository where dirstate has parents (40 bytes) only while (!da.isEmpty()) { @@ -144,11 +143,9 @@ } else if (state == 'm') { merged.put(r.name1, r); } else { - 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); + repo.getContext().getLog().dump(getClass(), Severity.Warn, "Dirstate record for file %s (size: %d, tstamp:%d) has unknown state '%c'", r.name1, r.size(), r.time, state); } } - } catch (CharacterCodingException ex) { - throw new HgInvalidControlFileException(String.format("Failed reading file names from dirstate using encoding %s", encodingHelper.charset().name()), ex, dirstateFile); } catch (IOException ex) { throw new HgInvalidControlFileException("Dirstate read failed", ex, dirstateFile); } finally { @@ -182,10 +179,10 @@ return new Pair(NULL, NULL); } DataAccess da = repo.getDataAccess().create(dirstateFile); - if (da.isEmpty()) { - return new Pair(NULL, NULL); - } try { + if (da.isEmpty()) { + return new Pair(NULL, NULL); + } return internalReadParents(da); } catch (IOException ex) { throw new HgInvalidControlFileException("Error reading working copy parents from dirstate", ex, dirstateFile); @@ -195,24 +192,11 @@ } /** - * FIXME move to a better place, e.g. WorkingCopy container that tracks both dirstate and branches (and, perhaps, undo, lastcommit and other similar information) + * TODO [post-1.0] it's really not a proper place for the method, need WorkingCopyContainer or similar * @return branch associated with the working directory */ - public String branch() throws HgInvalidControlFileException { - // XXX is it really proper place for the method? - if (currentBranch == null) { - currentBranch = readBranch(repo); - } - return currentBranch; - } - - /** - * XXX is it really proper place for the method? - * @return branch associated with the working directory - */ - /*package-local*/ static String readBranch(HgRepository repo) throws HgInvalidControlFileException { + /*package-local*/ static String readBranch(HgRepository repo, File branchFile) throws HgInvalidControlFileException { String branch = HgRepository.DEFAULT_BRANCH_NAME; - File branchFile = new File(repo.getRepositoryRoot(), "branch"); if (branchFile.exists()) { try { BufferedReader r = new BufferedReader(new FileReader(branchFile)); @@ -223,7 +207,7 @@ branch = b == null || b.length() == 0 ? HgRepository.DEFAULT_BRANCH_NAME : b; r.close(); } catch (FileNotFoundException ex) { - repo.getContext().getLog().debug(HgDirstate.class, ex, null); // log verbose debug, exception might be legal here + repo.getContext().getLog().dump(HgDirstate.class, Debug, ex, null); // log verbose debug, exception might be legal here // IGNORE } catch (IOException ex) { throw new HgInvalidControlFileException("Error reading file with branch information", ex, branchFile); diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgIgnore.java --- a/src/org/tmatesoft/hg/repo/HgIgnore.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgIgnore.java Wed Jul 11 20:40:47 2012 +0200 @@ -213,7 +213,7 @@ } /** - * A handy wrap of {@link #isIgnored(Path)} into {@link Path.Matcher}. Yields same result as {@link #isIgnored(Path)}. + * A handy wrap of {@link #isIgnored(Path)} into {@link org.tmatesoft.hg.util.Path.Matcher}. Yields same result as {@link #isIgnored(Path)}. * @return true if file is deemed ignored. */ public boolean accept(Path path) { diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgInternals.java --- a/src/org/tmatesoft/hg/repo/HgInternals.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgInternals.java Wed Jul 11 20:40:47 2012 +0200 @@ -25,24 +25,25 @@ import java.net.InetAddress; import java.net.UnknownHostException; -import org.tmatesoft.hg.core.HgInvalidControlFileException; -import org.tmatesoft.hg.core.HgInvalidRevisionException; +import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.core.SessionContext; import org.tmatesoft.hg.internal.Experimental; import org.tmatesoft.hg.internal.Internals; import org.tmatesoft.hg.internal.RelativePathRewrite; import org.tmatesoft.hg.internal.WinToNixPathRewrite; +import org.tmatesoft.hg.repo.HgSubrepoLocation.Kind; import org.tmatesoft.hg.util.FileIterator; import org.tmatesoft.hg.util.FileWalker; import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.PathPool; import org.tmatesoft.hg.util.PathRewrite; /** * DO NOT USE THIS CLASS, INTENDED FOR TESTING PURPOSES. * - * This class gives access to repository internals, and holds methods that I'm not confident have to be widely accessible + *

This class is not part of the public API and may change or vanish any moment. + * + *

This class gives access to repository internals, and holds methods that I'm not confident have to be widely accessible * Debug helper, to access otherwise restricted (package-local) methods * * @author Artem Tikhomirov @@ -58,7 +59,7 @@ } public HgDirstate getDirstate() throws HgInvalidControlFileException { - return repo.loadDirstate(new PathPool(new PathRewrite.Empty())); + return repo.loadDirstate(new Path.SimpleSource()); } // tests @@ -72,7 +73,7 @@ } }; } - HgDirstate ds = new HgDirstate(repo, new File(repo.getRepositoryRoot(), "dirstate"), new PathPool(new PathRewrite.Empty()), canonicalPath); + HgDirstate ds = new HgDirstate(repo, new File(repo.getRepositoryRoot(), "dirstate"), new Path.SimpleSource(), canonicalPath); ds.read(repo.getImplHelper().buildFileNameEncodingHelper()); return ds; } @@ -84,6 +85,10 @@ } return rv; } + + public HgSubrepoLocation newSubrepo(Path loc, String src, Kind kind, Nodeid rev) { + return new HgSubrepoLocation(repo, loc, src, kind, rev); + } public static File getRepositoryDir(HgRepository hgRepo) { return hgRepo.getRepositoryRoot(); @@ -134,17 +139,16 @@ } } - @Experimental(reason="Don't want to expose io.File from HgRepository; need to create FileIterator for working dir. Need a place to keep that code") /*package-local*/ FileIterator createWorkingDirWalker(Path.Matcher workindDirScope) { File repoRoot = repo.getWorkingDir(); Path.Source pathSrc = new Path.SimpleSource(new PathRewrite.Composite(new RelativePathRewrite(repoRoot), repo.getToRepoPathHelper())); // Impl note: simple source is enough as files in the working dir are all unique // even if they might get reused (i.e. after FileIterator#reset() and walking once again), // path caching is better to be done in the code which knows that path are being reused - return new FileWalker(repoRoot, pathSrc, workindDirScope); + return new FileWalker(repo.getContext(), repoRoot, pathSrc, workindDirScope); } - // expose othewise package-local information primarily to use in our own o.t.hg.core package + // expose otherwise package-local information primarily to use in our own o.t.hg.core package public static SessionContext getContext(HgRepository repo) { return repo.getContext(); } @@ -152,9 +156,10 @@ // Convenient check of revision index for validity (not all negative values are wrong as long as we use negative constants) public static boolean wrongRevisionIndex(int rev) { - return rev < 0 && rev != TIP && rev != WORKING_COPY && rev != BAD_REVISION; + // TODO Another method to check,throw and expand TIP at once (check[Revision|Revlog]Index() + return rev < 0 && rev != TIP && rev != WORKING_COPY && rev != BAD_REVISION && rev != NO_REVISION; } - + // throws HgInvalidRevisionException or IllegalArgumentException if [start..end] range is not a subrange of [0..lastRevision] public static void checkRevlogRange(int start, int end, int lastRevision) throws HgInvalidRevisionException { if (start < 0 || start > lastRevision) { diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgInvalidControlFileException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/repo/HgInvalidControlFileException.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,59 @@ +/* + * 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 + * 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.repo; + +import java.io.File; + +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.util.Path; + +/** + * Subclass of {@link HgInvalidFileException} to indicate failure to deal with one of Mercurial control files + * (most likely those under .hg/, but also those residing in the repository, with special meaning to the Mercurial, like .hgtags or .hgignore) + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@SuppressWarnings("serial") +public class HgInvalidControlFileException extends HgInvalidFileException { + // XXX Perhaps, child HgInvalidRevlogException and parent HgInvalidRepositoryFileException? + + public HgInvalidControlFileException(String message, Throwable th, File file) { + super(message, th, file); + } + + @Override + public HgInvalidControlFileException setFile(File file) { + super.setFile(file); + return this; + } + + @Override + public HgInvalidControlFileException setRevision(Nodeid r) { + return (HgInvalidControlFileException) super.setRevision(r); + } + + @Override + public HgInvalidControlFileException setRevisionIndex(int rev) { + return (HgInvalidControlFileException) super.setRevisionIndex(rev); + } + + @Override + public HgInvalidControlFileException setFileName(Path name) { + return (HgInvalidControlFileException) super.setFileName(name); + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgInvalidDataFormatException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/repo/HgInvalidDataFormatException.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,36 @@ +/* + * Copyright (c) 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 + * 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.repo; + +/** + * Indicates broken, unknown or otherwise bad data structure. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@SuppressWarnings("serial") +public class HgInvalidDataFormatException extends HgRuntimeException { + // IMPLEMENTATION NOTE. Perhaps, this might be intermediate class between HgRuntimeException and HgInvalidFileException + + public HgInvalidDataFormatException(String message) { + super(message, null); + } + + public HgInvalidDataFormatException(String message, Throwable cause) { + super(message, cause); + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgInvalidFileException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/repo/HgInvalidFileException.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,63 @@ +/* + * 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 + * 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.repo; + +import java.io.File; +import java.io.IOException; + +/** + * Thrown when there are troubles working with local file. Most likely (but not necessarily) wraps IOException. Might be + * perceived as specialized IOException with optional File and other repository information. + * + * Hg4J tries to minimize chances for IOException to occur (i.e. {@link File#canRead()} is checked before attempt to + * read a file that might not exist, and doesn't use this exception to wrap each and any {@link IOException} source (e.g. + * #close() calls are unlikely to yield it), hence it is likely to address real cases when I/O error occurs. + * + * On the other hand, when a file is supposed to exist and be readable, this exception might get thrown as well to indicate + * that's not true. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@SuppressWarnings("serial") +public class HgInvalidFileException extends HgRuntimeException { + // IMPLEMENTATION NOTE: Once needed, there might be intermediate e.g. HgDataStreamException + // (between HgInvalidFileException and HgRuntimeException) to root data access exceptions + // that do not originate from local files but e.g. a connection + + public HgInvalidFileException(String message, Throwable th) { + super(message, th); + } + + public HgInvalidFileException(String message, Throwable th, File file) { + super(message, th); + details.setFile(file); // allows null + } + + public HgInvalidFileException setFile(File file) { + assert file != null; // doesn't allow null not to clear file accidentally + details.setFile(file); + return this; + } + + /** + * @return file object that causes troubles, or null if specific file is unknown + */ + public File getFile() { + return details.getFile(); + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgInvalidRevisionException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/repo/HgInvalidRevisionException.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,62 @@ +/* + * 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 + * 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.repo; + +import org.tmatesoft.hg.core.Nodeid; + +/** + * Use of revision or revision local index that is not valid for a given revlog. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@SuppressWarnings("serial") +public class HgInvalidRevisionException extends HgRuntimeException { + + /** + * + * This exception is not expected to be initialized with another exception, although those who need to, + * may still use {@link #initCause(Throwable)} + * + * @param message optional description of the issue + * @param revision invalid revision, may be null if revisionIndex is used + * @param revisionIndex invalid revision index, may be null if not known and revision is supplied + */ + public HgInvalidRevisionException(String message, Nodeid revision, Integer revisionIndex) { + super(message, null); + assert revision != null || revisionIndex != null; + if (revision != null) { + setRevision(revision); + } + if (revisionIndex != null) { + setRevisionIndex(revisionIndex); + } + } + + public HgInvalidRevisionException(Nodeid revision) { + this(null, revision, null); + } + + public HgInvalidRevisionException(int revisionIndex) { + this(null, null, revisionIndex); + } + + public HgInvalidRevisionException setRevisionIndex(int revisionIndex, int rangeLeft, int rangeRight) { + details.setRevisionIndexBoundary(revisionIndex, rangeLeft, rangeRight); + return this; + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgInvalidStateException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/repo/HgInvalidStateException.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,34 @@ +/* + * Copyright (c) 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 + * 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.repo; + + +/** + * Thrown to indicate unexpected or otherwise inappropriate state of the library, assumptions/preconditions not met, etc. + * Unlike {@link HgInvalidFileException} and {@link HgInvalidControlFileException}, to describe error state not related to IO operations. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@SuppressWarnings("serial") +public class HgInvalidStateException extends HgRuntimeException { + + public HgInvalidStateException(String message) { + super(message, null); + // no cons with Throwable as it deemed exceptional to use HgInvalidStateException to wrap another exception + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgLookup.java --- a/src/org/tmatesoft/hg/repo/HgLookup.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgLookup.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -16,13 +16,15 @@ */ package org.tmatesoft.hg.repo; +import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; + import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import org.tmatesoft.hg.core.HgBadArgumentException; -import org.tmatesoft.hg.core.HgInvalidFileException; +import org.tmatesoft.hg.core.HgRepositoryNotFoundException; import org.tmatesoft.hg.core.SessionContext; import org.tmatesoft.hg.internal.BasicSessionContext; import org.tmatesoft.hg.internal.ConfigFile; @@ -46,42 +48,43 @@ sessionContext = ctx; } - public HgRepository detectFromWorkingDir() throws HgInvalidFileException { + public HgRepository detectFromWorkingDir() throws HgRepositoryNotFoundException { return detect(System.getProperty("user.dir")); } - public HgRepository detect(String location) throws HgInvalidFileException { + public HgRepository detect(String location) throws HgRepositoryNotFoundException { return detect(new File(location)); } // look up in specified location and above - public HgRepository detect(File location) throws HgInvalidFileException { - File dir = location.getAbsoluteFile(); - File repository; - do { - repository = new File(dir, ".hg"); - if (repository.exists() && repository.isDirectory()) { - break; + public HgRepository detect(File location) throws HgRepositoryNotFoundException { + try { + File dir = location.getAbsoluteFile(); + File repository; + do { + repository = new File(dir, ".hg"); + if (repository.exists() && repository.isDirectory()) { + break; + } + repository = null; + dir = dir.getParentFile(); + + } while(dir != null); + if (repository == null) { + throw new HgRepositoryNotFoundException(String.format("Can't locate .hg/ directory of Mercurial repository in %s nor in parent dirs", location)).setLocation(location.getPath()); } - repository = null; - dir = dir.getParentFile(); - - } while(dir != null); - if (repository == null) { - // return invalid repository - return new HgRepository(location.getPath()); - } - try { String repoPath = repository.getParentFile().getCanonicalPath(); return new HgRepository(getContext(), repoPath, repository); } catch (IOException ex) { - throw new HgInvalidFileException(location.toString(), ex, location); + HgRepositoryNotFoundException t = new HgRepositoryNotFoundException("Failed to access repository"); + t.setLocation(location.getPath()).initCause(ex); + throw t; } } - public HgBundle loadBundle(File location) throws HgInvalidFileException { + public HgBundle loadBundle(File location) throws HgRepositoryNotFoundException { if (location == null || !location.canRead()) { - throw new HgInvalidFileException(String.format("Can't read file %s", location == null ? null : location.getPath()), null, location); + throw new HgRepositoryNotFoundException(String.format("Can't read file %s", location)).setLocation(String.valueOf(location)); } return new HgBundle(getContext(), new DataAccessProvider(getContext()), location).link(); } @@ -134,7 +137,7 @@ globalCfg.addLocation(new File(System.getProperty("user.home"), ".hgrc")); } catch (IOException ex) { // XXX perhaps, makes sense to let caller/client know that we've failed to read global config? - getContext().getLog().warn(getClass(), ex, null); + getContext().getLog().dump(getClass(), Warn, ex, null); } } return globalCfg; @@ -142,7 +145,7 @@ private SessionContext getContext() { if (sessionContext == null) { - sessionContext = new BasicSessionContext(null, null); + sessionContext = new BasicSessionContext(null); } return sessionContext; } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgManifest.java --- a/src/org/tmatesoft/hg/repo/HgManifest.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgManifest.java Wed Jul 11 20:40:47 2012 +0200 @@ -17,44 +17,58 @@ package org.tmatesoft.hg.repo; import static org.tmatesoft.hg.core.Nodeid.NULL; -import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; -import static org.tmatesoft.hg.repo.HgRepository.TIP; +import static org.tmatesoft.hg.repo.HgRepository.*; +import static org.tmatesoft.hg.util.LogFacility.Severity.Info; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import org.tmatesoft.hg.core.HgBadStateException; -import org.tmatesoft.hg.core.HgException; -import org.tmatesoft.hg.core.HgInvalidControlFileException; +import org.tmatesoft.hg.core.HgChangesetFileSneaker; import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.Callback; import org.tmatesoft.hg.internal.DataAccess; import org.tmatesoft.hg.internal.DigestHelper; import org.tmatesoft.hg.internal.EncodingHelper; -import org.tmatesoft.hg.internal.Experimental; +import org.tmatesoft.hg.internal.IntMap; import org.tmatesoft.hg.internal.IterateControlMediator; import org.tmatesoft.hg.internal.Lifecycle; -import org.tmatesoft.hg.internal.Pool2; +import org.tmatesoft.hg.internal.IdentityPool; import org.tmatesoft.hg.internal.RevlogStream; import org.tmatesoft.hg.util.CancelSupport; import org.tmatesoft.hg.util.Path; import org.tmatesoft.hg.util.ProgressSupport; +import org.tmatesoft.hg.util.LogFacility.Severity; /** - * + * Representation of Mercurial manifest file (list of file names and their revisions in a particular changeset) + * + * @see http://mercurial.selenic.com/wiki/Manifest * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class HgManifest extends Revlog { +public final class HgManifest extends Revlog { private RevisionMapper revisionMap; private EncodingHelper encodingHelper; + /** + * File flags recorded in manifest + */ public enum Flags { - Exec, Link; + /** + * Executable bit set + */ + Exec, + /** + * Symbolic link + */ + Link, + /** + * Regular file + */ + RegularFile; static Flags parse(String flags) { if ("x".equalsIgnoreCase(flags)) { @@ -64,14 +78,14 @@ return Link; } if (flags == null) { - return null; + return RegularFile; } throw new IllegalStateException(flags); } static Flags parse(byte[] data, int start, int length) { if (length == 0) { - return null; + return RegularFile; } if (length == 1) { if (data[start] == 'x') { @@ -92,6 +106,9 @@ if (this == Link) { return "l"; } + if (this == RegularFile) { + return ""; + } throw new IllegalStateException(toString()); } } @@ -117,8 +134,10 @@ * incrementally, nor it mandates presence of manifest version for a changeset. Thus, there might be changesets that record {@link Nodeid#NULL} * as corresponding manifest revision. This situation is deemed exceptional now and what would inspector get depends on whether * start or end arguments point to such changeset, or such changeset happen to be somewhere inside the range - * [start..end]. Implementation does it best to report empty manifests (Inspector.begin(BAD_REVISION, NULL, csetRevIndex); - * followed immediately by Inspector.end(BAD_REVISION) when start and/or end point to changeset with no associated + * [start..end]. Implementation does it best to report empty manifests + * (Inspector.begin(HgRepository.NO_REVISION, NULL, csetRevIndex); + * followed immediately by Inspector.end(HgRepository.NO_REVISION) + * when start and/or end point to changeset with no associated * manifest revision. However, if changeset-manifest revision pairs look like: *

 	 *   3  8
@@ -128,12 +147,14 @@
 	 * call walk(3,5, insp) would yield only (3,8) and (5,9) to the inspector, without additional empty 
 	 * Inspector.begin(); Inspector.end() call pair.   
 	 * 
+	 * @see HgRepository#NO_REVISION
 	 * @param start changelog (not manifest!) revision to begin with
 	 * @param end changelog (not manifest!) revision to stop, inclusive.
 	 * @param inspector manifest revision visitor, can't be null
-	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
+	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
+	 * @throws IllegalArgumentException if inspector callback is null
 	 */
-	public void walk(int start, int end, final Inspector inspector) throws /*FIXME HgInvalidRevisionException,*/ HgInvalidControlFileException {
+	public void walk(int start, int end, final Inspector inspector) throws HgRuntimeException, IllegalArgumentException {
 		if (inspector == null) {
 			throw new IllegalArgumentException();
 		}
@@ -141,14 +162,14 @@
 		int manifestFirst, manifestLast, i = 0;
 		do {
 			manifestFirst = fromChangelog(csetFirst+i);
-			if (manifestFirst == -1) {
-				inspector.begin(BAD_REVISION, NULL, csetFirst+i);
-				inspector.end(BAD_REVISION);
+			if (manifestFirst == BAD_REVISION) {
+				inspector.begin(NO_REVISION, NULL, csetFirst+i);
+				inspector.end(NO_REVISION);
 			}
 			i++;
-		} while (manifestFirst == -1 && csetFirst+i <= csetLast);
-		if (manifestFirst == -1) {
-			getRepo().getContext().getLog().info(getClass(), "None of changesets [%d..%d] have associated manifest revision", csetFirst, csetLast);
+		} while (manifestFirst == BAD_REVISION && csetFirst+i <= csetLast);
+		if (manifestFirst == BAD_REVISION) {
+			getRepo().getContext().getLog().dump(getClass(), Info, "None of changesets [%d..%d] have associated manifest revision", csetFirst, csetLast);
 			// we ran through all revisions in [start..end] and none of them had manifest.
 			// we reported that to inspector and proceeding is done now.
 			return;
@@ -156,16 +177,16 @@
 		i = 0;
 		do {
 			manifestLast = fromChangelog(csetLast-i);
-			if (manifestLast == -1) {
-				inspector.begin(BAD_REVISION, NULL, csetLast-i);
-				inspector.end(BAD_REVISION);
+			if (manifestLast == BAD_REVISION) {
+				inspector.begin(NO_REVISION, NULL, csetLast-i);
+				inspector.end(NO_REVISION);
 			}
 			i++;
-		} while (manifestLast == -1 && csetLast-i >= csetFirst);
-		if (manifestLast == -1) {
-			// hmm, manifestFirst != -1 here, hence there's i from [csetFirst..csetLast] for which manifest entry exists, 
-			// and thus it's impossible to run into manifestLast == -1. Nevertheless, never hurts to check.
-			throw new HgBadStateException(String.format("Manifest %d-%d(!) for cset range [%d..%d] ", manifestFirst, manifestLast, csetFirst, csetLast));
+		} while (manifestLast == BAD_REVISION && csetLast-i >= csetFirst);
+		if (manifestLast == BAD_REVISION) {
+			// hmm, manifestFirst != BAD_REVISION here, hence there's i from [csetFirst..csetLast] for which manifest entry exists, 
+			// and thus it's impossible to run into manifestLast == BAD_REVISION. Nevertheless, never hurts to check.
+			throw new HgInvalidStateException(String.format("Manifest %d-%d(!) for cset range [%d..%d] ", manifestFirst, manifestLast, csetFirst, csetLast));
 		}
 		if (manifestLast < manifestFirst) {
 			// there are tool-constructed repositories that got order of changeset revisions completely different from that of manifest
@@ -185,8 +206,10 @@
 	 * 
 	 * @param inspector manifest revision visitor, can't be null
 	 * @param revisionIndexes local indexes of changesets to visit, non-null
+	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
+	 * @throws InvalidArgumentException if supplied arguments are nulls
 	 */
-	public void walk(final Inspector inspector, int... revisionIndexes) throws HgInvalidControlFileException{
+	public void walk(final Inspector inspector, int... revisionIndexes) throws HgRuntimeException, IllegalArgumentException {
 		if (inspector == null || revisionIndexes == null) {
 			throw new IllegalArgumentException();
 		}
@@ -196,15 +219,18 @@
 	
 	// 
 	/**
-	 * Tells manifest revision number that corresponds to the given changeset.
-	 * @return manifest revision index, or -1 if changeset has no associated manifest (cset records NULL nodeid for manifest) 
+	 * Tells manifest revision number that corresponds to the given changeset. May return {@link HgRepository#BAD_REVISION} 
+	 * if changeset has no associated manifest (cset records NULL nodeid for manifest).
+	 * @return manifest revision index, non-negative, or {@link HgRepository#BAD_REVISION}.
+	 * @throws HgInvalidRevisionException if method argument specifies non-existent revision index
+	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
 	 */
-	/*package-local*/ int fromChangelog(int changesetRevisionIndex) throws HgInvalidControlFileException {
+	/*package-local*/ int fromChangelog(int changesetRevisionIndex) throws HgInvalidRevisionException, HgInvalidControlFileException {
 		if (HgInternals.wrongRevisionIndex(changesetRevisionIndex)) {
-			throw new IllegalArgumentException(String.valueOf(changesetRevisionIndex));
+			throw new HgInvalidRevisionException(changesetRevisionIndex);
 		}
 		if (changesetRevisionIndex == HgRepository.WORKING_COPY || changesetRevisionIndex == HgRepository.BAD_REVISION) {
-			throw new IllegalArgumentException("Can't use constants like WORKING_COPY or BAD_REVISION");
+			throw new HgInvalidRevisionException("Can't use constants like WORKING_COPY or BAD_REVISION", null, changesetRevisionIndex);
 		}
 		// revisionNumber == TIP is processed by RevisionMapper 
 		if (revisionMap == null) {
@@ -216,70 +242,93 @@
 	
 	/**
 	 * Extracts file revision as it was known at the time of given changeset.
+	 * 

For more thorough details about file at specific changeset, use {@link HgChangesetFileSneaker}. + *

To visit few changesets for the same file, use {@link #walkFileRevisions(Path, Inspector, int...)} * + * @see #walkFileRevisions(Path, Inspector, int...) + * @see HgChangesetFileSneaker * @param changelogRevisionIndex local changeset index * @param file path to file in question * @return file revision or null if manifest at specified revision doesn't list such file + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - @Experimental(reason="Perhaps, HgDataFile shall own this method, or get a delegate?") - public Nodeid getFileRevision(int changelogRevisionIndex, final Path file) throws HgInvalidControlFileException{ - return getFileRevisions(file, changelogRevisionIndex).get(changelogRevisionIndex); + public Nodeid getFileRevision(int changelogRevisionIndex, final Path file) throws HgInvalidRevisionException, HgInvalidControlFileException { + // there's no need for HgDataFile to own this method, or get a delegate + // as most of HgDataFile API is using file revision indexes, and there's easy step from file revision index to + // both file revision and changeset revision index. But there's no easy way to go from changesetRevisionIndex to + // file revision (the task this method solves), exept for HgFileInformer + // I feel methods dealing with changeset indexes shall be more exposed in HgChangelog and HgManifest API. + // TODO need tests + int manifestRevIndex = fromChangelog(changelogRevisionIndex); + if (manifestRevIndex == BAD_REVISION) { + return null; + } + IntMap resMap = new IntMap(3); + FileLookupInspector parser = new FileLookupInspector(encodingHelper, file, resMap, null); + parser.walk(manifestRevIndex, content); + return resMap.get(changelogRevisionIndex); } - // XXX package-local, IntMap, and HgDataFile getFileRevisionAt(int... localChangelogRevisions) - @Experimental(reason="@see #getFileRevision") - public Map getFileRevisions(final Path file, int... changelogRevisionIndexes) throws HgInvalidControlFileException{ - // FIXME need tests - int[] manifestRevisionIndexes = toManifestRevisionIndexes(changelogRevisionIndexes, null); - final HashMap rv = new HashMap(changelogRevisionIndexes.length); - content.iterate(manifestRevisionIndexes, true, new RevlogStream.Inspector() { - - public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) throws HgException { - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - try { - byte b; - while (!data.isEmpty() && (b = data.readByte()) != '\n') { - if (b != 0) { - bos.write(b); - } else { - String fname = new String(bos.toByteArray()); - bos.reset(); - if (file.toString().equals(fname)) { - byte[] nid = new byte[40]; - data.readBytes(nid, 0, 40); - rv.put(linkRevision, Nodeid.fromAscii(nid, 0, 40)); - break; - } else { - data.skip(40); - } - // else skip to the end of line - while (!data.isEmpty() && (b = data.readByte()) != '\n') - ; - } - } - } catch (IOException ex) { - throw new HgException(ex); - } - } - }); - return rv; + /** + * Visit file revisions as they were recorded at the time of given changesets. Same file revision may be reported as many times as + * there are changesets that refer to that revision. Both {@link Inspector#begin(int, Nodeid, int)} and {@link Inspector#end(int)} + * with appropriate values are invoked around {@link Inspector#next(Nodeid, Path, Flags)} call for the supplied file + * + *

NOTE, this method doesn't respect return values from callback (i.e. to stop iteration), as it's lookup of a single file + * and canceling it seems superfluous. However, this may change in future and it's recommended to return true from + * all {@link Inspector} methods. + * + * @see #getFileRevision(int, Path) + * @param file path of interest + * @param inspector callback to receive details about selected file + * @param changelogRevisionIndexes changeset indexes to visit + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public void walkFileRevisions(Path file, Inspector inspector, int... changelogRevisionIndexes) throws HgRuntimeException { + if (file == null || inspector == null || changelogRevisionIndexes == null) { + throw new IllegalArgumentException(); + } + // TODO [post-1.0] need tests. There's Main#checkWalkFileRevisions that may be a starting point + int[] manifestRevIndexes = toManifestRevisionIndexes(changelogRevisionIndexes, null); + FileLookupInspector parser = new FileLookupInspector(encodingHelper, file, inspector); + parser.walk(manifestRevIndexes, content); + } + + /** + * Extract file {@link Flags flags} as they were recorded in appropriate manifest version. + * + * @see HgDataFile#getFlags(int) + * @param changesetRevIndex changeset revision index + * @param file path to look up + * @return one of predefined enum values, or null if file was not known in the specified revision + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public Flags getFileFlags(int changesetRevIndex, Path file) throws HgInvalidRevisionException, HgInvalidControlFileException { + int manifestRevIdx = fromChangelog(changesetRevIndex); + IntMap resMap = new IntMap(2); + FileLookupInspector parser = new FileLookupInspector(encodingHelper, file, null, resMap); + parser.walk(manifestRevIdx, content); + return resMap.get(changesetRevIndex); } /** * @param changelogRevisionIndexes non-null * @param inspector may be null if reporting of missing manifests is not needed + * @throws HgInvalidRevisionException if arguments specify non-existent revision index + * @throws IllegalArgumentException if any index argument is not a revision index + * @throws HgInvalidControlFileException if access to revlog index/data entry failed */ - private int[] toManifestRevisionIndexes(int[] changelogRevisionIndexes, Inspector inspector) throws HgInvalidControlFileException { + private int[] toManifestRevisionIndexes(int[] changelogRevisionIndexes, Inspector inspector) throws HgInvalidRevisionException, HgInvalidControlFileException { int[] manifestRevs = new int[changelogRevisionIndexes.length]; boolean needsSort = false; int j = 0; for (int i = 0; i < changelogRevisionIndexes.length; i++) { final int manifestRevisionIndex = fromChangelog(changelogRevisionIndexes[i]); - if (manifestRevisionIndex == -1) { + if (manifestRevisionIndex == BAD_REVISION) { if (inspector != null) { - inspector.begin(BAD_REVISION, NULL, changelogRevisionIndexes[i]); - inspector.end(BAD_REVISION); + inspector.begin(NO_REVISION, NULL, changelogRevisionIndexes[i]); + inspector.end(NO_REVISION); } // othrwise, ignore changeset without manifest } else { @@ -303,21 +352,19 @@ } } + @Callback public interface Inspector { boolean begin(int mainfestRevision, Nodeid nid, int changelogRevision); /** - * @deprecated switch to {@link Inspector2#next(Nodeid, Path, Flags)} + * @param nid file revision + * @param fname file name + * @param flags one of {@link HgManifest.Flags} constants, not null + * @return true to continue iteration, false to stop */ - @Deprecated - boolean next(Nodeid nid, String fname, String flags); + boolean next(Nodeid nid, Path fname, Flags flags); boolean end(int manifestRevision); } - @Experimental(reason="Explore Path alternative for filenames and enum for flags") - public interface Inspector2 extends Inspector { - boolean next(Nodeid nid, Path fname, Flags flags); - } - /** * When Pool uses Strings directly, * ManifestParser creates new String instance with new char[] value, and does byte->char conversion. @@ -391,9 +438,8 @@ private static class ManifestParser implements RevlogStream.Inspector, Lifecycle { private final Inspector inspector; - private final Inspector2 inspector2; - private Pool2 nodeidPool, thisRevPool; - private final Pool2 fnamePool; + private IdentityPool nodeidPool, thisRevPool; + private final IdentityPool fnamePool; private byte[] nodeidLookupBuffer = new byte[20]; // get reassigned each time new Nodeid is added to pool private final ProgressSupport progressHelper; private IterateControlMediator iterateControl; @@ -402,15 +448,14 @@ public ManifestParser(Inspector delegate, EncodingHelper eh) { assert delegate != null; inspector = delegate; - inspector2 = delegate instanceof Inspector2 ? (Inspector2) delegate : null; encHelper = eh; - nodeidPool = new Pool2(); - fnamePool = new Pool2(); - thisRevPool = new Pool2(); + nodeidPool = new IdentityPool(); + fnamePool = new IdentityPool(); + thisRevPool = new IdentityPool(); progressHelper = ProgressSupport.Factory.get(delegate); } - public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) throws HgException { + public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) { try { if (!inspector.begin(revisionNumber, new Nodeid(nodeid, true), linkRevision)) { iterateControl.stop(); @@ -453,15 +498,9 @@ // for cpython 0..10k, there are 4361062 flag checks, and there's only 1 unique flag flags = Flags.parse(data, x + nodeidLen, i-x-nodeidLen); } else { - flags = null; + flags = Flags.RegularFile; } - boolean good2go; - if (inspector2 == null) { - String flagString = flags == null ? null : flags.nativeString(); - good2go = inspector.next(nid, fname.toString(), flagString); - } else { - good2go = inspector2.next(nid, fname, flags); - } + boolean good2go = inspector.next(nid, fname, flags); if (!good2go) { iterateControl.stop(); return; @@ -481,13 +520,13 @@ // (next manifest is likely to refer to most of them, although in specific cases // like commit in another branch a lot may be useless) nodeidPool.clear(); - Pool2 t = nodeidPool; + IdentityPool t = nodeidPool; nodeidPool = thisRevPool; thisRevPool = t; iterateControl.checkCancelled(); progressHelper.worked(1); } catch (IOException ex) { - throw new HgException(ex); + throw new HgInvalidControlFileException("Failed reading manifest", ex, null).setRevisionIndex(revisionNumber); } } @@ -504,24 +543,32 @@ private static class RevisionMapper implements RevlogStream.Inspector, Lifecycle { - private final int changelogRevisions; + private final int changelogRevisionCount; private int[] changelog2manifest; private final HgRepository repo; public RevisionMapper(HgRepository hgRepo) { repo = hgRepo; - changelogRevisions = repo.getChangelog().getRevisionCount(); + changelogRevisionCount = repo.getChangelog().getRevisionCount(); } - // respects TIP - public int at(int revisionNumber) { - if (revisionNumber == TIP) { - revisionNumber = changelogRevisions - 1; + /** + * Get index of manifest revision that corresponds to specified changeset + * @param changesetRevisionIndex non-negative index of changelog revision, or {@link HgRepository#TIP} + * @return index of manifest revision, or {@link HgRepository#BAD_REVISION} if changeset doesn't reference a valid manifest + * @throws HgInvalidRevisionException if method argument specifies non-existent revision index + */ + public int at(int changesetRevisionIndex) throws HgInvalidRevisionException { + if (changesetRevisionIndex == TIP) { + changesetRevisionIndex = changelogRevisionCount - 1; + } + if (changesetRevisionIndex >= changelogRevisionCount) { + throw new HgInvalidRevisionException(changesetRevisionIndex); } if (changelog2manifest != null) { - return changelog2manifest[revisionNumber]; + return changelog2manifest[changesetRevisionIndex]; } - return revisionNumber; + return changesetRevisionIndex; } // XXX likely can be replaced with Revlog.RevisionInspector @@ -530,12 +577,12 @@ // next assertion is not an error, rather assumption check, which is too development-related to be explicit exception - // I just wonder if there are manifests that have two entries pointing to single changeset. It seems unrealistic, though - // changeset records one and only one manifest nodeid - assert changelog2manifest[linkRevision] == -1 : String.format("revision:%d, link:%d, already linked to revision:%d", revisionNumber, linkRevision, changelog2manifest[linkRevision]); + assert changelog2manifest[linkRevision] == BAD_REVISION : String.format("revision:%d, link:%d, already linked to revision:%d", revisionNumber, linkRevision, changelog2manifest[linkRevision]); changelog2manifest[linkRevision] = revisionNumber; } else { if (revisionNumber != linkRevision) { - changelog2manifest = new int[changelogRevisions]; - Arrays.fill(changelog2manifest, -1); + changelog2manifest = new int[changelogRevisionCount]; + Arrays.fill(changelog2manifest, BAD_REVISION); for (int i = 0; i < revisionNumber; changelog2manifest[i] = i, i++) ; changelog2manifest[linkRevision] = revisionNumber; @@ -544,14 +591,14 @@ } public void start(int count, Callback callback, Object token) { - if (count != changelogRevisions) { - assert count < changelogRevisions; // no idea what to do if manifest has more revisions than changelog + if (count != changelogRevisionCount) { + assert count < changelogRevisionCount; // no idea what to do if manifest has more revisions than changelog // the way how manifest may contain more revisions than changelog, as I can imagine, is a result of // some kind of an import tool (e.g. from SVN or CVS), that creates manifest and changelog independently. // Note, it's pure guess, I didn't see such repository yet (although the way manifest revisions // in cpython repo are numbered makes me think aforementioned way) - changelog2manifest = new int[changelogRevisions]; - Arrays.fill(changelog2manifest, -1); + changelog2manifest = new int[changelogRevisionCount]; + Arrays.fill(changelog2manifest, BAD_REVISION); } } @@ -562,26 +609,122 @@ // I assume there'd be not too many revisions we don't know manifest of ArrayList undefinedChangelogRevision = new ArrayList(); for (int i = 0; i < changelog2manifest.length; i++) { - if (changelog2manifest[i] == -1) { + if (changelog2manifest[i] == BAD_REVISION) { undefinedChangelogRevision.add(i); } } for (int u : undefinedChangelogRevision) { - try { - Nodeid manifest = repo.getChangelog().range(u, u).get(0).manifest(); - // TODO calculate those missing effectively (e.g. cache and sort nodeids to speed lookup - // right away in the #next (may refactor ParentWalker's sequential and sorted into dedicated helper and reuse here) - if (manifest.isNull()) { - repo.getContext().getLog().warn(getClass(), "Changeset %d has no associated manifest entry", u); - // keep -1 in the changelog2manifest map. - } else { - changelog2manifest[u] = repo.getManifest().getRevisionIndex(manifest); - } - } catch (HgInvalidControlFileException ex) { - // FIXME need to propagate the error up to client - repo.getContext().getLog().error(getClass(), ex, null); + Nodeid manifest = repo.getChangelog().range(u, u).get(0).manifest(); + // TODO calculate those missing effectively (e.g. cache and sort nodeids to speed lookup + // right away in the #next (may refactor ParentWalker's sequential and sorted into dedicated helper and reuse here) + if (manifest.isNull()) { + repo.getContext().getLog().dump(getClass(), Severity.Warn, "Changeset %d has no associated manifest entry", u); + // keep -1 in the changelog2manifest map. + } else { + changelog2manifest[u] = repo.getManifest().getRevisionIndex(manifest); } } } } + + /** + * Look up specified file in possibly multiple manifest revisions, collect file revision and flags. + */ + private static class FileLookupInspector implements RevlogStream.Inspector { + + private final Path filename; + private final byte[] filenameAsBytes; + private final IntMap csetIndex2FileRev; + private final IntMap csetIndex2Flags; + private final Inspector delegate; + + public FileLookupInspector(EncodingHelper eh, Path fileToLookUp, IntMap csetIndex2FileRevMap, IntMap csetIndex2FlagsMap) { + assert fileToLookUp != null; + // need at least one map for the inspector to make any sense + assert csetIndex2FileRevMap != null || csetIndex2FlagsMap != null; + filename = fileToLookUp; + filenameAsBytes = eh.toManifest(fileToLookUp.toString()); + delegate = null; + csetIndex2FileRev = csetIndex2FileRevMap; + csetIndex2Flags = csetIndex2FlagsMap; + } + + public FileLookupInspector(EncodingHelper eh, Path fileToLookUp, Inspector delegateInspector) { + assert fileToLookUp != null; + assert delegateInspector != null; + filename = fileToLookUp; + filenameAsBytes = eh.toManifest(fileToLookUp.toString()); + delegate = delegateInspector; + csetIndex2FileRev = null; + csetIndex2Flags = null; + } + + void walk(int manifestRevIndex, RevlogStream content) { + content.iterate(manifestRevIndex, manifestRevIndex, true, this); + } + + void walk(int[] manifestRevIndexes, RevlogStream content) { + content.iterate(manifestRevIndexes, true, this); + } + + public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + byte b; + while (!data.isEmpty() && (b = data.readByte()) != '\n') { + if (b != 0) { + bos.write(b); + } else { + byte[] byteArray = bos.toByteArray(); + bos.reset(); + if (Arrays.equals(filenameAsBytes, byteArray)) { + Nodeid fileRev = null; + Flags flags = null; + if (csetIndex2FileRev != null || delegate != null) { + byte[] nid = new byte[40]; + data.readBytes(nid, 0, 40); + fileRev = Nodeid.fromAscii(nid, 0, 40); + } else { + data.skip(40); + } + if (csetIndex2Flags != null || delegate != null) { + while (!data.isEmpty() && (b = data.readByte()) != '\n') { + bos.write(b); + } + if (bos.size() == 0) { + flags = Flags.RegularFile; + } else { + flags = Flags.parse(bos.toByteArray(), 0, bos.size()); + } + + } + if (delegate != null) { + assert flags != null; + assert fileRev != null; + delegate.begin(revisionNumber, Nodeid.fromBinary(nodeid, 0), linkRevision); + delegate.next(fileRev, filename, flags); + delegate.end(revisionNumber); + + } else { + if (csetIndex2FileRev != null) { + csetIndex2FileRev.put(linkRevision, fileRev); + } + if (csetIndex2Flags != null) { + csetIndex2Flags.put(linkRevision, flags); + } + } + break; + } else { + data.skip(40); + } + // else skip to the end of line + while (!data.isEmpty() && (b = data.readByte()) != '\n') + ; + } + } + } catch (IOException ex) { + throw new HgInvalidControlFileException("Failed reading manifest", ex, null); + } + } + } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgMergeState.java --- a/src/org/tmatesoft/hg/repo/HgMergeState.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgMergeState.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -27,19 +27,17 @@ import java.util.Collections; import java.util.List; -import org.tmatesoft.hg.core.HgBadStateException; import org.tmatesoft.hg.core.HgFileRevision; -import org.tmatesoft.hg.core.HgInvalidControlFileException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.internal.ManifestRevision; import org.tmatesoft.hg.internal.Pool; import org.tmatesoft.hg.util.Pair; import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.PathPool; import org.tmatesoft.hg.util.PathRewrite; /** - * + * Access to repository's merge state + * * @author Artem Tikhomirov * @author TMate Software Ltd. */ @@ -92,7 +90,11 @@ repo = hgRepo; } - public void refresh() throws HgInvalidControlFileException { + /** + * Update our knowledge about repository's merge state + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public void refresh() throws HgRuntimeException { entries = null; // it's possible there are two parents but no merge/state, we shall report this case as 'merging', with proper // first and second parent values @@ -108,9 +110,8 @@ } try { ArrayList result = new ArrayList(); - // FIXME need to settle use of Pool and PathPool - // latter is pool that can create objects on demand, former is just cache - PathPool pathPool = new PathPool(new PathRewrite.Empty()); + // pipe (already normalized) names from mergestate through same pool of filenames as use manifest revisions + Path.Source pathPool = new Path.SimpleSource(new PathRewrite.Empty(), fnamePool); final ManifestRevision m1 = new ManifestRevision(nodeidPool, fnamePool); final ManifestRevision m2 = new ManifestRevision(nodeidPool, fnamePool); if (!wcp2.isNull()) { @@ -127,12 +128,12 @@ Path p1fname = pathPool.path(r[3]); Nodeid nidP1 = m1.nodeid(p1fname); Nodeid nidCA = nodeidPool.unify(Nodeid.fromAscii(r[5])); - HgFileRevision p1 = new HgFileRevision(repo, nidP1, p1fname); + HgFileRevision p1 = new HgFileRevision(repo, nidP1, m1.flags(p1fname), p1fname); HgFileRevision ca; if (nidCA == nidP1 && r[3].equals(r[4])) { ca = p1; } else { - ca = new HgFileRevision(repo, nidCA, pathPool.path(r[4])); + ca = new HgFileRevision(repo, nidCA, null, pathPool.path(r[4])); } HgFileRevision p2; if (!wcp2.isNull() || !r[6].equals(r[4])) { @@ -142,7 +143,7 @@ assert false : "There's not enough information (or I don't know where to look) in merge/state to find out what's the second parent"; nidP2 = NULL; } - p2 = new HgFileRevision(repo, nidP2, p2fname); + p2 = new HgFileRevision(repo, nidP2, m2.flags(p2fname), p2fname); } else { // no second parent known. no idea what to do here, assume linear merge, use common ancestor as parent p2 = ca; @@ -153,14 +154,13 @@ } else if ("r".equals(r[1])) { k = Kind.Resolved; } else { - throw new HgBadStateException(r[1]); + throw new HgInvalidStateException(String.format("Unknown merge kind %s", r[1])); } Entry e = new Entry(k, pathPool.path(r[0]), p1, p2, ca); result.add(e); } entries = result.toArray(new Entry[result.size()]); br.close(); - pathPool.clear(); } catch (IOException ex) { throw new HgInvalidControlFileException("Merge state read failed", ex, f); } @@ -184,7 +184,7 @@ */ public boolean isStale() { if (wcp1 == null) { - throw new HgBadStateException("Call #refresh() first"); + refresh(); } return !stateParent.isNull() /*there's merge state*/ && !wcp1.equals(stateParent) /*and it doesn't match*/; } @@ -192,11 +192,12 @@ /** * It's possible for a repository to be in a 'merging' state (@see {@link #isMerging()} without any * conflict to resolve (no merge state information file). + * * @return first parent of the working copy, never null */ public Nodeid getFirstParent() { if (wcp1 == null) { - throw new HgBadStateException("Call #refresh() first"); + refresh(); } return wcp1; } @@ -206,7 +207,7 @@ */ public Nodeid getSecondParent() { if (wcp2 == null) { - throw new HgBadStateException("Call #refresh() first"); + refresh(); } return wcp2; } @@ -216,7 +217,7 @@ */ public Nodeid getStateParent() { if (stateParent == null) { - throw new HgBadStateException("Call #refresh() first"); + refresh(); } return stateParent; } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgParentChildMap.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/repo/HgParentChildMap.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,242 @@ +/* + * 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 + * 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.repo; + +import static org.tmatesoft.hg.repo.HgRepository.TIP; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; + +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.Revlog.ParentInspector; + +/** + * Helper class to deal with parent-child relationship between revisions en masse. + * Works in terms of {@link Nodeid nodeids}, there's no need to deal with revision indexes. + * For a given revision, answers questions like "who's my parent and what are my immediate children". + * + *

Comes handy when multiple revisions are analyzed and distinct {@link Revlog#parents(int, int[], byte[], byte[])} + * queries are ineffective. + * + *

Next code snippet shows typical use: + *

+ *   HgChangelog clog = repo.getChangelog();
+ *   ParentWalker<HgChangelog> pw = new ParentWalker<HgChangelog>(clog);
+ *   pw.init();
+ *   
+ *   Nodeid me = Nodeid.fromAscii("...");
+ *   List immediateChildren = pw.directChildren(me);
+ * 
+ * + * + *

Perhaps, later may add alternative way to access (and reuse) map instance, Revlog#getParentWalker(), + * that instantiates and initializes ParentWalker, and keep SoftReference to allow its reuse. + * + * @see HgRevisionMap + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public final class HgParentChildMap implements ParentInspector { + + + private Nodeid[] sequential; // natural repository order, childrenOf rely on ordering + private Nodeid[] sorted; // for binary search + private int[] sorted2natural; + private Nodeid[] firstParent; + private Nodeid[] secondParent; + private final T revlog; + + // Nodeid instances shall be shared between all arrays + + public HgParentChildMap(T owner) { + revlog = owner; + } + + public HgRepository getRepo() { + return revlog.getRepo(); + } + + public void next(int revisionNumber, Nodeid revision, int parent1Revision, int parent2Revision, Nodeid nidParent1, Nodeid nidParent2) { + if (parent1Revision >= revisionNumber || parent2Revision >= revisionNumber) { + throw new IllegalStateException(); // sanity, revisions are sequential + } + int ix = revisionNumber; + sequential[ix] = sorted[ix] = revision; + if (parent1Revision != -1) { + firstParent[ix] = sequential[parent1Revision]; + } + if (parent2Revision != -1) { // revlog of DataAccess.java has p2 set when p1 is -1 + secondParent[ix] = sequential[parent2Revision]; + } + } + + public void init() throws HgInvalidControlFileException { + final int revisionCount = revlog.getRevisionCount(); + firstParent = new Nodeid[revisionCount]; + // TODO [post 1.0] Branches/merges are less frequent, and most of secondParent would be -1/null, hence + // IntMap might be better alternative here, but need to carefully analyze (test) whether this brings + // real improvement (IntMap has 2n capacity, and element lookup is log(n) instead of array's constant) + secondParent = new Nodeid[revisionCount]; + // + sequential = new Nodeid[revisionCount]; + sorted = new Nodeid[revisionCount]; + revlog.indexWalk(0, TIP, this); + Arrays.sort(sorted); + sorted2natural = new int[revisionCount]; + for (int i = 0; i < revisionCount; i++) { + Nodeid n = sequential[i]; + int x = Arrays.binarySearch(sorted, n); + assertSortedIndex(x); + sorted2natural[x] = i; + } + } + + private void assertSortedIndex(int x) { + if (x < 0) { + throw new HgInvalidStateException(String.format("Bad index", x)); + } + } + + /** + * Tells whether supplied revision is from the walker's associated revlog. + * Note, {@link Nodeid#NULL}, although implicitly present as parent of a first revision, is not recognized as known. + * @param nid revision to check, not null + * @return true if revision matches any revision in this revlog + */ + public boolean knownNode(Nodeid nid) { + return Arrays.binarySearch(sorted, nid) >= 0; + } + + /** + * null if none. only known nodes (as per #knownNode) are accepted as arguments + */ + public Nodeid firstParent(Nodeid nid) { + int x = Arrays.binarySearch(sorted, nid); + assertSortedIndex(x); + int i = sorted2natural[x]; + return firstParent[i]; + } + + // never null, Nodeid.NULL if none known + public Nodeid safeFirstParent(Nodeid nid) { + Nodeid rv = firstParent(nid); + return rv == null ? Nodeid.NULL : rv; + } + + public Nodeid secondParent(Nodeid nid) { + int x = Arrays.binarySearch(sorted, nid); + assertSortedIndex(x); + int i = sorted2natural[x]; + return secondParent[i]; + } + + public Nodeid safeSecondParent(Nodeid nid) { + Nodeid rv = secondParent(nid); + return rv == null ? Nodeid.NULL : rv; + } + + public boolean appendParentsOf(Nodeid nid, Collection c) { + int x = Arrays.binarySearch(sorted, nid); + assertSortedIndex(x); + int i = sorted2natural[x]; + Nodeid p1 = firstParent[i]; + boolean modified = false; + if (p1 != null) { + modified = c.add(p1); + } + Nodeid p2 = secondParent[i]; + if (p2 != null) { + modified = c.add(p2) || modified; + } + return modified; + } + + // XXX alternative (and perhaps more reliable) approach would be to make a copy of allNodes and remove + // nodes, their parents and so on. + + // @return ordered collection of all children rooted at supplied nodes. Nodes shall not be descendants of each other! + // Nodeids shall belong to this revlog + public List childrenOf(List roots) { + HashSet parents = new HashSet(); + LinkedList result = new LinkedList(); + int earliestRevision = Integer.MAX_VALUE; + assert sequential.length == firstParent.length && firstParent.length == secondParent.length; + // first, find earliest index of roots in question, as there's no sense + // to check children among nodes prior to branch's root node + for (Nodeid r : roots) { + int x = Arrays.binarySearch(sorted, r); + assertSortedIndex(x); + int i = sorted2natural[x]; + if (i < earliestRevision) { + earliestRevision = i; + } + parents.add(sequential[i]); // add canonical instance in hope equals() is bit faster when can do a == + } + for (int i = earliestRevision + 1; i < sequential.length; i++) { + if (parents.contains(firstParent[i]) || parents.contains(secondParent[i])) { + parents.add(sequential[i]); // to find next child + result.add(sequential[i]); + } + } + return result; + } + + /** + * @return revisions that have supplied revision as their immediate parent + */ + public List directChildren(Nodeid nid) { + LinkedList result = new LinkedList(); + int x = Arrays.binarySearch(sorted, nid); + assertSortedIndex(x); + nid = sorted[x]; // canonical instance + int start = sorted2natural[x]; + for (int i = start + 1; i < sequential.length; i++) { + if (nid == firstParent[i] || nid == secondParent[i]) { + result.add(sequential[i]); + } + } + return result; + } + + /** + * @param nid possibly parent node, shall be {@link #knownNode(Nodeid) known} in this revlog. + * @return true if there's any node in this revlog that has specified node as one of its parents. + */ + public boolean hasChildren(Nodeid nid) { + int x = Arrays.binarySearch(sorted, nid); + assertSortedIndex(x); + int i = sorted2natural[x]; + assert firstParent.length == secondParent.length; // just in case later I implement sparse array for secondParent + assert firstParent.length == sequential.length; + // to use == instead of equals, take the same Nodeid instance we used to fill all the arrays. + final Nodeid canonicalNode = sequential[i]; + i++; // no need to check node itself. child nodes may appear in sequential only after revision in question + for (; i < sequential.length; i++) { + // TODO [post 1.0] likely, not very effective. + // May want to optimize it with another (Tree|Hash)Set, created on demand on first use, + // however, need to be careful with memory usage + if (firstParent[i] == canonicalNode || secondParent[i] == canonicalNode) { + return true; + } + } + return false; + } +} \ No newline at end of file diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgRemoteRepository.java --- a/src/org/tmatesoft/hg/repo/HgRemoteRepository.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgRemoteRepository.java Wed Jul 11 20:40:47 2012 +0200 @@ -16,6 +16,9 @@ */ package org.tmatesoft.hg.repo; +import static org.tmatesoft.hg.util.LogFacility.Severity.Info; + +import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -30,13 +33,16 @@ import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; import java.util.zip.InflaterInputStream; @@ -47,11 +53,11 @@ import javax.net.ssl.X509TrustManager; import org.tmatesoft.hg.core.HgBadArgumentException; -import org.tmatesoft.hg.core.HgBadStateException; -import org.tmatesoft.hg.core.HgInvalidFileException; import org.tmatesoft.hg.core.HgRemoteConnectionException; +import org.tmatesoft.hg.core.HgRepositoryNotFoundException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.core.SessionContext; +import org.tmatesoft.hg.internal.PropertyMarshal; /** * WORK IN PROGRESS, DO NOT USE @@ -69,15 +75,15 @@ private final boolean debug; private HgLookup lookupHelper; private final SessionContext sessionContext; - + private Set remoteCapabilities; + HgRemoteRepository(SessionContext ctx, URL url) throws HgBadArgumentException { if (url == null || ctx == null) { throw new IllegalArgumentException(); } this.url = url; sessionContext = ctx; - Object debugProp = ctx.getProperty("hg4j.remote.debug", false); - debug = debugProp instanceof Boolean ? ((Boolean) debugProp).booleanValue() : Boolean.parseBoolean(String.valueOf(debugProp)); + debug = new PropertyMarshal(ctx).getBoolean("hg4j.remote.debug", false); if ("https".equals(url.getProtocol())) { try { sslContext = SSLContext.getInstance("SSL"); @@ -112,7 +118,7 @@ ai = tempNode.get("xxx", null); tempNode.removeNode(); } catch (BackingStoreException ex) { - sessionContext.getLog().info(getClass(), ex, null); + sessionContext.getLog().dump(getClass(), Info, ex, null); // IGNORE } authInfo = ai; @@ -122,11 +128,49 @@ } public boolean isInvalid() throws HgRemoteConnectionException { - // say hello to server, check response - if (Boolean.FALSE.booleanValue()) { - throw HgRepository.notImplemented(); + if (remoteCapabilities == null) { + remoteCapabilities = new HashSet(); + // say hello to server, check response + try { + URL u = new URL(url, url.getPath() + "?cmd=hello"); + HttpURLConnection c = setupConnection(u.openConnection()); + c.connect(); + if (debug) { + dumpResponseHeader(u, c); + } + BufferedReader r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII")); + String line = r.readLine(); + c.disconnect(); + final String capsPrefix = "capabilities:"; + if (line == null || !line.startsWith(capsPrefix)) { + // for whatever reason, some servers do not respond to hello command (e.g. svnkit) + // but respond to 'capabilities' instead. Try it. + // TODO [post-1.0] tests needed + u = new URL(url, url.getPath() + "?cmd=capabilities"); + c = setupConnection(u.openConnection()); + c.connect(); + if (debug) { + dumpResponseHeader(u, c); + } + r = new BufferedReader(new InputStreamReader(c.getInputStream(), "US-ASCII")); + line = r.readLine(); + c.disconnect(); + if (line == null || line.trim().length() == 0) { + return true; + } + } else { + line = line.substring(capsPrefix.length()).trim(); + } + String[] caps = line.split("\\s"); + remoteCapabilities.addAll(Arrays.asList(caps)); + c.disconnect(); + } catch (MalformedURLException ex) { + throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("hello").setServerInfo(getLocation()); + } catch (IOException ex) { + throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("hello").setServerInfo(getLocation()); + } } - return false; // FIXME implement remote repository hello/check + return remoteCapabilities.isEmpty(); } /** @@ -153,7 +197,7 @@ } InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); StreamTokenizer st = new StreamTokenizer(is); - st.ordinaryChars('0', '9'); + st.ordinaryChars('0', '9'); // wordChars performs |, hence need to 0 first st.wordChars('0', '9'); st.eolIsSignificant(false); LinkedList parseResult = new LinkedList(); @@ -234,12 +278,12 @@ assert currRange == null; assert currRangeList == null; if (!rangeItr.hasNext()) { - throw new HgBadStateException(); + throw new HgInvalidStateException("Internal error"); // TODO revisit-1.1 } rv.put(rangeItr.next(), Collections.emptyList()); } else { if (currRange == null || currRangeList == null) { - throw new HgBadStateException(); + throw new HgInvalidStateException("Internal error"); // TODO revisit-1.1 } // indicate next range value is needed currRange = null; @@ -250,7 +294,7 @@ possiblyEmptyNextLine = false; if (currRange == null) { if (!rangeItr.hasNext()) { - throw new HgBadStateException(); + throw new HgInvalidStateException("Internal error"); // TODO revisit-1.1 } currRange = rangeItr.next(); currRangeList = new LinkedList(); @@ -328,7 +372,7 @@ * (there's no header like HG10?? with the server output, though, * as one may expect according to http://mercurial.selenic.com/wiki/BundleFormat) */ - public HgBundle getChanges(List roots) throws HgRemoteConnectionException, HgInvalidFileException { + public HgBundle getChanges(List roots) throws HgRemoteConnectionException, HgRuntimeException { List _roots = roots.isEmpty() ? Collections.singletonList(Nodeid.NULL) : roots; StringBuilder sb = new StringBuilder(20 + _roots.size() * 41); sb.append("roots="); @@ -357,6 +401,8 @@ throw new HgRemoteConnectionException("Bad URL", ex).setRemoteCommand("changegroup").setServerInfo(getLocation()); } catch (IOException ex) { throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("changegroup").setServerInfo(getLocation()); + } catch (HgRepositoryNotFoundException ex) { + throw new HgRemoteConnectionException("Communication failure", ex).setRemoteCommand("changegroup").setServerInfo(getLocation()); } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgRepoConfig.java --- a/src/org/tmatesoft/hg/repo/HgRepoConfig.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgRepoConfig.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -23,18 +23,14 @@ import java.util.Map; import org.tmatesoft.hg.internal.ConfigFile; -import org.tmatesoft.hg.internal.Experimental; import org.tmatesoft.hg.util.Pair; /** - * WORK IN PROGRESS - * * Repository-specific configuration. * * @author Artem Tikhomirov * @author TMate Software Ltd. */ -@Experimental(reason="WORK IN PROGRESS") public final class HgRepoConfig /*implements RepoChangeListener, perhaps, also RepoChangeNotifier? */{ /*ease access for inner classes*/ final ConfigFile config; diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgRepository.java --- a/src/org/tmatesoft/hg/repo/HgRepository.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgRepository.java Wed Jul 11 20:40:47 2012 +0200 @@ -16,6 +16,8 @@ */ package org.tmatesoft.hg.repo; +import static org.tmatesoft.hg.util.LogFacility.Severity.*; + import java.io.File; import java.io.IOException; import java.io.StringReader; @@ -25,14 +27,11 @@ import java.util.HashMap; import java.util.List; -import org.tmatesoft.hg.core.HgDataStreamException; -import org.tmatesoft.hg.core.HgInvalidControlFileException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.core.SessionContext; import org.tmatesoft.hg.internal.ByteArrayChannel; import org.tmatesoft.hg.internal.ConfigFile; 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; @@ -40,7 +39,6 @@ import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.Pair; import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.PathPool; import org.tmatesoft.hg.util.PathRewrite; import org.tmatesoft.hg.util.ProgressSupport; @@ -54,11 +52,40 @@ */ public final class HgRepository { - // if new constants added, consider fixing HgInternals#wrongRevisionIndex - public static final int TIP = -3; - public static final int BAD_REVISION = Integer.MIN_VALUE; - public static final int WORKING_COPY = -2; + // IMPORTANT: if new constants added, consider fixing HgInternals#wrongRevisionIndex and HgInvalidRevisionException#getMessage + + /** + * Revision index constant to indicate most recent revision + */ + public static final int TIP = -3; // XXX TIP_REVISION? + + /** + * Revision index constant to indicate invalid revision index value. + * Primary use is default/uninitialized values where user input is expected and as return value where + * an exception (e.g. {@link HgInvalidRevisionException}) is not desired + */ + public static final int BAD_REVISION = Integer.MIN_VALUE; // XXX INVALID_REVISION? + + /** + * Revision index constant to indicate working copy + */ + public static final int WORKING_COPY = -2; // XXX WORKING_COPY_REVISION? + /** + * Constant ({@value #NO_REVISION}) to indicate revision absence or a fictitious revision of an empty repository. + * + *

Revision absence is vital e.g. for missing parent from {@link HgChangelog#parents(int, int[], byte[], byte[])} call and + * to report cases when changeset records no corresponding manifest + * revision {@link HgManifest#walk(int, int, org.tmatesoft.hg.repo.HgManifest.Inspector)}. + * + *

Use as imaginary revision/empty repository is handy as an argument (contrary to {@link #BAD_REVISION}) + * e.g in a status operation to visit changes from the very beginning of a repository. + */ + public static final int NO_REVISION = -1; + + /** + * Name of the primary branch, "default". + */ public static final String DEFAULT_BRANCH_NAME = "default"; // temp aux marker method @@ -89,6 +116,14 @@ private HgIgnore ignore; private HgRepoConfig repoConfig; + /* + * TODO [post-1.0] move to a better place, e.g. WorkingCopy container that tracks both dirstate and branches + * (and, perhaps, undo, lastcommit and other similar information), and is change listener so that we don't need to + * worry about this cached value become stale + */ + private String wcBranch; + + HgRepository(String repositoryPath) { repoDir = null; workingDir = null; @@ -100,7 +135,10 @@ impl = null; } - HgRepository(SessionContext ctx, String repositoryPath, File repositoryRoot) { + /** + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + HgRepository(SessionContext ctx, String repositoryPath, File repositoryRoot) throws HgRuntimeException { assert ".hg".equals(repositoryRoot.getName()) && repositoryRoot.isDirectory(); assert repositoryPath != null; assert repositoryRoot != null; @@ -155,7 +193,7 @@ tags = new HgTags(this); HgDataFile hgTags = getFileNode(".hgtags"); if (hgTags.exists()) { - for (int i = 0; i <= hgTags.getLastRevision(); i++) { // FIXME in fact, would be handy to have walk(start,end) + for (int i = 0; i <= hgTags.getLastRevision(); i++) { // TODO post-1.0 in fact, would be handy to have walk(start,end) // method for data files as well, though it looks odd. try { ByteArrayChannel sink = new ByteArrayChannel(); @@ -164,15 +202,12 @@ tags.readGlobal(new StringReader(content)); } catch (CancelledException ex) { // IGNORE, can't happen, we did not configure cancellation - getContext().getLog().debug(getClass(), ex, null); - } catch (HgDataStreamException ex) { - getContext().getLog().error(getClass(), ex, null); - // FIXME need to react + getContext().getLog().dump(getClass(), Debug, ex, null); } catch (IOException ex) { // UnsupportedEncodingException can't happen (UTF8) // only from readGlobal. Need to reconsider exceptions thrown from there: // BufferedReader wraps String and unlikely to throw IOException, perhaps, log is enough? - getContext().getLog().error(getClass(), ex, null); + getContext().getLog().dump(getClass(), Error, ex, null); // XXX need to decide what to do this. failure to read single revision shall not break complete cycle } } @@ -184,7 +219,7 @@ file2read = new File(repoDir, "localtags"); tags.readLocal(file2read); } catch (IOException ex) { - getContext().getLog().error(getClass(), ex, null); + getContext().getLog().dump(getClass(), Error, ex, null); throw new HgInvalidControlFileException("Failed to read tags", ex, file2read); } } @@ -199,7 +234,6 @@ return branches; } - @Experimental(reason="Perhaps, shall not cache instance, and provide loadMergeState as it may change often") public HgMergeState getMergeState() { if (mergeState == null) { mergeState = new HgMergeState(this); @@ -246,7 +280,10 @@ * @throws HgInvalidControlFileException if attempt to read branch name failed. */ public String getWorkingCopyBranchName() throws HgInvalidControlFileException { - return HgDirstate.readBranch(this); + if (wcBranch == null) { + wcBranch = HgDirstate.readBranch(this, new File(repoDir, "branch")); + } + return wcBranch; } /** @@ -277,7 +314,7 @@ repoConfig = new HgRepoConfig(configFile); } catch (IOException ex) { String m = "Errors while reading user configuration file"; - getContext().getLog().warn(getClass(), ex, m); + getContext().getLog().dump(getClass(), Warn, ex, m); return new HgRepoConfig(new ConfigFile()); // empty config, do not cache, allow to try once again //throw new HgInvalidControlFileException(m, ex, null); } @@ -290,14 +327,14 @@ return repoDir; } - // FIXME remove once NPE in HgWorkingCopyStatusCollector.areTheSame is solved /*package-local, debug*/String getStoragePath(HgDataFile df) { + // may come handy for debug return dataPathHelper.rewrite(df.getPath().toString()).toString(); } // 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) // XXX consider passing Path pool or factory to produce (shared) Path instead of Strings - /*package-local*/ final HgDirstate loadDirstate(PathPool pathPool) throws HgInvalidControlFileException { + /*package-local*/ final HgDirstate loadDirstate(Path.Source pathFactory) throws HgInvalidControlFileException { PathRewrite canonicalPath = null; if (!impl.isCaseSensitiveFileSystem()) { canonicalPath = new PathRewrite() { @@ -307,7 +344,7 @@ } }; } - HgDirstate ds = new HgDirstate(this, new File(repoDir, "dirstate"), pathPool, canonicalPath); + HgDirstate ds = new HgDirstate(this, new File(repoDir, "dirstate"), pathFactory, canonicalPath); ds.read(impl.buildFileNameEncodingHelper()); return ds; } @@ -324,11 +361,11 @@ try { final List errors = ignore.read(ignoreFile); if (errors != null) { - getContext().getLog().warn(getClass(), "Syntax errors parsing .hgignore:\n%s", Internals.join(errors, ",\n")); + getContext().getLog().dump(getClass(), Warn, "Syntax errors parsing .hgignore:\n%s", Internals.join(errors, ",\n")); } } catch (IOException ex) { final String m = "Error reading .hgignore file"; - getContext().getLog().warn(getClass(), ex, m); + getContext().getLog().dump(getClass(), Warn, ex, m); // throw new HgInvalidControlFileException(m, ex, ignoreFile); } } @@ -363,7 +400,7 @@ fake.deleteOnExit(); return new RevlogStream(dataAccess, fake); } catch (IOException ex) { - getContext().getLog().info(getClass(), ex, null); + getContext().getLog().dump(getClass(), Info, ex, null); } } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgRepositoryFiles.java --- a/src/org/tmatesoft/hg/repo/HgRepositoryFiles.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgRepositoryFiles.java Wed Jul 11 20:40:47 2012 +0200 @@ -16,18 +16,18 @@ */ package org.tmatesoft.hg.repo; -import org.tmatesoft.hg.internal.Experimental; /** - * + * Names of some Mercurial configuration/service files. + * * @author Artem Tikhomirov * @author TMate Software Ltd. */ -@Experimental public enum HgRepositoryFiles { HgIgnore(".hgignore"), HgTags(".hgtags"), HgEol(".hgeol"), - Dirstate(".hg/dirstate"), HgLocalTags(".hg/localtags"); + Dirstate(".hg/dirstate"), HgLocalTags(".hg/localtags"), + HgSub(".hgsub"), HgSubstate(".hgsubstate"); private String fname; diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgRevisionMap.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/repo/HgRevisionMap.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,116 @@ +/* + * 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 + * 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.repo; + +import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; +import static org.tmatesoft.hg.repo.HgRepository.TIP; + +import java.util.Arrays; + +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.ArrayHelper; +import org.tmatesoft.hg.repo.Revlog.RevisionInspector; + +/** + * Effective int to Nodeid and vice versa translation. It's advised to use this class instead of + * multiple {@link Revlog#getRevisionIndex(Nodeid)} calls. Rule of thumb is 20+ calls (given + * initialization costs). It's also important to take into account memory consumption, for huge + * repositories use of this class may pay off only when accessing greatest fraction of all revisions. + * + *

Next code snippet shows instantiation and sample use: + *

+ *   RevisionMap clogMap = new RevisionMap(clog).init();
+ *   RevisionMap fileMap = new RevisionMap(fileNode).init();
+ *   
+ *   int fileRevIndex = 0;
+ *   Nodeid fileRev = fileMap.revision(fileRevIndex);
+ *   int csetRevIndex = fileNode.getChangesetRevisionIndex(fileRevIndex);
+ *   Nodeid csetRev = clogMap.revision(localCset);
+ *   changesetToNodeidMap.put(csetRev, fileRev);
+ * 
+ * + *

+ * {@link Revlog#getRevisionIndex(Nodeid)} with straightforward lookup approach performs O(n/2) + *

+ * {@link HgRevisionMap#revisionIndex(Nodeid)} is log(n), plus initialization is O(n) (just once). + * + * @see HgParentChildMap + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public final class HgRevisionMap implements RevisionInspector { + /* + * in fact, initialization is much slower as it instantiates Nodeids, while #getRevisionIndex + * compares directly against byte buffer. Measuring cpython with 70k+ gives 3 times difference (47 vs 171) + * for complete changelog iteration. + */ + + /* + * XXX 3 * (x * 4) bytes. Can I do better? + * It seems, yes. Don't need to keep sorted, always can emulate it with indirect access to sequential through sorted2natural. + * i.e. instead sorted[mid].compareTo(toFind), do sequential[sorted2natural[mid]].compareTo(toFind) + */ + private Nodeid[] sequential; // natural repository order, childrenOf rely on ordering + private Nodeid[] sorted; // for binary search + private int[] sorted2natural; + private final T revlog; + + public HgRevisionMap(T owner) { + revlog = owner; + } + + public HgRepository getRepo() { + return revlog.getRepo(); + } + + public void next(int revisionIndex, Nodeid revision, int linkedRevision) { + sequential[revisionIndex] = sorted[revisionIndex] = revision; + } + + /** + * @return this for convenience. + */ + public HgRevisionMap init(/*XXX Pool to reuse nodeids, if possible. */) throws HgInvalidControlFileException{ + // XXX HgRepository.register((RepoChangeListener) this); // listen to changes in repo, re-init if needed? + final int revisionCount = revlog.getRevisionCount(); + sequential = new Nodeid[revisionCount]; + sorted = new Nodeid[revisionCount]; + revlog.indexWalk(0, TIP, this); + // next is alternative to Arrays.sort(sorted), and build sorted2natural looking up each element of sequential in sorted. + // the way sorted2natural was build is O(n*log n). + final ArrayHelper ah = new ArrayHelper(); + ah.sort(sorted); + // note, values in ArrayHelper#getReversed are 1-based indexes, not 0-based + sorted2natural = ah.getReverse(); + return this; + } + + public Nodeid revision(int revisionIndex) { + return sequential[revisionIndex]; + } + public int revisionIndex(Nodeid revision) { + if (revision == null || revision.isNull()) { + return BAD_REVISION; + } + int x = Arrays.binarySearch(sorted, revision); + if (x < 0) { + return BAD_REVISION; + } + return sorted2natural[x]-1; + } +} \ No newline at end of file diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgRuntimeException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/repo/HgRuntimeException.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,100 @@ +/* + * Copyright (c) 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 + * 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.repo; + +import org.tmatesoft.hg.core.HgException; +import org.tmatesoft.hg.core.HgLibraryFailureException; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.ExceptionInfo; +import org.tmatesoft.hg.util.Path; + +/** + * Almost any method in Hg4J low-level API (@link org.tmatesoft.hg.repo} may throw subclass of this exception + * to indicate unexpected state/condition encountered, flawed data or IO error. + * Since most cases can't be handled in a reasonable manner (other than catch all exceptions and tell client + * something went wrong), and propagating all possible exceptions up through API is dubious task, low-level + * exceptions are made runtime, rooting at this single class. + * + *

Hi-level api, {@link org.tmatesoft.hg.core}, where interaction with user-supplied values is more explicit, + * follows different exception strategy, namely checked exceptions rooted at {@link HgException}. + * + * @see HgException + * @see HgLibraryFailureException + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@SuppressWarnings("serial") +public abstract class HgRuntimeException extends RuntimeException { + + protected final ExceptionInfo details = new ExceptionInfo(this); + + protected HgRuntimeException(String reason, Throwable cause) { + super(reason, cause); + } + + /** + * @return {@link HgRepository#BAD_REVISION} unless revision index was set during exception instantiation + */ + public int getRevisionIndex() { + return details.getRevisionIndex(); + } + + public HgRuntimeException setRevisionIndex(int rev) { + return details.setRevisionIndex(rev); + } + + public boolean isRevisionIndexSet() { + return details.isRevisionIndexSet(); + } + + + /** + * @return non-null when revision was supplied at construction time + */ + public Nodeid getRevision() { + return details.getRevision(); + } + + public HgRuntimeException setRevision(Nodeid r) { + return details.setRevision(r); + } + + public boolean isRevisionSet() { + return details.isRevisionSet(); + } + + /** + * @return non-null only if file name was set at construction time + */ + public Path getFileName() { + return details.getFileName(); + } + + public HgRuntimeException setFileName(Path name) { + return details.setFileName(name); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(super.toString()); + sb.append(' '); + sb.append('('); + details.appendDetails(sb); + sb.append(')'); + return sb.toString(); + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgStatusCollector.java --- a/src/org/tmatesoft/hg/repo/HgStatusCollector.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgStatusCollector.java Wed Jul 11 20:40:47 2012 +0200 @@ -16,8 +16,7 @@ */ package org.tmatesoft.hg.repo; -import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; -import static org.tmatesoft.hg.repo.HgRepository.TIP; +import static org.tmatesoft.hg.repo.HgRepository.*; import java.util.Collection; import java.util.Collections; @@ -27,21 +26,18 @@ import java.util.Map; import java.util.TreeSet; -import org.tmatesoft.hg.core.HgBadStateException; -import org.tmatesoft.hg.core.HgDataStreamException; -import org.tmatesoft.hg.core.HgException; -import org.tmatesoft.hg.core.HgInvalidControlFileException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.internal.IntMap; import org.tmatesoft.hg.internal.ManifestRevision; import org.tmatesoft.hg.internal.Pool; +import org.tmatesoft.hg.util.CancelSupport; +import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.util.Convertor; import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.PathPool; -import org.tmatesoft.hg.util.PathRewrite; /** - * RevisionWalker? + * Collect status information for changes between two repository revisions. * * @author Artem Tikhomirov * @author TMate Software Ltd. @@ -55,7 +51,7 @@ // no cache limit, but with cached nodeids and filenames - 1730+ // cache limit 100 - 19+ minutes to process 10000, and still working (too long, stopped) private final int cacheMaxSize = 50; // do not keep too much manifest revisions - private PathPool pathPool; + private Convertor pathPool; private final Pool cacheNodes; private final Pool cacheFilenames; private final ManifestRevision emptyFakeState; @@ -80,7 +76,7 @@ private ManifestRevision get(int rev) throws HgInvalidControlFileException { ManifestRevision i = cache.get(rev); if (i == null) { - if (rev == -1) { + if (rev == NO_REVISION) { return emptyFakeState; } ensureCacheSize(); @@ -92,7 +88,7 @@ } private boolean cached(int revision) { - return cache.containsKey(revision) || revision == -1; + return cache.containsKey(revision) || revision == NO_REVISION; } private void ensureCacheSize() { @@ -114,13 +110,13 @@ for (int x = minRev, i = 0; x <= maxRev; i++, x++) { revisionsToCollect[i] = x; } - repo.getManifest().walk(new HgManifest.Inspector2() { + repo.getManifest().walk(new HgManifest.Inspector() { private ManifestRevision delegate; private boolean cacheHit; // range may include revisions we already know about, do not re-create them public boolean begin(int manifestRevision, Nodeid nid, int changelogRevision) { assert delegate == null; - if (cache.containsKey(changelogRevision)) { // don't need to check emptyFakeState hit as revision never -1 here + if (cache.containsKey(changelogRevision)) { // don't need to check emptyFakeState hit as revision never NO_REVISION here cacheHit = true; } else { cache.put(changelogRevision, delegate = new ManifestRevision(cacheNodes, cacheFilenames)); @@ -132,10 +128,6 @@ return true; } - public boolean next(Nodeid nid, String fname, String flags) { - throw new HgBadStateException(HgManifest.Inspector2.class.getName()); - } - public boolean next(Nodeid nid, Path fname, HgManifest.Flags flags) { if (!cacheHit) { delegate.next(nid, fname, flags); @@ -156,17 +148,23 @@ /*package-local*/ static ManifestRevision createEmptyManifestRevision() { ManifestRevision fakeEmptyRev = new ManifestRevision(null, null); - fakeEmptyRev.begin(-1, null, -1); - fakeEmptyRev.end(-1); + fakeEmptyRev.begin(NO_REVISION, null, NO_REVISION); + fakeEmptyRev.end(NO_REVISION); return fakeEmptyRev; } + /** + * Access specific manifest revision + * @param rev + * @return + * @throws HgInvalidControlFileException + */ /*package-local*/ ManifestRevision raw(int rev) throws HgInvalidControlFileException { return get(rev); } - /*package-local*/ PathPool getPathPool() { + /*package-local*/ Convertor getPathPool() { if (pathPool == null) { - pathPool = new PathPool(new PathRewrite.Empty()); + pathPool = cacheFilenames; } return pathPool; } @@ -174,8 +172,8 @@ /** * Allows sharing of a common path cache */ - public void setPathPool(PathPool pathPool) { - this.pathPool = pathPool; + public void setPathPool(Convertor pathConvertor) { + pathPool = pathConvertor; } /** @@ -211,30 +209,55 @@ detectCopies = detect; } - // hg status --change - public void change(int rev, HgStatusInspector inspector) throws /*FIXME HInvalidRevisionException,*/ HgInvalidControlFileException { - int[] parents = new int[2]; - repo.getChangelog().parents(rev, parents, null, null); - walk(parents[0], rev, inspector); + /** + * 'hg status --change REV' command counterpart. + * + * @throws CancelledException if operation execution was cancelled + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public void change(int revisionIndex, HgStatusInspector inspector) throws CancelledException, HgRuntimeException { + int p; + if (revisionIndex == 0) { + p = NO_REVISION; + } else { + int[] parents = new int[2]; + repo.getChangelog().parents(revisionIndex, parents, null, null); + // #parents call above is responsible for NO_REVISION + p = parents[0]; // hg --change alsways uses first parent, despite the fact there might be valid (-1, 18) pair of parents + } + walk(p, revisionIndex, inspector); } - // rev1 and rev2 are changelog revision numbers, argument order matters. - // Either rev1 or rev2 may be -1 to indicate comparison to empty repository (XXX this is due to use of - // parents in #change(), I believe. Perhaps, need a constant for this? Otherwise this hidden knowledge gets - // exposed to e.g. Record - public void walk(int rev1, int rev2, HgStatusInspector inspector) throws /*FIXME HInvalidRevisionException,*/ HgInvalidControlFileException { + /** + * Parameters rev1 and rev2 are changelog revision indexes, shall not be the same. Argument order matters. + * Either rev1 or rev2 may be {@link HgRepository#NO_REVISION} to indicate comparison to empty repository + * + * @param rev1 from changeset index, non-negative or {@link HgRepository#TIP} + * @param rev2 to changeset index, non-negative or {@link HgRepository#TIP} + * @param inspector callback for status information + * @throws CancelledException if operation execution was cancelled + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + * @throws IllegalArgumentException inspector other incorrect argument values + */ + public void walk(int rev1, int rev2, HgStatusInspector inspector) throws CancelledException, HgRuntimeException, IllegalArgumentException { if (rev1 == rev2) { throw new IllegalArgumentException(); } if (inspector == null) { throw new IllegalArgumentException(); } - final int lastManifestRevision = repo.getChangelog().getLastRevision(); + final int lastChangelogRevision = repo.getChangelog().getLastRevision(); if (rev1 == TIP) { - rev1 = lastManifestRevision; + rev1 = lastChangelogRevision; } if (rev2 == TIP) { - rev2 = lastManifestRevision; + rev2 = lastChangelogRevision; + } + if (rev1 != NO_REVISION && (HgInternals.wrongRevisionIndex(rev1) || rev1 == WORKING_COPY || rev1 == BAD_REVISION || rev1 > lastChangelogRevision)) { + throw new HgInvalidRevisionException(rev1); + } + if (rev2 != NO_REVISION && (HgInternals.wrongRevisionIndex(rev2) || rev2 == WORKING_COPY || rev2 == BAD_REVISION || rev2 > lastChangelogRevision)) { + throw new HgInvalidRevisionException(rev2); } if (inspector instanceof Record) { ((Record) inspector).init(rev1, rev2, this); @@ -260,18 +283,20 @@ // which going to be read anyway if (need1) { minRev = rev1; - maxRev = rev1 < lastManifestRevision-5 ? rev1+5 : lastManifestRevision; + maxRev = rev1 < lastChangelogRevision-5 ? rev1+5 : lastChangelogRevision; initCacheRange(minRev, maxRev); } if (need2) { minRev = rev2; - maxRev = rev2 < lastManifestRevision-5 ? rev2+5 : lastManifestRevision; + maxRev = rev2 < lastChangelogRevision-5 ? rev2+5 : lastChangelogRevision; initCacheRange(minRev, maxRev); } } r1 = get(rev1); r2 = get(rev2); + final CancelSupport cs = CancelSupport.Factory.get(inspector); + TreeSet r1Files = new TreeSet(r1.files()); for (Path r2fname : r2.files()) { if (!scope.accept(r2fname)) { @@ -287,40 +312,58 @@ } else { inspector.modified(r2fname); } + cs.checkCancelled(); } else { try { Path copyTarget = r2fname; Path copyOrigin = detectCopies ? getOriginIfCopy(repo, copyTarget, r1Files, rev1) : null; if (copyOrigin != null) { - inspector.copied(getPathPool().path(copyOrigin) /*pipe through pool, just in case*/, copyTarget); + inspector.copied(getPathPool().mangle(copyOrigin) /*pipe through pool, just in case*/, copyTarget); } else { inspector.added(copyTarget); } - } catch (HgException ex) { + } catch (HgInvalidFileException ex) { // record exception to a mediator and continue, // for a single file not to be irresolvable obstacle for a status operation inspector.invalid(r2fname, ex); } + cs.checkCancelled(); } } for (Path r1fname : r1Files) { if (scope.accept(r1fname)) { inspector.removed(r1fname); + cs.checkCancelled(); } } } - public Record status(int rev1, int rev2) throws /*FIXME HInvalidRevisionException,*/ HgInvalidControlFileException { + /** + * Collects status between two revisions, changes from rev1 up to rev2. + * + * @param rev1 from changeset index + * @param rev2 to changeset index + * @return information object that describes change between the revisions + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public Record status(int rev1, int rev2) throws HgInvalidRevisionException, HgInvalidControlFileException { Record rv = new Record(); - walk(rev1, rev2, rv); + try { + walk(rev1, rev2, rv); + } catch (CancelledException ex) { + // can't happen as long our Record class doesn't implement CancelSupport + HgInvalidStateException t = new HgInvalidStateException("Internal error"); + t.initCause(ex); + throw t; + } return rv; } - /*package-local*/static Path getOriginIfCopy(HgRepository hgRepo, Path fname, Collection originals, int originalChangelogRevision) throws HgDataStreamException, HgInvalidControlFileException { + /*package-local*/static Path getOriginIfCopy(HgRepository hgRepo, Path fname, Collection originals, int originalChangelogRevision) throws HgInvalidFileException { HgDataFile df = hgRepo.getFileNode(fname); if (!df.exists()) { String msg = String.format("Didn't find file '%s' in the repo. Perhaps, bad storage name conversion?", fname); - throw new HgDataStreamException(fname, msg, null).setRevisionIndex(originalChangelogRevision); + throw new HgInvalidFileException(msg, null).setFileName(fname).setRevisionIndex(originalChangelogRevision); } while (df.isCopy()) { Path original = df.getCopySourceName(); @@ -353,6 +396,7 @@ * from {@link #getAdded()}. */ public static class Record implements HgStatusInspector { + // NOTE, shall not implement CancelSupport, or methods that use it and don't expect this exception shall be changed private List modified, added, removed, clean, missing, unknown, ignored; private Map copied; private Map failures; diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgStatusInspector.java --- a/src/org/tmatesoft/hg/repo/HgStatusInspector.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgStatusInspector.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -16,6 +16,7 @@ */ package org.tmatesoft.hg.repo; +import org.tmatesoft.hg.internal.Callback; import org.tmatesoft.hg.util.Path; /** @@ -24,13 +25,17 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ +@Callback public interface HgStatusInspector { void modified(Path fname); void added(Path fname); /** * This method is invoked for files that we added as a result of a copy/move operation, and it's the sole - * method invoked in this case, that is {@link #added(Path)} method is NOT invoked along with it. - * If copied files of no interest, it is implementation responsibility to delegate to this.added(fnameAdded) + * method invoked in this case, that is {@link #added(Path)} method is NOT invoked along with it. + * Note, however, {@link #removed(Path)} IS invoked for the removed file in all cases, regardless whether it's a mere rename or not. + *

The reason why it's not symmetrical ({@link #copied(Path, Path)} and {@link #removed(Path)} but not {@link #added(Path)}) is that Mercurial + * does it this way ('copy' is just an extra attribute for Added file), and we try to stay as close as possible here. + *

If copied files of no interest, it is implementation responsibility to delegate to this.added(fnameAdded) */ void copied(Path fnameOrigin, Path fnameAdded); void removed(Path fname); diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgSubrepoLocation.java --- a/src/org/tmatesoft/hg/repo/HgSubrepoLocation.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgSubrepoLocation.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -18,77 +18,117 @@ import java.io.File; -import org.tmatesoft.hg.core.HgBadStateException; -import org.tmatesoft.hg.core.HgInvalidFileException; -import org.tmatesoft.hg.internal.Experimental; +import org.tmatesoft.hg.core.HgRepositoryNotFoundException; +import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.util.Path; /** - * WORK IN PROGRESS, DO NOT USE + * Descriptor for subrepository location + * + * @see http://mercurial.selenic.com/wiki/Subrepository * @author Artem Tikhomirov * @author TMate Software Ltd. */ -@Experimental(reason="Work in progress") public class HgSubrepoLocation { private final HgRepository owner; private final Kind kind; private final Path location; private final String source; - private final String revInfo; + private final Nodeid revInfo; public enum Kind { Hg, SVN, Git, } - public HgSubrepoLocation(HgRepository parentRepo, String repoLocation, String actualLocation, Kind type, String revision) { + /** + * + * @param parentRepo + * @param repoLocation path, shall be valid directory (i.e. even if .hgsub doesn't specify trailing slash, this one shall) + * @param actualLocation + * @param type + * @param revision may be null + */ + /*package-local*/ HgSubrepoLocation(HgRepository parentRepo, Path repoLocation, String actualLocation, Kind type, Nodeid revision) { owner = parentRepo; - location = Path.create(repoLocation); + location = repoLocation; source = actualLocation; kind = type; revInfo = revision; } - // as defined in .hgsub, key value + /** + * Sub-repository's location within owning repository, always directory, path/to/nested. + *

+ * May differ from left-hand, key value from .hgsub if the latter doesn't include trailing slash, which is required + * for {@link Path} objects + * + * @return path to nested repository relative to owner's location + */ public Path getLocation() { return location; } - // value from .hgsub + /** + * Right-hand value from .hgsub, with [kind] stripped, if any. + * @return sub-repository's source + */ public String getSource() { return source; } + /** + * Sub-repository kind, either Mercurial, Subversion or Git + * @return one of predefined constants + */ public Kind getType() { return kind; } - public String getRevision() { + /** + * For a nested repository that has been committed at least once, returns + * its revision as known from .hgsubstate + * + *

Note, this revision belongs to the nested repository history, not that of owning repository. + * + * @return revision of the nested repository, or null if not yet committed + */ + public Nodeid getRevision() { return revInfo; } /** - * @return whether this sub repository is known only locally + * Answers whether this sub repository has ever been part of a commit of the owner repository + * + * @return true if owning repository records {@link #getRevision() revision} of this sub-repository */ public boolean isCommitted() { return revInfo != null; } /** - * @return true when there are local changes in the sub repository + * Answers whether there are local changes in the sub-repository, + * @return true if it's dirty */ public boolean hasChanges() { throw HgRepository.notImplemented(); } - -// public boolean isLocal() { -// } - + + /** + * Access repository that owns nested one described by this object + */ public HgRepository getOwner() { return owner; } - public HgRepository getRepo() throws HgInvalidFileException { + /** + * Access nested repository as a full-fledged Mercurial repository + * + * @return object to access sub-repository + * @throws HgRepositoryNotFoundException if failed to find repository + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public HgRepository getRepo() throws HgRepositoryNotFoundException { if (kind != Kind.Hg) { - throw new HgBadStateException(); + throw new HgInvalidStateException(String.format("Unsupported subrepository %s", kind)); } return new HgLookup().detect(new File(owner.getWorkingDir(), source)); } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgTags.java --- a/src/org/tmatesoft/hg/repo/HgTags.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgTags.java Wed Jul 11 20:40:47 2012 +0200 @@ -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,8 @@ */ package org.tmatesoft.hg.repo; +import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; + import java.io.BufferedReader; import java.io.File; import java.io.FileReader; @@ -29,7 +31,6 @@ import java.util.Map; import java.util.TreeMap; -import org.tmatesoft.hg.core.HgInvalidControlFileException; import org.tmatesoft.hg.core.Nodeid; /** @@ -108,7 +109,7 @@ continue; } if (line.length() < 40+2 /*nodeid, space and at least single-char tagname*/) { - repo.getContext().getLog().warn(getClass(), "Bad tags line: %s", line); + repo.getContext().getLog().dump(getClass(), Warn, "Bad tags line: %s", line); continue; } int spacePos = line.indexOf(' '); @@ -152,7 +153,7 @@ } } else { - repo.getContext().getLog().warn(getClass(), "Bad tags line: %s", line); + repo.getContext().getLog().dump(getClass(), Warn, "Bad tags line: %s", line); } } } @@ -184,14 +185,6 @@ } return rv; } - - /** - * @deprecated use {@link #getAllTags()} instead - */ - @Deprecated - public Map getTags() { - return getAllTags(); - } /** * All tag entries from the repository, for both active and removed tags diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java --- a/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java Wed Jul 11 20:40:47 2012 +0200 @@ -19,6 +19,7 @@ import static java.lang.Math.max; import static java.lang.Math.min; import static org.tmatesoft.hg.repo.HgRepository.*; +import static org.tmatesoft.hg.util.LogFacility.Severity.*; import java.io.File; import java.io.IOException; @@ -30,24 +31,24 @@ import java.util.Set; import java.util.TreeSet; -import org.tmatesoft.hg.core.HgException; -import org.tmatesoft.hg.core.HgInvalidControlFileException; -import org.tmatesoft.hg.core.HgInvalidFileException; import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.core.SessionContext; import org.tmatesoft.hg.internal.ByteArrayChannel; -import org.tmatesoft.hg.internal.Experimental; import org.tmatesoft.hg.internal.FilterByteChannel; +import org.tmatesoft.hg.internal.Internals; import org.tmatesoft.hg.internal.ManifestRevision; +import org.tmatesoft.hg.internal.PathPool; import org.tmatesoft.hg.internal.PathScope; import org.tmatesoft.hg.internal.Preview; import org.tmatesoft.hg.util.Adaptable; import org.tmatesoft.hg.util.ByteChannel; +import org.tmatesoft.hg.util.CancelSupport; import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.util.Convertor; import org.tmatesoft.hg.util.FileInfo; import org.tmatesoft.hg.util.FileIterator; import org.tmatesoft.hg.util.FileWalker; import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.PathPool; import org.tmatesoft.hg.util.PathRewrite; import org.tmatesoft.hg.util.RegularFileInfo; @@ -62,7 +63,7 @@ private final FileIterator repoWalker; private HgDirstate dirstate; private HgStatusCollector baseRevisionCollector; - private PathPool pathPool; + private Convertor pathPool; private ManifestRevision dirstateParentManifest; /** @@ -92,7 +93,7 @@ baseRevisionCollector = sc; } - /*package-local*/ PathPool getPathPool() { + /*package-local*/ Convertor getPathPool() { if (pathPool == null) { if (baseRevisionCollector == null) { pathPool = new PathPool(new PathRewrite.Empty()); @@ -103,8 +104,8 @@ return pathPool; } - public void setPathPool(PathPool pathPool) { - this.pathPool = pathPool; + public void setPathPool(Convertor pathConvertor) { + pathPool = pathConvertor; } /** @@ -113,7 +114,14 @@ */ public HgDirstate getDirstate() throws HgInvalidControlFileException { if (dirstate == null) { - dirstate = repo.loadDirstate(getPathPool()); + Convertor pp = getPathPool(); + Path.Source ps; + if (pp instanceof Path.Source) { + ps = (Path.Source) pp; + } else { + ps = new Path.SimpleSource(new PathRewrite.Empty(), pp); + } + dirstate = repo.loadDirstate(ps); } return dirstate; } @@ -137,7 +145,7 @@ private void initDirstateParentManifest() throws HgInvalidControlFileException { Nodeid dirstateParent = getDirstateImpl().parents().first(); if (dirstateParent.isNull()) { - dirstateParentManifest = baseRevisionCollector != null ? baseRevisionCollector.raw(-1) : HgStatusCollector.createEmptyManifestRevision(); + dirstateParentManifest = baseRevisionCollector != null ? baseRevisionCollector.raw(NO_REVISION) : HgStatusCollector.createEmptyManifestRevision(); } else { int changeloRevIndex = repo.getChangelog().getRevisionIndex(dirstateParent); dirstateParentManifest = getManifest(changeloRevIndex); @@ -150,11 +158,24 @@ return dirstateParentManifest; } - // may be invoked few times, TIP or WORKING_COPY indicate comparison shall be run against working copy parent - // NOTE, use of TIP constant requires certain care. TIP here doesn't mean latest cset, but actual working copy parent. - public void walk(int baseRevision, HgStatusInspector inspector) throws HgInvalidControlFileException, IOException { + /** + * Walk working copy, analyze status for each file found and missing. + * May be invoked few times. + * + *

There's no dedicated constant to for working copy parent, at least now. + * Use {@link HgRepository#WORKING_COPY} to indicate comparison + * shall be run against working copy parent. Although a bit confusing, single case doesn't + * justify a dedicated constant. + * + * @param baseRevision revision index to check against, or {@link HgRepository#WORKING_COPY}. Note, {@link HgRepository#TIP} is not supported. + * @param inspector callback to receive status information + * @throws IOException to propagate IO errors from {@link FileIterator} + * @throws CancelledException if operation execution was cancelled + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public void walk(int baseRevision, HgStatusInspector inspector) throws IOException, CancelledException, HgRuntimeException { if (HgInternals.wrongRevisionIndex(baseRevision) || baseRevision == BAD_REVISION) { - throw new IllegalArgumentException(String.valueOf(baseRevision)); + throw new HgInvalidRevisionException(baseRevision); } if (getDirstateImpl() == null) { getDirstate(); @@ -162,6 +183,7 @@ if (getDirstateParentManifest() == null) { initDirstateParentManifest(); } + // XXX NOTE, use of TIP for working copy parent is questionable, at least. Instead, TIP shall mean latest cset or not allowed at all ManifestRevision collect = null; // non null indicates we compare against base revision Set baseRevFiles = Collections.emptySet(); // files from base revision not affected by status calculation if (baseRevision != TIP && baseRevision != WORKING_COPY) { @@ -183,14 +205,16 @@ } ((HgStatusCollector.Record) inspector).init(rev1, rev2, sc); } + final CancelSupport cs = CancelSupport.Factory.get(inspector); final HgIgnore hgIgnore = repo.getIgnore(); repoWalker.reset(); TreeSet processed = new TreeSet(); // names of files we handled as they known to Dirstate (not FileIterator) final HgDirstate ds = getDirstateImpl(); TreeSet knownEntries = ds.all(); // here just to get dirstate initialized while (repoWalker.hasNext()) { + cs.checkCancelled(); repoWalker.next(); - final Path fname = getPathPool().path(repoWalker.name()); + final Path fname = getPathPool().mangle(repoWalker.name()); FileInfo f = repoWalker.file(); Path knownInDirstate; if (!f.exists()) { @@ -249,6 +273,7 @@ for (Path fromBase : baseRevFiles) { if (repoWalker.inScope(fromBase)) { inspector.removed(fromBase); + cs.checkCancelled(); } } } @@ -258,6 +283,7 @@ // do not report as missing/removed those FileIterator doesn't care about. continue; } + cs.checkCancelled(); // missing known file from a working dir if (ds.checkRemoved(m) == null) { // not removed from the repository = 'deleted' @@ -272,9 +298,25 @@ } } - public HgStatusCollector.Record status(int baseRevision) throws HgInvalidControlFileException, IOException { + /** + * A {@link #walk(int, HgStatusInspector)} that records all the status information in the {@link HgStatusCollector.Record} object. + * + * @see #walk(int, HgStatusInspector) + * @param baseRevision revision index to check against, or {@link HgRepository#WORKING_COPY}. Note, {@link HgRepository#TIP} is not supported. + * @return information object that describes change between the revisions + * @throws IOException to propagate IO errors from {@link FileIterator} + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public HgStatusCollector.Record status(int baseRevision) throws IOException, HgRuntimeException { HgStatusCollector.Record rv = new HgStatusCollector.Record(); - walk(baseRevision, rv); + try { + walk(baseRevision, rv); + } catch (CancelledException ex) { + // can't happen as long our Record class doesn't implement CancelSupport + HgInvalidStateException t = new HgInvalidStateException("Internal error"); + t.initCause(ex); + throw t; + } return rv; } @@ -287,30 +329,40 @@ // either clean or modified final boolean timestampEqual = f.lastModified() == r.modificationTime(), sizeEqual = r.size() == f.length(); if (timestampEqual && sizeEqual) { - inspector.clean(fname); + // if flags change (chmod -x), timestamp does not change + if (checkFlagsEqual(f, r.mode())) { + inspector.clean(fname); + } else { + inspector.modified(fname); // flags are not the same + } } else if (!sizeEqual && r.size() >= 0) { inspector.modified(fname); } else { + // size is the same or unknown, and, perhaps, different timestamp + // check actual content to avoid false modified files try { - // size is the same or unknown, and, perhaps, different timestamp - // check actual content to avoid false modified files - HgDataFile df = repo.getFileNode(fname); - if (!df.exists()) { - String msg = String.format("File %s known as normal in dirstate (%d, %d), doesn't exist at %s", fname, r.modificationTime(), r.size(), repo.getStoragePath(df)); - throw new HgInvalidFileException(msg, null).setFileName(fname); + if (!checkFlagsEqual(f, r.mode())) { + // flags modified, no need to do expensive content check + inspector.modified(fname); + } else { + HgDataFile df = repo.getFileNode(fname); + if (!df.exists()) { + String msg = String.format("File %s known as normal in dirstate (%d, %d), doesn't exist at %s", fname, r.modificationTime(), r.size(), repo.getStoragePath(df)); + throw new HgInvalidFileException(msg, null).setFileName(fname); + } + Nodeid rev = getDirstateParentManifest().nodeid(fname); + // rev might be null here if fname comes to dirstate as a result of a merge operation + // where one of the parents (first parent) had no fname file, but second parent had. + // E.g. fork revision 3, revision 4 gets .hgtags, few modifications and merge(3,12) + // see Issue 14 for details + if (rev == null || !areTheSame(f, df, rev)) { + inspector.modified(df.getPath()); + } else { + inspector.clean(df.getPath()); + } } - Nodeid rev = getDirstateParentManifest().nodeid(fname); - // rev might be null here if fname comes to dirstate as a result of a merge operation - // where one of the parents (first parent) had no fname file, but second parent had. - // E.g. fork revision 3, revision 4 gets .hgtags, few modifications and merge(3,12) - // see Issue 14 for details - if (rev == null || !areTheSame(f, df, rev)) { - inspector.modified(df.getPath()); - } else { - inspector.clean(df.getPath()); - } - } catch (HgException ex) { - repo.getContext().getLog().warn(getClass(), ex, null); + } catch (HgRuntimeException ex) { + repo.getContext().getLog().dump(getClass(), Warn, ex, null); inspector.invalid(fname, ex); } } @@ -333,29 +385,31 @@ Nodeid nid1 = collect.nodeid(fname); HgManifest.Flags flags = collect.flags(fname); HgDirstate.Record r; + final HgDirstate ds = getDirstateImpl(); if (nid1 == null) { - // normal: added? - // added: not known at the time of baseRevision, shall report - // merged: was not known, report as added? - if ((r = getDirstateImpl().checkNormal(fname)) != null) { + // not known at the time of baseRevision: + // normal, added, merged: either added or copied since base revision. + // removed: nothing to report, + if (ds.checkNormal(fname) != null || ds.checkMerged(fname) != null) { try { Path origin = HgStatusCollector.getOriginIfCopy(repo, fname, baseRevNames, baseRevision); if (origin != null) { - inspector.copied(getPathPool().path(origin), fname); + inspector.copied(getPathPool().mangle(origin), fname); return; } - } catch (HgException ex) { + // fall-through, report as added + } catch (HgInvalidFileException ex) { // report failure and continue status collection inspector.invalid(fname, ex); } - } else if ((r = getDirstateImpl().checkAdded(fname)) != null) { + } else if ((r = ds.checkAdded(fname)) != null) { if (r.copySource() != null && baseRevNames.contains(r.copySource())) { - baseRevNames.remove(r.copySource()); // XXX surely I shall not report rename source as Removed? + baseRevNames.remove(r.copySource()); // FIXME likely I shall report rename source as Removed, same as above for Normal? inspector.copied(r.copySource(), fname); return; } // fall-through, report as added - } else if (getDirstateImpl().checkRemoved(fname) != null) { + } else if (ds.checkRemoved(fname) != null) { // removed: removed file was not known at the time of baseRevision, and we should not report it as removed return; } @@ -363,7 +417,7 @@ } else { // was known; check whether clean or modified Nodeid nidFromDirstate = getDirstateParentManifest().nodeid(fname); - if ((r = getDirstateImpl().checkNormal(fname)) != null && nid1.equals(nidFromDirstate)) { + if ((r = ds.checkNormal(fname)) != null && nid1.equals(nidFromDirstate)) { // regular file, was the same up to WC initialization. Check if was modified since, and, if not, report right away // same code as in #checkLocalStatusAgainstFile final boolean timestampEqual = f.lastModified() == r.modificationTime(), sizeEqual = r.size() == f.length(); @@ -374,7 +428,7 @@ } else if (!sizeEqual && r.size() >= 0) { inspector.modified(fname); handled = true; - } else if (!todoCheckFlagsEqual(f, flags)) { + } else if (!checkFlagsEqual(f, flags)) { // seems like flags have changed, no reason to check content further inspector.modified(fname); handled = true; @@ -387,7 +441,7 @@ // or nodeid in dirstate is different, but local change might have brought it back to baseRevision state) // FALL THROUGH } - if (r != null || (r = getDirstateImpl().checkMerged(fname)) != null || (r = getDirstateImpl().checkAdded(fname)) != null) { + if (r != null || (r = ds.checkMerged(fname)) != null || (r = ds.checkAdded(fname)) != null) { try { // check actual content to see actual changes // when added - seems to be the case of a file added once again, hence need to check if content is different @@ -398,8 +452,8 @@ } else { inspector.modified(fname); } - } catch (HgException ex) { - repo.getContext().getLog().warn(getClass(), ex, null); + } catch (HgRuntimeException ex) { + repo.getContext().getLog().dump(getClass(), Warn, ex, null); inspector.invalid(fname, ex); } baseRevNames.remove(fname); // consumed, processed, handled. @@ -419,7 +473,7 @@ // The question is whether original Hg treats this case (same content, different parents and hence nodeids) as 'modified' or 'clean' } - private boolean areTheSame(FileInfo f, HgDataFile dataFile, Nodeid revision) throws HgException { + private boolean areTheSame(FileInfo f, HgDataFile dataFile, Nodeid revision) throws HgInvalidFileException { // XXX consider adding HgDataDile.compare(File/byte[]/whatever) operation to optimize comparison ByteArrayChannel bac = new ByteArrayChannel(); try { @@ -433,7 +487,7 @@ return areTheSame(f, bac.toArray(), dataFile.getPath()); } - private boolean areTheSame(FileInfo f, final byte[] data, Path p) throws HgException { + private boolean areTheSame(FileInfo f, final byte[] data, Path p) throws HgInvalidFileException { ReadableByteChannel is = null; class Check implements ByteChannel { final boolean debug = repo.getContext().getLog().isDebug(); @@ -456,7 +510,7 @@ int offset = max(0, x - 4); exp = new String(data, offset, min(data.length - offset, 20)); } - repo.getContext().getLog().debug(getClass(), "expected >>%s<< but got >>%s<<", exp, new String(xx)); + repo.getContext().getLog().dump(getClass(), Debug, "expected >>%s<< but got >>%s<<", exp, new String(xx)); } sameSoFar = false; break; @@ -489,7 +543,7 @@ try { is.close(); } catch (IOException ex) { - repo.getContext().getLog().info(getClass(), ex, null); + repo.getContext().getLog().dump(getClass(), Info, ex, null); } is = f.newInputChannel(); fb.clear(); @@ -501,7 +555,7 @@ } return check.ultimatelyTheSame(); } catch (CancelledException ex) { - repo.getContext().getLog().warn(getClass(), ex, "Unexpected cancellation"); + repo.getContext().getLog().dump(getClass(), Warn, ex, "Unexpected cancellation"); return check.ultimatelyTheSame(); } catch (IOException ex) { throw new HgInvalidFileException("File comparison failed", ex).setFileName(p); @@ -510,15 +564,45 @@ try { is.close(); } catch (IOException ex) { - repo.getContext().getLog().info(getClass(), ex, null); + repo.getContext().getLog().dump(getClass(), Info, ex, null); } } } } - private static boolean todoCheckFlagsEqual(FileInfo f, HgManifest.Flags originalManifestFlags) { - // FIXME implement - return true; + /** + * @return true if flags are the same + */ + private boolean checkFlagsEqual(FileInfo f, HgManifest.Flags originalManifestFlags) { + boolean same = true; + if (repoWalker.supportsLinkFlag()) { + if (originalManifestFlags == HgManifest.Flags.Link) { + return f.isSymlink(); + } + // original flag is not link, hence flags are the same if file is not link, too. + same = !f.isSymlink(); + } // otherwise treat flags the same + if (repoWalker.supportsExecFlag()) { + if (originalManifestFlags == HgManifest.Flags.Exec) { + return f.isExecutable(); + } + // original flag has no executable attribute, hence file shall not be executable, too + same = same || !f.isExecutable(); + } + return same; + } + + private boolean checkFlagsEqual(FileInfo f, int dirstateFileMode) { + // source/include/linux/stat.h + final int S_IFLNK = 0120000, S_IXUSR = 00100; + // TODO post-1.0 HgManifest.Flags.parse(int) + if ((dirstateFileMode & S_IFLNK) == S_IFLNK) { + return checkFlagsEqual(f, HgManifest.Flags.Link); + } + if ((dirstateFileMode & S_IXUSR) == S_IXUSR) { + return checkFlagsEqual(f, HgManifest.Flags.Exec); + } + return checkFlagsEqual(f, HgManifest.Flags.RegularFile); // no flags } /** @@ -530,7 +614,6 @@ * * @return new instance of {@link HgWorkingCopyStatusCollector}, ready to {@link #walk(int, HgStatusInspector) walk} associated working copy */ - @Experimental(reason="Provisional API") public static HgWorkingCopyStatusCollector create(HgRepository hgRepo, Path... paths) { ArrayList f = new ArrayList(5); ArrayList d = new ArrayList(5); @@ -544,7 +627,7 @@ // final Path[] dirs = f.toArray(new Path[d.size()]); if (d.isEmpty()) { final Path[] files = f.toArray(new Path[f.size()]); - FileIterator fi = new FileListIterator(hgRepo.getWorkingDir(), files); + FileIterator fi = new FileListIterator(hgRepo.getContext(), hgRepo.getWorkingDir(), files); return new HgWorkingCopyStatusCollector(hgRepo, fi); } // @@ -562,7 +645,6 @@ * * @return new instance of {@link HgWorkingCopyStatusCollector}, ready to {@link #walk(int, HgStatusInspector) walk} associated working copy */ - @Experimental(reason="Provisional API. May add boolean strict argument for those who write smart matchers that can be used in FileWalker") public static HgWorkingCopyStatusCollector create(HgRepository hgRepo, Path.Matcher scope) { FileIterator w = new HgInternals(hgRepo).createWorkingDirWalker(null); FileIterator wf = (scope == null || scope instanceof Path.Matcher.Any) ? w : new FileIteratorFilter(w, scope); @@ -580,16 +662,21 @@ private final Path[] paths; private int index; private RegularFileInfo nextFile; + private final boolean execCap, linkCap; + private final SessionContext sessionContext; - public FileListIterator(File startDir, Path... files) { + public FileListIterator(SessionContext ctx, File startDir, Path... files) { + sessionContext = ctx; dir = startDir; paths = files; reset(); + execCap = Internals.checkSupportsExecutables(startDir); + linkCap = Internals.checkSupportsSymlinks(startDir); } public void reset() { index = -1; - nextFile = new RegularFileInfo(); + nextFile = new RegularFileInfo(sessionContext, execCap, linkCap); } public boolean hasNext() { @@ -620,6 +707,14 @@ } return false; } + + public boolean supportsExecFlag() { + return execCap; + } + + public boolean supportsLinkFlag() { + return linkCap; + } } private static class FileIteratorFilter implements FileIterator { @@ -670,5 +765,13 @@ public boolean inScope(Path file) { return filter.accept(file); } + + public boolean supportsExecFlag() { + return walker.supportsExecFlag(); + } + + public boolean supportsLinkFlag() { + return walker.supportsLinkFlag(); + } } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/Revlog.java --- a/src/org/tmatesoft/hg/repo/Revlog.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/Revlog.java Wed Jul 11 20:40:47 2012 +0200 @@ -16,24 +16,16 @@ */ package org.tmatesoft.hg.repo; -import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; -import static org.tmatesoft.hg.repo.HgRepository.TIP; +import static org.tmatesoft.hg.repo.HgRepository.*; +import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collection; -import java.util.HashSet; -import java.util.LinkedList; import java.util.List; -import org.tmatesoft.hg.core.HgBadStateException; -import org.tmatesoft.hg.core.HgException; -import org.tmatesoft.hg.core.HgInvalidControlFileException; -import org.tmatesoft.hg.core.HgInvalidRevisionException; import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.internal.ArrayHelper; import org.tmatesoft.hg.internal.DataAccess; import org.tmatesoft.hg.internal.Experimental; import org.tmatesoft.hg.internal.IntMap; @@ -83,32 +75,44 @@ return repo; } - public final int getRevisionCount() { + /** + * @return total number of revisions kept in this revlog + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public final int getRevisionCount() throws HgRuntimeException { return content.revisionCount(); } - public final int getLastRevision() { + /** + * @return index of last known revision, a.k.a. {@link HgRepository#TIP} + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ + public final int getLastRevision() throws HgRuntimeException { return content.revisionCount() - 1; } /** * Map revision index to unique revision identifier (nodeid). * - * @param revision index of the entry in this revlog, may be {@link HgRepository#TIP} + * @param revisionIndex 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 - * @throws HgInvalidControlFileException if access to revlog index/data entry failed + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public final Nodeid getRevision(int revision) throws HgInvalidRevisionException, HgInvalidControlFileException { + public final Nodeid getRevision(int revisionIndex) throws HgRuntimeException { // XXX cache nodeids? Rather, if context.getCache(this).getRevisionMap(create == false) != null, use it - return Nodeid.fromBinary(content.nodeid(revision), 0); + return Nodeid.fromBinary(content.nodeid(revisionIndex), 0); } /** - * FIXME need to be careful about (1) ordering of the revisions in the return list; (2) modifications (sorting) of the argument array + * Effective alternative to map few revision indexes to corresponding nodeids at once. + *

Note, there are few aspects to be careful about when using this method

    + *
  • ordering of the revisions in the return list is unspecified, it's likely won't match that of the method argument + *
  • supplied array get modified (sorted)
+ * @return list of mapped revisions in no particular order + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public final List getRevisions(int... revisions) throws HgInvalidRevisionException, HgInvalidControlFileException { + public final List getRevisions(int... revisions) throws HgRuntimeException { ArrayList rv = new ArrayList(revisions.length); Arrays.sort(revisions); getRevisionsInternal(rv, revisions); @@ -131,14 +135,13 @@ * If unsure, use {@link #isKnown(Nodeid)} to find out whether nodeid belongs to this revlog. * * For occasional queries, this method works with decent performance, despite its O(n/2) approach. - * Alternatively, if you need to perform multiple queries (e.g. at least 15-20), {@link RevisionMap} may come handy. + * Alternatively, if you need to perform multiple queries (e.g. at least 15-20), {@link HgRevisionMap} may come handy. * * @param nid revision to look up * @return revision local index in this revlog - * @throws HgInvalidRevisionException if supplied nodeid doesn't identify any revision from this revlog - * @throws HgInvalidControlFileException if access to revlog index/data entry failed + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public final int getRevisionIndex(Nodeid nid) throws HgInvalidControlFileException, HgInvalidRevisionException { + public final int getRevisionIndex(Nodeid nid) throws HgRuntimeException { int revision = content.findRevisionIndex(nid); if (revision == BAD_REVISION) { // using toString() to identify revlog. HgDataFile.toString includes path, HgManifest and HgChangelog instances @@ -150,66 +153,82 @@ } /** - * @deprecated use {@link #getRevisionIndex(Nodeid)} instead - */ - @Deprecated - public final int getLocalRevision(Nodeid nid) throws HgInvalidControlFileException, HgInvalidRevisionException { - return getRevisionIndex(nid); - } - - - /** * Note, {@link Nodeid#NULL} nodeid is not reported as known in any revlog. * * @param nodeid - * @return - * @throws HgInvalidControlFileException if access to revlog index/data entry failed + * @return true if revision is part of this revlog + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public final boolean isKnown(Nodeid nodeid) throws HgInvalidControlFileException { + public final boolean isKnown(Nodeid nodeid) throws HgRuntimeException { final int rn = content.findRevisionIndex(nodeid); if (BAD_REVISION == rn) { return false; } if (rn < 0 || rn >= content.revisionCount()) { // Sanity check - throw new HgBadStateException(String.format("Revision index %d found for nodeid %s is not from the range [0..%d]", rn, nodeid.shortNotation(), content.revisionCount()-1)); + throw new HgInvalidStateException(String.format("Revision index %d found for nodeid %s is not from the range [0..%d]", rn, nodeid.shortNotation(), content.revisionCount()-1)); } return true; } /** - * Access to revision data as is (decompressed, but otherwise unprocessed, i.e. not parsed for e.g. changeset or manifest entries) - * @param nodeid + * Access to revision data as is, equivalent to rawContent(getRevisionIndex(nodeid), sink) + * + * @param nodeid revision to retrieve + * @param sink data destination + * + * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog + * @throws HgInvalidControlFileException if access to revlog index/data entry failed + * @throws CancelledException if content retrieval operation was cancelled + * + * @see #rawContent(int, ByteChannel) */ - protected void rawContent(Nodeid nodeid, ByteChannel sink) throws HgException, IOException, CancelledException, HgInvalidRevisionException { + protected void rawContent(Nodeid nodeid, ByteChannel sink) throws HgInvalidControlFileException, CancelledException, HgInvalidRevisionException { rawContent(getRevisionIndex(nodeid), sink); } /** - * @param fileRevisionIndex - index of this file change (not a changelog revision index), non-negative. From predefined constants, only {@link HgRepository#TIP} makes sense. - * FIXME is it necessary to have IOException along with HgException here? + * Access to revision data as is (decompressed, but otherwise unprocessed, i.e. not parsed for e.g. changeset or manifest entries). + * + * @param revisionIndex index of this revlog change (not a changelog revision index), non-negative. From predefined constants, only {@link HgRepository#TIP} makes sense. + * @param sink data destination + * + * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog + * @throws HgInvalidControlFileException if access to revlog index/data entry failed + * @throws CancelledException if content retrieval operation was cancelled */ - protected void rawContent(int fileRevisionIndex, ByteChannel sink) throws HgException, IOException, CancelledException, HgInvalidRevisionException { + protected void rawContent(int revisionIndex, ByteChannel sink) throws HgInvalidControlFileException, CancelledException, HgInvalidRevisionException { if (sink == null) { throw new IllegalArgumentException(); } - ContentPipe insp = new ContentPipe(sink, 0, repo.getContext().getLog()); - insp.checkCancelled(); - content.iterate(fileRevisionIndex, fileRevisionIndex, true, insp); - insp.checkFailed(); + try { + ContentPipe insp = new ContentPipe(sink, 0, repo.getContext().getLog()); + insp.checkCancelled(); + content.iterate(revisionIndex, revisionIndex, true, insp); + insp.checkFailed(); + } catch (IOException ex) { + HgInvalidControlFileException e = new HgInvalidControlFileException(String.format("Access to revision %d content failed", revisionIndex), ex, null); + e.setRevisionIndex(revisionIndex); + // TODO post 1.0 e.setFileName(content.getIndexFile() or this.getHumanFriendlyPath()) - shall decide whether + // protected abstract getHFPath() with impl in HgDataFile, HgManifest and HgChangelog or path is data of either Revlog or RevlogStream + // Do the same (add file name) below + throw e; + } catch (HgInvalidControlFileException ex) { + throw ex.isRevisionIndexSet() ? ex : ex.setRevisionIndex(revisionIndex); + } } /** - * XXX perhaps, return value Nodeid[2] and boolean needNodeids is better (and higher level) API for this query? + * Fills supplied arguments with information about revision parents. * * @param revision - revision to query parents, or {@link HgRepository#TIP} - * @param parentRevisions - int[2] to get local revision numbers of parents (e.g. {6, -1}) + * @param parentRevisions - int[2] to get local revision numbers of parents (e.g. {6, -1}), {@link HgRepository#NO_REVISION} indicates parent not set * @param parent1 - byte[20] or null, if parent's nodeid is not needed * @param parent2 - byte[20] or null, if second parent's nodeid is not needed - * @throws HgInvalidRevisionException - * @throws IllegalArgumentException + * @throws IllegalArgumentException if passed arrays can't fit requested data + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception */ - public void parents(int revision, int[] parentRevisions, byte[] parent1, byte[] parent2) throws HgInvalidRevisionException, HgInvalidControlFileException { + public void parents(int revision, int[] parentRevisions, byte[] parent1, byte[] parent2) throws HgRuntimeException, IllegalArgumentException { if (revision != TIP && !(revision >= 0 && revision < content.revisionCount())) { throw new HgInvalidRevisionException(revision); } @@ -237,10 +256,11 @@ }; ParentCollector pc = new ParentCollector(); content.iterate(revision, revision, false, pc); - parentRevisions[0] = pc.p1; - parentRevisions[1] = pc.p2; + // although next code looks odd (NO_REVISION *is* -1), it's safer to be explicit + parentRevisions[0] = pc.p1 == -1 ? NO_REVISION : pc.p1; + parentRevisions[1] = pc.p2 == -1 ? NO_REVISION : pc.p2; if (parent1 != null) { - if (parentRevisions[0] == -1) { + if (parentRevisions[0] == NO_REVISION) { Arrays.fill(parent1, 0, 20, (byte) 0); } else { content.iterate(parentRevisions[0], parentRevisions[0], false, pc); @@ -248,7 +268,7 @@ } } if (parent2 != null) { - if (parentRevisions[1] == -1) { + if (parentRevisions[1] == NO_REVISION) { Arrays.fill(parent2, 0, 20, (byte) 0); } else { content.iterate(parentRevisions[1], parentRevisions[1], false, pc); @@ -256,9 +276,19 @@ } } } - + + /** + * EXPERIMENTAL CODE, DO NOT USE + * + * Alternative revlog iteration + * + * @param start + * @param end + * @param inspector + * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception + */ @Experimental - public final void walk(int start, int end, final Revlog.Inspector inspector) throws HgInvalidRevisionException, HgInvalidControlFileException { + public final void indexWalk(int start, int end, final Revlog.Inspector inspector) throws HgRuntimeException { int lastRev = getLastRevision(); final int _start = start == TIP ? lastRev : start; if (end == TIP) { @@ -354,273 +384,15 @@ void next(int revisionIndex, Nodeid revision, int parent1, int parent2, Nodeid nidParent1, Nodeid nidParent2); } - /* - * XXX think over if it's better to do either: - * pw = getChangelog().new ParentWalker(); pw.init() and pass pw instance around as needed - * or - * add Revlog#getParentWalker(), static class, make cons() and #init package-local, and keep SoftReference to allow walker reuse. - * - * and yes, walker is not a proper name - */ - public final class ParentWalker implements ParentInspector { - - - private Nodeid[] sequential; // natural repository order, childrenOf rely on ordering - private Nodeid[] sorted; // for binary search - private int[] sorted2natural; - private Nodeid[] firstParent; - private Nodeid[] secondParent; - - // Nodeid instances shall be shared between all arrays - - public ParentWalker() { - } - - public HgRepository getRepo() { - return Revlog.this.getRepo(); - } - - public void next(int revisionNumber, Nodeid revision, int parent1Revision, int parent2Revision, Nodeid nidParent1, Nodeid nidParent2) { - if (parent1Revision >= revisionNumber || parent2Revision >= revisionNumber) { - throw new IllegalStateException(); // sanity, revisions are sequential - } - int ix = revisionNumber; - sequential[ix] = sorted[ix] = revision; - if (parent1Revision != -1) { - firstParent[ix] = sequential[parent1Revision]; - } - if (parent2Revision != -1) { // revlog of DataAccess.java has p2 set when p1 is -1 - secondParent[ix] = sequential[parent2Revision]; - } - } - - public void init() throws HgInvalidControlFileException { - final int revisionCount = Revlog.this.getRevisionCount(); - firstParent = new Nodeid[revisionCount]; - // TODO [post 1.0] Branches/merges are less frequent, and most of secondParent would be -1/null, hence - // IntMap might be better alternative here, but need to carefully analyze (test) whether this brings - // real improvement (IntMap has 2n capacity, and element lookup is log(n) instead of array's constant) - secondParent = new Nodeid[revisionCount]; - // - sequential = new Nodeid[revisionCount]; - sorted = new Nodeid[revisionCount]; - Revlog.this.walk(0, TIP, this); - Arrays.sort(sorted); - sorted2natural = new int[revisionCount]; - for (int i = 0; i < revisionCount; i++) { - Nodeid n = sequential[i]; - int x = Arrays.binarySearch(sorted, n); - assertSortedIndex(x); - sorted2natural[x] = i; - } - } - - private void assertSortedIndex(int x) { - if (x < 0) { - throw new HgBadStateException(); - } - } - - /** - * Tells whether supplied revision is from the walker's associated revlog. - * Note, {@link Nodeid#NULL}, although implicitly present as parent of a first revision, is not recognized as known. - * @param nid revision to check, not null - * @return true if revision matches any revision in this revlog - */ - public boolean knownNode(Nodeid nid) { - return Arrays.binarySearch(sorted, nid) >= 0; - } - - /** - * null if none. only known nodes (as per #knownNode) are accepted as arguments - */ - public Nodeid firstParent(Nodeid nid) { - int x = Arrays.binarySearch(sorted, nid); - assertSortedIndex(x); - int i = sorted2natural[x]; - return firstParent[i]; - } - - // never null, Nodeid.NULL if none known - public Nodeid safeFirstParent(Nodeid nid) { - Nodeid rv = firstParent(nid); - return rv == null ? Nodeid.NULL : rv; - } - - public Nodeid secondParent(Nodeid nid) { - int x = Arrays.binarySearch(sorted, nid); - assertSortedIndex(x); - int i = sorted2natural[x]; - return secondParent[i]; - } - - public Nodeid safeSecondParent(Nodeid nid) { - Nodeid rv = secondParent(nid); - return rv == null ? Nodeid.NULL : rv; - } - - public boolean appendParentsOf(Nodeid nid, Collection c) { - int x = Arrays.binarySearch(sorted, nid); - assertSortedIndex(x); - int i = sorted2natural[x]; - Nodeid p1 = firstParent[i]; - boolean modified = false; - if (p1 != null) { - modified = c.add(p1); - } - Nodeid p2 = secondParent[i]; - if (p2 != null) { - modified = c.add(p2) || modified; - } - return modified; - } - - // XXX alternative (and perhaps more reliable) approach would be to make a copy of allNodes and remove - // nodes, their parents and so on. - - // @return ordered collection of all children rooted at supplied nodes. Nodes shall not be descendants of each other! - // Nodeids shall belong to this revlog - public List childrenOf(List roots) { - HashSet parents = new HashSet(); - LinkedList result = new LinkedList(); - int earliestRevision = Integer.MAX_VALUE; - assert sequential.length == firstParent.length && firstParent.length == secondParent.length; - // first, find earliest index of roots in question, as there's no sense - // to check children among nodes prior to branch's root node - for (Nodeid r : roots) { - int x = Arrays.binarySearch(sorted, r); - assertSortedIndex(x); - int i = sorted2natural[x]; - if (i < earliestRevision) { - earliestRevision = i; - } - parents.add(sequential[i]); // add canonical instance in hope equals() is bit faster when can do a == - } - for (int i = earliestRevision + 1; i < sequential.length; i++) { - if (parents.contains(firstParent[i]) || parents.contains(secondParent[i])) { - parents.add(sequential[i]); // to find next child - result.add(sequential[i]); - } - } - return result; - } - - /** - * @return revisions that have supplied revision as their immediate parent - */ - public List directChildren(Nodeid nid) { - LinkedList result = new LinkedList(); - int x = Arrays.binarySearch(sorted, nid); - assertSortedIndex(x); - nid = sorted[x]; // canonical instance - int start = sorted2natural[x]; - for (int i = start + 1; i < sequential.length; i++) { - if (nid == firstParent[i] || nid == secondParent[i]) { - result.add(sequential[i]); - } - } - return result; - } - - /** - * @param nid possibly parent node, shall be {@link #knownNode(Nodeid) known} in this revlog. - * @return true if there's any node in this revlog that has specified node as one of its parents. - */ - public boolean hasChildren(Nodeid nid) { - int x = Arrays.binarySearch(sorted, nid); - assertSortedIndex(x); - int i = sorted2natural[x]; - assert firstParent.length == secondParent.length; // just in case later I implement sparse array for secondParent - assert firstParent.length == sequential.length; - // to use == instead of equals, take the same Nodeid instance we used to fill all the arrays. - final Nodeid canonicalNode = sequential[i]; - i++; // no need to check node itself. child nodes may appear in sequential only after revision in question - for (; i < sequential.length; i++) { - // TODO [post 1.0] likely, not very effective. - // May want to optimize it with another (Tree|Hash)Set, created on demand on first use, - // however, need to be careful with memory usage - if (firstParent[i] == canonicalNode || secondParent[i] == canonicalNode) { - return true; - } - } - return false; - } + protected HgParentChildMap getParentWalker() { + HgParentChildMap pw = new HgParentChildMap(this); + pw.init(); + return pw; } - /** - * Effective int to Nodeid and vice versa translation. It's advised to use this class instead of - * multiple {@link Revlog#getRevisionIndex(Nodeid)} calls. - * - * {@link Revlog#getRevisionIndex(Nodeid)} with straightforward lookup approach performs O(n/2) - * {@link RevisionMap#revisionIndex(Nodeid)} is log(n), plus initialization is O(n) (just once). + /* + * class with cancel and few other exceptions support. TODO consider general superclass to share with e.g. HgManifestCommand.Mediator */ - public final class RevisionMap implements RevisionInspector { - /* - * in fact, initialization is much slower as it instantiates Nodeids, while #getRevisionIndex - * compares directly against byte buffer. Measuring cpython with 70k+ gives 3 times difference (47 vs 171) - * for complete changelog iteration. - */ - - /* - * XXX 3 * (x * 4) bytes. Can I do better? - * It seems, yes. Don't need to keep sorted, always can emulate it with indirect access to sequential through sorted2natural. - * i.e. instead sorted[mid].compareTo(toFind), do sequential[sorted2natural[mid]].compareTo(toFind) - */ - private Nodeid[] sequential; // natural repository order, childrenOf rely on ordering - private Nodeid[] sorted; // for binary search - private int[] sorted2natural; - - public RevisionMap() { - } - - public HgRepository getRepo() { - return Revlog.this.getRepo(); - } - - public void next(int revisionIndex, Nodeid revision, int linkedRevision) { - sequential[revisionIndex] = sorted[revisionIndex] = revision; - } - - /** - * @return this for convenience. - */ - public RevisionMap init(/*XXX Pool to reuse nodeids, if possible. */) throws HgInvalidControlFileException{ - // XXX HgRepository.register((RepoChangeListener) this); // listen to changes in repo, re-init if needed? - final int revisionCount = Revlog.this.getRevisionCount(); - sequential = new Nodeid[revisionCount]; - sorted = new Nodeid[revisionCount]; - Revlog.this.walk(0, TIP, this); - // next is alternative to Arrays.sort(sorted), and build sorted2natural looking up each element of sequential in sorted. - // the way sorted2natural was build is O(n*log n). - final ArrayHelper ah = new ArrayHelper(); - ah.sort(sorted); - // note, values in ArrayHelper#getReversed are 1-based indexes, not 0-based - sorted2natural = ah.getReverse(); - return this; - } - - public Nodeid revision(int revisionIndex) { - return sequential[revisionIndex]; - } - public int revisionIndex(Nodeid revision) { - if (revision == null || revision.isNull()) { - return BAD_REVISION; - } - int x = Arrays.binarySearch(sorted, revision); - if (x < 0) { - return BAD_REVISION; - } - return sorted2natural[x]-1; - } - /** - * @deprecated use {@link #revisionIndex(Nodeid)} instead - */ - @Deprecated - public int localRevision(Nodeid revision) { - return revisionIndex(revision); - } - } - protected abstract static class ErrorHandlingInspector implements RevlogStream.Inspector, CancelSupport { private Exception failure; private CancelSupport cancelSupport; @@ -635,7 +407,7 @@ failure = ex; } - public void checkFailed() throws HgException, IOException, CancelledException { + public void checkFailed() throws HgRuntimeException, IOException, CancelledException { if (failure == null) { return; } @@ -645,10 +417,10 @@ if (failure instanceof CancelledException) { throw (CancelledException) failure; } - if (failure instanceof HgException) { - throw (HgException) failure; + if (failure instanceof HgRuntimeException) { + throw (HgRuntimeException) failure; } - throw new HgBadStateException(failure); + throw new HgInvalidStateException(failure.toString()); } public void checkCancelled() throws CancelledException { @@ -676,7 +448,7 @@ logFacility = log; } - protected void prepare(int revisionNumber, DataAccess da) throws HgException, IOException { + protected void prepare(int revisionNumber, DataAccess da) throws IOException { if (offset > 0) { // save few useless reset/rewind operations da.seek(offset); } @@ -711,10 +483,10 @@ int consumed = sink.write(buf); if ((consumed == 0 || consumed != buf.position()) && logFacility != null) { - logFacility.warn(getClass(), "Bad data sink when reading revision %d. Reported %d bytes consumed, byt actually read %d", revisionNumber, consumed, buf.position()); + logFacility.dump(getClass(), Warn, "Bad data sink when reading revision %d. Reported %d bytes consumed, byt actually read %d", revisionNumber, consumed, buf.position()); } if (buf.position() == 0) { - throw new HgBadStateException("Bad sink implementation (consumes no bytes) results in endless loop"); + throw new HgInvalidStateException("Bad sink implementation (consumes no bytes) results in endless loop"); } buf.compact(); // ensure (a) there's space for new (b) data starts at 0 progressSupport.worked(consumed); @@ -724,8 +496,6 @@ recordFailure(ex); } catch (CancelledException ex) { recordFailure(ex); - } catch (HgException ex) { - recordFailure(ex); } } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/ext/MqManager.java --- a/src/org/tmatesoft/hg/repo/ext/MqManager.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/ext/MqManager.java Wed Jul 11 20:40:47 2012 +0200 @@ -16,6 +16,8 @@ */ package org.tmatesoft.hg.repo.ext; +import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; + import java.io.BufferedReader; import java.io.File; import java.io.FileReader; @@ -28,10 +30,10 @@ import java.util.List; import java.util.Map; -import org.tmatesoft.hg.core.HgInvalidControlFileException; -import org.tmatesoft.hg.core.HgInvalidFileException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.repo.HgInternals; +import org.tmatesoft.hg.repo.HgInvalidControlFileException; +import org.tmatesoft.hg.repo.HgInvalidFileException; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.util.LogFacility; import org.tmatesoft.hg.util.Path; @@ -78,7 +80,7 @@ ArrayList contents = new ArrayList(); new LineReader(activeQueueFile, log).read(new SimpleLineCollector(), contents); if (contents.isEmpty()) { - log.warn(getClass(), "File %s with active queue name is empty", activeQueueFile.getName()); + log.dump(getClass(), Warn, "File %s with active queue name is empty", activeQueueFile.getName()); activeQueue = PATCHES_DIR; queueLocation = PATCHES_DIR + '/'; } else { @@ -91,7 +93,7 @@ } final Path.Source patchLocation = new Path.Source() { - public Path path(String p) { + public Path path(CharSequence p) { StringBuilder sb = new StringBuilder(64); sb.append(".hg/"); sb.append(queueLocation); @@ -107,7 +109,7 @@ public boolean consume(String line, List result) throws IOException { int sep = line.indexOf(':'); if (sep == -1) { - log.warn(MqManager.class, "Bad line in %s:%s", fileStatus.getPath(), line); + log.dump(MqManager.class, Warn, "Bad line in %s:%s", fileStatus.getPath(), line); return true; } Nodeid nid = Nodeid.fromAscii(line.substring(0, sep)); @@ -310,7 +312,7 @@ try { statusFileReader.close(); } catch (IOException ex) { - log.warn(MqManager.class, ex, null); + log.dump(MqManager.class, Warn, ex, null); } // try { // consumer.end(file, paramObj); diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/repo/package.html --- a/src/org/tmatesoft/hg/repo/package.html Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/package.html Wed Jul 11 20:40:47 2012 +0200 @@ -1,5 +1,6 @@ - -Low-level API operations - + +

Low-level API

+

Close perspective of Mercurial repository from Java standpoint. Unlike {@link org.tmatesoft.hg.core}, classes in this package focus on repository concepts rather than tasks against the repository

+ \ No newline at end of file diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/CancelSupport.java --- a/src/org/tmatesoft/hg/util/CancelSupport.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/util/CancelSupport.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -47,10 +47,11 @@ if (cs != null) { return cs; } - return new CancelSupport() { + class NoCancel implements CancelSupport { public void checkCancelled() { } }; + return new NoCancel(); } public static CancelSupport get(Object target, CancelSupport defaultValue) { diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/Convertor.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/util/Convertor.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,27 @@ +/* + * Copyright (c) 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 + * 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.util; + +/** + * Transformations, e.g. unique instances + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public interface Convertor { + T mangle(T t); +} \ No newline at end of file diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/DirectHashSet.java --- a/src/org/tmatesoft/hg/util/DirectHashSet.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/util/DirectHashSet.java Wed Jul 11 20:40:47 2012 +0200 @@ -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,7 +16,6 @@ */ package org.tmatesoft.hg.util; -import org.tmatesoft.hg.internal.Experimental; /** * Memory-friendly alternative to HashSet. With slightly worse performance than that of HashSet, uses n * sizeof(HashMap.Entry) less memory @@ -26,7 +25,6 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -@Experimental public class DirectHashSet { private Object[] table; diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/FileInfo.java --- a/src/org/tmatesoft/hg/util/FileInfo.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/util/FileInfo.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -47,4 +47,16 @@ * @return file reader object, never null */ ReadableByteChannel newInputChannel(); + + /** + * This method is invoked only if source FileIterator tells true for {@link FileIterator#supportsExecFlag()} + * @return true if this object describes an executable file + */ + boolean isExecutable(); + + /** + * This method is be invoked only if source FileIterator tells true for {@link FileIterator#supportsLinkFlag()}. + * @return true if this file object represents a symbolic link + */ + boolean isSymlink(); } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/FileIterator.java --- a/src/org/tmatesoft/hg/util/FileIterator.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/util/FileIterator.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -18,7 +18,8 @@ import java.io.IOException; -import org.tmatesoft.hg.internal.Experimental; +import org.tmatesoft.hg.repo.HgStatusCollector; +import org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector; /** * Abstracts iteration over file system. @@ -60,8 +61,27 @@ * specific file (path) belongs to that fraction or not. Paths and files returned by this {@link FileIterator} * are always considered as representing the fraction, nonetheless, {@link FileIterator} shall return true for such names if * asked. + *

+ * Implementors are advised to use {@link Path.Matcher}, as this scope is very similar to what regular + * {@link HgStatusCollector} (which doesn't use FI) supports, and use of matcher makes {@link HgWorkingCopyStatusCollector} + * look similar. + * * @return true if this {@link FileIterator} is responsible for (interested in) specified repository-local path */ - @Experimental(reason="Perhaps, shall not be part of FileIterator, but rather separate Path.Matcher. Approaches in regular StatusCollector (doesn't use FI, but supports scope) and WC collector to look similar, and for HgStatusCommand to use single approach to set the scope") - boolean inScope(Path file); + boolean inScope(Path file); // PathMatcher scope() + + /** + * Tells whether caller shall be aware of distinction between executable and non-executable files coming from this iterator. + * Note, these days Mercurial (as of 2.1) doesn't recognize Windows .exe files as executable (nor it treats any Windows filesystem as exec-capable) + * @return true if file descriptors are capable to provide executable flag + */ + boolean supportsExecFlag(); + + /** + * POSIX file systems allow symbolic links to files, and these links are handled in a special way with Mercurial, i.e. it tracks value of + * the link, not its actual target. + * Note, these days Mercurial (as of 2.1) doesn't support Windows Vista/7 symlinks. + * @return true if file descriptors are capable to tell symlink files from regular ones. + */ + boolean supportsLinkFlag(); } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/FileWalker.java --- a/src/org/tmatesoft/hg/util/FileWalker.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/util/FileWalker.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -20,7 +20,11 @@ import java.util.LinkedList; import java.util.NoSuchElementException; +import org.tmatesoft.hg.core.SessionContext; +import org.tmatesoft.hg.internal.Internals; + /** + * Implementation of {@link FileIterator} using regular {@link java.io.File} * * @author Artem Tikhomirov * @author TMate Software Ltd. @@ -32,11 +36,13 @@ private final LinkedList dirQueue; private final LinkedList fileQueue; private final Path.Matcher scope; + private final boolean execCap, linkCap; + private final SessionContext sessionContext; private RegularFileInfo nextFile; private Path nextPath; - public FileWalker(File dir, Path.Source pathFactory) { - this(dir, pathFactory, null); + public FileWalker(SessionContext ctx, File dir, Path.Source pathFactory) { + this(ctx, dir, pathFactory, null); } /** @@ -47,12 +53,15 @@ * also whether directories shall be traversed or not (Paths it gets in {@link Path.Matcher#accept(Path)} may * point to directories) */ - public FileWalker(File dir, Path.Source pathFactory, Path.Matcher scopeMatcher) { + public FileWalker(SessionContext ctx, File dir, Path.Source pathFactory, Path.Matcher scopeMatcher) { + sessionContext = ctx; startDir = dir; pathHelper = pathFactory; dirQueue = new LinkedList(); fileQueue = new LinkedList(); scope = scopeMatcher; + execCap = Internals.checkSupportsExecutables(startDir); + linkCap = Internals.checkSupportsSymlinks(startDir); reset(); } @@ -60,7 +69,7 @@ fileQueue.clear(); dirQueue.clear(); dirQueue.add(startDir); - nextFile = new RegularFileInfo(); + nextFile = new RegularFileInfo(sessionContext, supportsExecFlag(), supportsLinkFlag()); nextPath = null; } @@ -90,6 +99,14 @@ return scope == null ? true : scope.accept(file); } + public boolean supportsExecFlag() { + return execCap; + } + + public boolean supportsLinkFlag() { + return linkCap; + } + // returns non-null private File[] listFiles(File f) { // in case we need to solve os-related file issues (mac with some encodings?) @@ -113,7 +130,11 @@ continue; } if (isDir) { - if (!".hg/".equals(path.toString())) { + // do not dive into /.hg and + // if there's .hg/ under f/, it's a nested repository, which shall not be walked into + if (".hg".equals(f.getName()) || new File(f, ".hg").isDirectory()) { + continue; + } else { dirQueue.addLast(f); } } else { diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/LogFacility.java --- a/src/org/tmatesoft/hg/util/LogFacility.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/util/LogFacility.java Wed Jul 11 20:40:47 2012 +0200 @@ -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,10 +16,9 @@ */ package org.tmatesoft.hg.util; -import org.tmatesoft.hg.internal.Experimental; /** - * WORK IN PROGRESS + * Facility to dump various messages. * * Intention of this class is to abstract away almost any log facility out there clients might be using with the Hg4J library, * not to be a full-fledged logging facility of its own. @@ -30,21 +29,41 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -@Experimental(reason="API might get changed") public interface LogFacility { - - boolean isDebug(); - boolean isInfo(); + + public enum Severity { + Debug, Info, Warn, Error // order is important + } - // src and format never null - void debug(Class src, String format, Object... args); - void info(Class src, String format, Object... args); - void warn(Class src, String format, Object... args); - void error(Class src, String format, Object... args); + /** + * Effective way to avoid attempts to construct debug dumps when they are of no interest. Basically, getLevel() < Info + * + * @return true if interested in debug dumps + */ + boolean isDebug(); - // src shall be non null, either th or message or both - void debug(Class src, Throwable th, String message); - void info(Class src, Throwable th, String message); - void warn(Class src, Throwable th, String message); - void error(Class src, Throwable th, String message); + /** + * + * @return lowest (from {@link Severity#Debug} to {@link Severity#Error} active severity level + */ + Severity getLevel(); + + /** + * Dump a message + * @param src identifies source of the message, never null + * @param severity one of predefined levels + * @param format message format suitable for {@link String#format(String, Object...)}, never null + * @param args optional arguments for the preceding format argument, may be null + */ + void dump(Class src, Severity severity, String format, Object... args); + + /** + * Alternative to dump an exception + * + * @param src identifies source of the message, never null + * @param severity one of predefined levels + * @param th original exception, never null + * @param message additional description of the error/conditions, may be null + */ + void dump(Class src, Severity severity, Throwable th, String message); } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/Outcome.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/util/Outcome.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,60 @@ +/* + * 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 + * 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.util; + +/** + * Success/failure descriptor. When exception is too much. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public final class Outcome { + // XXX perhaps private enum and factory method createError() and createOk()? + public enum Kind { + Success, Failure; + } + + private final Kind kind; + private final String message; + private final Exception error; + + public Outcome(Kind k, String msg) { + this(k, msg, null); + } + + public Outcome(Kind k, String msg, Exception err) { + kind = k; + message = msg; + error = err; + } + + public boolean isOk() { + return kind == Kind.Success; + } + + public Kind getKind() { + return kind; + } + + public String getMessage() { + return message; + } + + public Exception getException() { + return error; + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/Pair.java --- a/src/org/tmatesoft/hg/util/Pair.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/util/Pair.java Wed Jul 11 20:40:47 2012 +0200 @@ -16,7 +16,6 @@ */ package org.tmatesoft.hg.util; -import org.tmatesoft.hg.internal.Experimental; /** * Nothing but a holder for two values. @@ -24,7 +23,6 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -@Experimental public final class Pair { private final T1 value1; private final T2 value2; diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/Path.java --- a/src/org/tmatesoft/hg/util/Path.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/util/Path.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -118,11 +118,11 @@ } public enum CompareResult { - Same, Unrelated, Nested, Parent, /* perhaps, also ImmediateParent, DirectChild? */ + Same, Unrelated, ImmediateChild, Nested, ImmediateParent, Parent /* +CommonParent ?*/ } - /* - * a/file and a/dir ? + /** + * @return one of {@link CompareResult} constants to indicate relations between the paths */ public CompareResult compareWith(Path another) { if (another == null) { @@ -131,14 +131,23 @@ if (another == this || (another.length() == length() && equals(another))) { return CompareResult.Same; } - if (path.startsWith(another.path)) { - return CompareResult.Nested; + // one of the parties can't be parent in parent/nested, the other may be either file or folder + if (another.isDirectory() && path.startsWith(another.path)) { + return isOneSegmentDifference(path, another.path) ? CompareResult.ImmediateChild : CompareResult.Nested; } - if (another.path.startsWith(path)) { - return CompareResult.Parent; + if (isDirectory() && another.path.startsWith(path)) { + return isOneSegmentDifference(another.path, path) ? CompareResult.ImmediateParent : CompareResult.Parent; } return CompareResult.Unrelated; } + + // true if p1 is only one segment larger than p2 + private static boolean isOneSegmentDifference(String p1, String p2) { + assert p1.startsWith(p2); + String p1Tail= p1.substring(p2.length()); + int slashLoc = p1Tail.indexOf('/'); + return slashLoc == -1 || slashLoc == p1Tail.length() - 1; + } public static Path create(CharSequence path) { if (path == null) { @@ -187,24 +196,36 @@ * Factory for paths */ public interface Source { - Path path(String p); + Path path(CharSequence p); } - + /** * Straightforward {@link Source} implementation that creates new Path instance for each supplied string + * and optionally piping through a converter to get e.g. cached instance */ public static class SimpleSource implements Source { private final PathRewrite normalizer; + private final Convertor convertor; + + public SimpleSource() { + this(new PathRewrite.Empty(), null); + } public SimpleSource(PathRewrite pathRewrite) { - if (pathRewrite == null) { - throw new IllegalArgumentException(); - } - normalizer = pathRewrite; + this(pathRewrite, null); } - public Path path(String p) { - return Path.create(normalizer.rewrite(p)); + public SimpleSource(PathRewrite pathRewrite, Convertor pathConvertor) { + normalizer = pathRewrite; + convertor = pathConvertor; + } + + public Path path(CharSequence p) { + Path rv = Path.create(normalizer.rewrite(p)); + if (convertor != null) { + return convertor.mangle(rv); + } + return rv; } } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/PathPool.java --- a/src/org/tmatesoft/hg/util/PathPool.java Thu Jun 21 21:36:06 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,87 +0,0 @@ -/* - * Copyright (c) 2011 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.util; - -import java.lang.ref.SoftReference; -import java.util.WeakHashMap; - - -/** - * Produces path from strings and caches result for reuse - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class PathPool implements Path.Source { - private final WeakHashMap> cache; - private final PathRewrite pathRewrite; - - public PathPool(PathRewrite rewrite) { - pathRewrite = rewrite; - cache = new WeakHashMap>(); - } - - public Path path(String p) { - p = pathRewrite.rewrite(p).toString(); - return get(p, true); - } - - // pipes path object through cache to reuse instance, if possible - // TODO unify with Pool - public Path path(Path p) { - String s = pathRewrite.rewrite(p).toString(); - Path cached = get(s, false); - if (cached == null) { - cache.put(s, new SoftReference(cached = p)); - } - return cached; - } - - // XXX what would be parent of an empty path? - // Path shall have similar functionality - public Path parent(Path path) { - if (path.length() == 0) { - throw new IllegalArgumentException(); - } - for (int i = path.length() - 2 /*if path represents a dir, trailing char is slash, skip*/; i >= 0; i--) { - if (path.charAt(i) == '/') { - return get(path.subSequence(0, i+1).toString(), true); - } - } - return get("", true); - } - - // invoke when path pool is no longer in use, to ease gc work - public void clear() { - cache.clear(); - } - - private Path get(String p, boolean create) { - SoftReference sr = cache.get(p); - Path path = sr == null ? null : sr.get(); - if (path == null) { - if (create) { - path = Path.create(p); - cache.put(p, new SoftReference(path)); - } else if (sr != null) { - // cached path no longer used, clear cache entry - do not wait for RefQueue to step in - cache.remove(p); - } - } - return path; - } -} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/ProgressSupport.java --- a/src/org/tmatesoft/hg/util/ProgressSupport.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/util/ProgressSupport.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -26,7 +26,7 @@ // -1 for unspecified? public void start(int totalUnits); - public void worked(int units); + public void worked(int units); // fraction of totalUnits from #start(int) // XXX have to specify whether PS implementors may expect #done regardless of job completion (i.e. in case of cancellation) public void done(); @@ -53,10 +53,12 @@ } class Sub implements ProgressSupport { + private int perChildWorkUnitMultiplier; // to multiply child ps units + private int perChildWorkUnitDivisor; // to scale down to parent ps units + private int unitsConsumed; // parent ps units consumed so far + private int fraction = 0; // leftovers of previous not completely consumed work units private final ProgressSupport ps; - private int total; - private int units; - private int psUnits; + private final int psUnits; // total parent ps units public Sub(ProgressSupport parent, int parentUnits) { if (parent == null) { @@ -67,23 +69,27 @@ } public void start(int totalUnits) { - total = totalUnits; +// perChildWorkUnit = (psUnits*100) / totalUnits; + perChildWorkUnitDivisor = 10 * totalUnits; + perChildWorkUnitMultiplier = psUnits * perChildWorkUnitDivisor / totalUnits; + } public void worked(int worked) { - // FIXME fine-grained subprogress report. now only report at about 50% - if (psUnits > 1 && units < total/2 && units+worked > total/2) { - ps.worked(psUnits/2); - psUnits -= psUnits/2; + int x = fraction + worked * perChildWorkUnitMultiplier; + int u = x / perChildWorkUnitDivisor; + fraction = x % perChildWorkUnitDivisor; + if (u > 0) { + ps.worked(u); + unitsConsumed += u; } - units += worked; } public void done() { - ps.worked(psUnits); + ps.worked(psUnits - unitsConsumed); } } - + interface Target { T set(ProgressSupport ps); } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/RegularFileInfo.java --- a/src/org/tmatesoft/hg/util/RegularFileInfo.java Thu Jun 21 21:36:06 2012 +0200 +++ b/src/org/tmatesoft/hg/util/RegularFileInfo.java Wed Jul 11 20:40:47 2012 +0200 @@ -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,13 +16,17 @@ */ package org.tmatesoft.hg.util; +import static org.tmatesoft.hg.util.LogFacility.Severity.Info; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; import java.nio.channels.ReadableByteChannel; +import org.tmatesoft.hg.core.SessionContext; import org.tmatesoft.hg.internal.StreamLogFacility; /** @@ -31,48 +35,111 @@ * @author TMate Software Ltd. */ public class RegularFileInfo implements FileInfo { + private final boolean supportsExec, supportsLink; + private final RegularFileStats fileFlagsHelper; // null if both supportsLink and supportExec are false private File file; - public RegularFileInfo() { + public RegularFileInfo(SessionContext ctx) { + this(ctx, false, false); + } + public RegularFileInfo(SessionContext ctx, boolean supportExecFlag, boolean supportSymlink) { + supportsLink = supportSymlink; + supportsExec = supportExecFlag; + if (supportSymlink || supportExecFlag) { + fileFlagsHelper = new RegularFileStats(ctx); + } else { + fileFlagsHelper = null; + } } public void init(File f) { file = f; + if (fileFlagsHelper != null) { + fileFlagsHelper.init(file); + } } public boolean exists() { - return file.canRead() && file.isFile(); + // java.io.File for symlinks without proper target says it doesn't exist. + // since we found this symlink in directory listing, it's safe to say it exists just based on the fact it's link + return isSymlink() || (file.canRead() && file.isFile()); } public int lastModified() { + // TODO post-1.0 for symlinks, this returns incorrect mtime of the target file, not that of link itself + // Besides, timestame if link points to non-existing file is 0. + // However, it result only in slowdown in WCStatusCollector, as it need to perform additional content check return (int) (file.lastModified() / 1000); } public long length() { + if (isSymlink()) { + return getLinkTargetBytes().length; + } return file.length(); } public ReadableByteChannel newInputChannel() { try { - return new FileInputStream(file).getChannel(); + if (isSymlink()) { + return new ByteArrayReadableChannel(getLinkTargetBytes()); + } else { + return new FileInputStream(file).getChannel(); + } } catch (FileNotFoundException ex) { - StreamLogFacility.newDefault().debug(getClass(), ex, null); + StreamLogFacility.newDefault().dump(getClass(), Info, ex, null); // shall not happen, provided this class is used correctly - return new ReadableByteChannel() { - - public boolean isOpen() { - return true; - } - - public void close() throws IOException { - } - - public int read(ByteBuffer dst) throws IOException { - // EOF right away - return -1; - } - }; + return new ByteArrayReadableChannel(null); } } + public boolean isExecutable() { + return supportsExec && fileFlagsHelper.isExecutable(); + } + + public boolean isSymlink() { + return supportsLink && fileFlagsHelper.isSymlink(); + } + + private byte[] getLinkTargetBytes() { + assert isSymlink(); + // no idea what encoding Mercurial uses for link targets, assume platform native is ok + return fileFlagsHelper.getSymlinkTarget().getBytes(); + } + + + private static class ByteArrayReadableChannel implements ReadableByteChannel { + private final byte[] data; + private boolean closed = false; // initially open + private int firstAvailIndex = 0; + + ByteArrayReadableChannel(byte[] dataToStream) { + data = dataToStream; + } + + public boolean isOpen() { + return !closed; + } + + public void close() throws IOException { + closed = true; + } + + public int read(ByteBuffer dst) throws IOException { + if (closed) { + throw new ClosedChannelException(); + } + int remainingBytes = data.length - firstAvailIndex; + if (data == null || remainingBytes == 0) { + // EOF right away + return -1; + } + int x = Math.min(dst.remaining(), remainingBytes); + for (int i = firstAvailIndex, lim = firstAvailIndex + x; i < lim; i++) { + dst.put(data[i]); + } + firstAvailIndex += x; + return x; + } + } } diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/RegularFileStats.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/util/RegularFileStats.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,169 @@ +/* + * Copyright (c) 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 + * 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.util; + +import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.tmatesoft.hg.core.SessionContext; +import org.tmatesoft.hg.internal.Internals; +import org.tmatesoft.hg.internal.ProcessExecHelper; + +/** + * Utility to collect executable files and symbolic links in a directory. + * + * + * Not public as present approach (expect file but collect once per directory) may need to be made explicit + * + * TODO post-1.0 Add Linux-specific set of tests (similar to my test-flags repository, with symlink, executable and regular file, + * and few revisions where link and exec flags change. +testcase when link points to non-existing file (shall not report as missing, + * iow either FileInfo.exist() shall respect symlinks or WCSC account for ) + * + * TODO post-1.0 Add extraction of link modification time, see RegularFileInfo#lastModified() + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +/*package-local*/ class RegularFileStats { + private boolean isExec, isSymlink; + private String symlinkValue; + private final List command; + private final ProcessExecHelper execHelper; + private final Matcher linkMatcher, execMatcher; + private final SessionContext sessionContext; + + + // directory name to (short link name -> link target) + private Map> dir2links = new TreeMap>(); + // directory name to set of executable file short names + private Map> dir2execs = new TreeMap>(); + + + RegularFileStats(SessionContext ctx) { + sessionContext = ctx; + if (Internals.runningOnWindows()) { + // XXX this implementation is not yet tested against any Windows repository, + // only against sample dir listings. As long as Mercurial doesn't handle Windows + // links, we don't really need this + command = Arrays.asList("cmd", "/c", "dir"); + // Windows patterns need to work against full directory listing (I didn't find a way + // to list single file with its attributes like SYMLINK) + Pattern pLink = Pattern.compile("^\\S+.*\\s+\\s+(\\S.*)\\s+\\[(.+)\\]$", Pattern.MULTILINE); + Pattern pExec = Pattern.compile("^\\S+.*\\s+\\d+\\s+(\\S.*\\.exe)$", Pattern.MULTILINE); + linkMatcher = pLink.matcher(""); + execMatcher = pExec.matcher(""); + } else { + command = Arrays.asList("/bin/ls", "-l", "-Q"); // -Q is essential to get quoted name - the only way to + // tell exact file name (which may start or end with spaces. + Pattern pLink = Pattern.compile("^lrwxrwxrwx\\s.*\\s\"(.*)\"\\s+->\\s+\"(.*)\"$", Pattern.MULTILINE); + // pLink: group(1) is full name if single file listing (ls -l /usr/bin/java) and short name if directory listing (ls -l /usr/bin) + // group(2) is link target + Pattern pExec = Pattern.compile("^-..[sx]..[sx]..[sx]\\s.*\\s\"(.+)\"$", Pattern.MULTILINE); + // pExec: group(1) is name of executable file + linkMatcher = pLink.matcher(""); + execMatcher = pExec.matcher(""); + } + execHelper = new ProcessExecHelper(); + } + + /** + * Fails silently indicating false for both x and l in case interaction with file system failed + * @param f file to check, doesn't need to exist + */ + public void init(File f) { + isExec = isSymlink = false; + symlinkValue = null; + // + // can't check isFile because Java would say false for a symlink with non-existing target + if (f.isDirectory()) { + // perhaps, shall just collect stats for all files and set false to exec/link flags? + throw new IllegalArgumentException(); + } + final String dirName = f.getParentFile().getAbsolutePath(); + final String fileName = f.getName(); + Map links = dir2links.get(dirName); + Set execs = dir2execs.get(dirName); + if (links == null || execs == null) { + try { + ArrayList cmd = new ArrayList(command); + cmd.add(dirName); + CharSequence result = execHelper.exec(cmd); + + if (execMatcher.reset(result).find()) { + execs = new HashSet(); + do { + execs.add(execMatcher.group(1)); + } while (execMatcher.find()); + } else { + execs = Collections.emptySet(); // indicate we tried and found nothing + } + if (linkMatcher.reset(result).find()) { + links = new HashMap(); + do { + links.put(linkMatcher.group(1), linkMatcher.group(2)); + } while (linkMatcher.find()); + } else { + links = Collections.emptyMap(); + } + dir2links.put(dirName, links); + dir2execs.put(dirName, execs); + isExec = execs.contains(fileName); + isSymlink = links.containsKey(fileName); + if (isSymlink) { + symlinkValue = links.get(fileName); + } else { + symlinkValue = null; + } + } catch (InterruptedException ex) { + sessionContext.getLog().dump(getClass(), Warn, ex, String.format("Failed to detect flags for %s", f)); + // try again? ensure not too long? stop right away? + // IGNORE, keep isExec and isSymlink false + } catch (IOException ex) { + sessionContext.getLog().dump(getClass(), Warn, ex, String.format("Failed to detect flags for %s", f)); + // IGNORE, keep isExec and isSymlink false + } + } + } + + public boolean isExecutable() { + return isExec; + } + + public boolean isSymlink() { + return isSymlink; + } + + public String getSymlinkTarget() { + if (isSymlink) { + return symlinkValue; + } + throw new UnsupportedOperationException(); + } +} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/Status.java --- a/src/org/tmatesoft/hg/util/Status.java Thu Jun 21 21:36:06 2012 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2011 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.util; - -import org.tmatesoft.hg.internal.Experimental; - -/** - * Success/failure descriptor. When exception is too much. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -@Experimental(reason="Accidental use, does not justify dedicated class, perhaps.") -public class Status { - // XXX perhaps private enum and factory method createError() and createOk()? - public enum Kind { - OK, ERROR; - } - - private final Kind kind; - private final String message; - private final Exception error; - - public Status(Kind k, String msg) { - this(k, msg, null); - } - - public Status(Kind k, String msg, Exception err) { - kind = k; - message = msg; - error = err; - } - - public boolean isOk() { - return kind == Kind.OK; - } - - public Kind getKind() { - return kind; - } - - public String getMessage() { - return message; - } - - public Exception getException() { - return error; - } -} diff -r 2078692eeb58 -r 7bcfbc255f48 src/org/tmatesoft/hg/util/package.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/util/package.html Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,6 @@ + + +

Utility API

+

Miscellaneous utility classes not related directly to Mercurial repositories

+ + \ No newline at end of file diff -r 2078692eeb58 -r 7bcfbc255f48 test-data/test-repos.jar Binary file test-data/test-repos.jar has changed diff -r 2078692eeb58 -r 7bcfbc255f48 test/org/tmatesoft/hg/test/ErrorCollectorExt.java --- a/test/org/tmatesoft/hg/test/ErrorCollectorExt.java Thu Jun 21 21:36:06 2012 +0200 +++ b/test/org/tmatesoft/hg/test/ErrorCollectorExt.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -20,6 +20,7 @@ import java.util.concurrent.Callable; +import org.hamcrest.CoreMatchers; import org.hamcrest.Matcher; import org.junit.internal.runners.model.MultipleFailureException; import org.junit.rules.ErrorCollector; @@ -51,6 +52,10 @@ }); } + public void assertTrue(final boolean value) { + assertTrue(null, value); + } + public void assertTrue(final String reason, final boolean value) { checkSucceeds(new Callable() { public Object call() throws Exception { @@ -59,4 +64,8 @@ } }); } + + public void assertEquals(T expected, T actual) { + checkThat(null, actual, CoreMatchers.equalTo(expected)); + } } \ No newline at end of file diff -r 2078692eeb58 -r 7bcfbc255f48 test/org/tmatesoft/hg/test/ExecHelper.java --- a/test/org/tmatesoft/hg/test/ExecHelper.java Thu Jun 21 21:36:06 2012 +0200 +++ b/test/org/tmatesoft/hg/test/ExecHelper.java Wed Jul 11 20:40:47 2012 +0200 @@ -18,84 +18,55 @@ import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.CharBuffer; import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedList; +import java.util.List; import java.util.StringTokenizer; +import org.tmatesoft.hg.internal.ProcessExecHelper; + /** * * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class ExecHelper { +public class ExecHelper extends ProcessExecHelper { private final OutputParser parser; - private File dir; - private int exitValue; public ExecHelper(OutputParser outParser, File workingDir) { parser = outParser; - dir = workingDir; + super.cwd(workingDir); } - - public void run(String... cmd) throws IOException, InterruptedException { - ProcessBuilder pb = null; + + @Override + protected List prepareCommand(List cmd) { + String commandName = cmd.get(0); if (System.getProperty("os.name").startsWith("Windows")) { StringTokenizer st = new StringTokenizer(System.getenv("PATH"), ";"); while (st.hasMoreTokens()) { File pe = new File(st.nextToken()); - if (new File(pe, cmd[0] + ".exe").exists()) { - break; + if (new File(pe, commandName + ".exe").exists()) { + return cmd; } // 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()) { + if (new File(pe, commandName + ".bat").exists() || new File(pe, commandName + ".cmd").exists()) { ArrayList command = new ArrayList(); command.add("cmd.exe"); command.add("/C"); - command.addAll(Arrays.asList(cmd)); - pb = new ProcessBuilder(command); - break; + command.addAll(cmd); + return command; } } } - if (pb == null) { - pb = new ProcessBuilder(cmd); - } - Process p = pb.directory(dir).redirectErrorStream(true).start(); - InputStreamReader stdOut = new InputStreamReader(p.getInputStream()); - LinkedList l = new LinkedList(); - int r = -1; - CharBuffer b = null; - do { - if (b == null || b.remaining() < b.capacity() / 3) { - b = CharBuffer.allocate(512); - l.add(b); - } - r = stdOut.read(b); - } while (r != -1); - int total = 0; - for (CharBuffer cb : l) { - total += cb.position(); - cb.flip(); - } - CharBuffer res = CharBuffer.allocate(total); - for (CharBuffer cb : l) { - res.put(cb); - } - res.flip(); - p.waitFor(); - exitValue = p.exitValue(); + return super.prepareCommand(cmd); + } + + public void run(String... cmd) throws IOException, InterruptedException { + CharSequence res = super.exec(cmd); parser.parse(res); } - + public int getExitValue() { - return exitValue; - } - - public void cwd(File wd) { - dir = wd; + return super.exitValue(); } } diff -r 2078692eeb58 -r 7bcfbc255f48 test/org/tmatesoft/hg/test/MapTagsToFileRevisions.java --- a/test/org/tmatesoft/hg/test/MapTagsToFileRevisions.java Thu Jun 21 21:36:06 2012 +0200 +++ b/test/org/tmatesoft/hg/test/MapTagsToFileRevisions.java Wed Jul 11 20:40:47 2012 +0200 @@ -5,13 +5,14 @@ import java.io.File; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.junit.Assert; -import org.tmatesoft.hg.core.HgBadStateException; +import org.tmatesoft.hg.core.HgCallbackTargetException; import org.tmatesoft.hg.core.HgChangeset; import org.tmatesoft.hg.core.HgChangesetHandler; import org.tmatesoft.hg.core.HgException; @@ -27,6 +28,7 @@ import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.repo.HgTags; import org.tmatesoft.hg.repo.HgTags.TagInfo; +import org.tmatesoft.hg.repo.HgRevisionMap; import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.Path; @@ -40,12 +42,12 @@ public static void main(String[] args) throws Exception { MapTagsToFileRevisions m = new MapTagsToFileRevisions(); System.out.printf("Free mem: %,d\n", Runtime.getRuntime().freeMemory()); - m.measurePatchAffectsArbitraryRevisionRead(); +// m.measurePatchAffectsArbitraryRevisionRead(); // m.collectTagsPerFile(); // m.manifestWalk(); // m.changelogWalk(); // m.revisionMap(); -// m.buildFile2ChangelogRevisionMap(); + m.buildFile2ChangelogRevisionMap(".hgtags", "README", "configure.in", "Misc/NEWS"); m = null; System.gc(); System.out.printf("Free mem: %,d\n", Runtime.getRuntime().freeMemory()); @@ -54,7 +56,7 @@ // revision == 2406 - 5 ms per run (baseRevision == 2406) // revision == 2405 - 69 ms per run (baseRevision == 1403) - private void measurePatchAffectsArbitraryRevisionRead() throws Exception { + public void measurePatchAffectsArbitraryRevisionRead() throws Exception { final HgRepository repository = new HgLookup().detect(new File("/temp/hg/cpython")); final DoNothingManifestInspector insp = new DoNothingManifestInspector(); final int revision = 2405; @@ -80,60 +82,96 @@ * Approach 1: total 409, init: 1, iteration: 407 * Approach 2: total 277, init: 203, iteration: 74 */ - private void buildFile2ChangelogRevisionMap() throws Exception { - final HgRepository repository = new HgLookup().detect(new File("/temp/hg/cpython")); + /* New data, 0.9.0v (another CPU!) + *.hgtags, 306 revisions + * Approach 0: total 136 + * Approach 1: total 53, init: 1, iteration: 52 + * Approach 2: total 95, init: 78, iteration: 17 + * Approach 3: total 17 + * + * README, 499 revisions + * Approach 0: total 149 + * Approach 1: total 43, init: 0, iteration: 43 + * Approach 2: total 102, init: 86, iteration: 16 + * Approach 3: total 18 + * + * configure.in, 1170 revisions + * Approach 0: total 321 + * Approach 1: total 116, init: 0, iteration: 115 + * Approach 2: total 140, init: 79, iteration: 60 + * Approach 3: total 30 + * + * Misc/NEWS, 10863 revisions + * Approach 0: total 4946 + * Approach 1: total 309, init: 6, iteration: 302 + * Approach 2: total 213, init: 63, iteration: 150 + * Approach 3: total 140 + */ + private void buildFile2ChangelogRevisionMap(String... fileNames) throws Exception { + final HgRepository repository = new HgLookup().detect(new File("/home/artem/hg/cpython")); final HgChangelog clog = repository.getChangelog(); - final HgDataFile fileNode = repository.getFileNode("configure.in"); // warm-up - HgChangelog.RevisionMap clogMap = clog.new RevisionMap().init(); - HgDataFile.RevisionMap fileMap = fileNode.new RevisionMap().init(); - // - final int latestRevision = fileNode.getLastRevision(); - // - final long start_1 = System.nanoTime(); - fileMap = fileNode.new RevisionMap().init(); - final long start_1a = System.nanoTime(); - final Map changesetToNodeid_1 = new HashMap(); - for (int revision = 0; revision <= latestRevision; revision++) { - final Nodeid nodeId = fileMap.revision(revision); -// final Nodeid changesetId = fileNode.getChangesetRevision(nodeId); - int localCset = fileNode.getChangesetRevisionIndex(revision); - final Nodeid changesetId = clog.getRevision(localCset); - changesetToNodeid_1.put(changesetId, nodeId); - } - final long end_1 = System.nanoTime(); - // - final long start_2 = System.nanoTime(); - clogMap = clog.new RevisionMap().init(); - fileMap = fileNode.new RevisionMap().init(); - final Map changesetToNodeid_2 = new HashMap(); - final long start_2a = System.nanoTime(); - for (int revision = 0; revision <= latestRevision; revision++) { - Nodeid nidFile = fileMap.revision(revision); - int localCset = fileNode.getChangesetRevisionIndex(revision); - Nodeid nidCset = clogMap.revision(localCset); - changesetToNodeid_2.put(nidCset, nidFile); - } - final long end_2 = System.nanoTime(); - Assert.assertEquals(changesetToNodeid_1, changesetToNodeid_2); - // - final long start_3 = System.nanoTime(); - final Map changesetToNodeid_3 = new HashMap(); - fileNode.walk(0, TIP, new HgDataFile.RevisionInspector() { + HgRevisionMap clogMap = new HgRevisionMap(clog).init(); - public void next(int fileRevisionIndex, Nodeid revision, int linkedRevisionIndex) { - try { + for (String fname : fileNames) { + HgDataFile fileNode = repository.getFileNode(fname); + // warm-up + HgRevisionMap fileMap = new HgRevisionMap(fileNode).init(); + // + final int latestRevision = fileNode.getLastRevision(); + // + final long start_0 = System.nanoTime(); + final Map changesetToNodeid_0 = new HashMap(); + for (int fileRevisionIndex = 0; fileRevisionIndex <= latestRevision; fileRevisionIndex++) { + Nodeid fileRevision = fileNode.getRevision(fileRevisionIndex); + Nodeid changesetRevision = fileNode.getChangesetRevision(fileRevision); + changesetToNodeid_0.put(changesetRevision, fileRevision); + } + final long end_0 = System.nanoTime(); + // + final long start_1 = System.nanoTime(); + fileMap = new HgRevisionMap(fileNode).init(); + final long start_1a = System.nanoTime(); + final Map changesetToNodeid_1 = new HashMap(); + for (int revision = 0; revision <= latestRevision; revision++) { + final Nodeid nodeId = fileMap.revision(revision); + int localCset = fileNode.getChangesetRevisionIndex(revision); + final Nodeid changesetId = clog.getRevision(localCset); +// final Nodeid changesetId = fileNode.getChangesetRevision(nodeId); + changesetToNodeid_1.put(changesetId, nodeId); + } + final long end_1 = System.nanoTime(); + // + final long start_2 = System.nanoTime(); + clogMap = new HgRevisionMap(clog).init(); + fileMap = new HgRevisionMap(fileNode).init(); + final Map changesetToNodeid_2 = new HashMap(); + final long start_2a = System.nanoTime(); + for (int revision = 0; revision <= latestRevision; revision++) { + Nodeid nidFile = fileMap.revision(revision); + int localCset = fileNode.getChangesetRevisionIndex(revision); + Nodeid nidCset = clogMap.revision(localCset); + changesetToNodeid_2.put(nidCset, nidFile); + } + final long end_2 = System.nanoTime(); + Assert.assertEquals(changesetToNodeid_1, changesetToNodeid_2); + // + final long start_3 = System.nanoTime(); + final Map changesetToNodeid_3 = new HashMap(); + fileNode.indexWalk(0, TIP, new HgDataFile.RevisionInspector() { + + public void next(int fileRevisionIndex, Nodeid revision, int linkedRevisionIndex) { changesetToNodeid_3.put(clog.getRevision(linkedRevisionIndex), revision); - } catch (HgException ex) { - ex.printStackTrace(); } - } - }); - final long end_3 = System.nanoTime(); - Assert.assertEquals(changesetToNodeid_1, changesetToNodeid_3); - System.out.printf("Approach 1: total %d, init: %d, iteration: %d\n", (end_1 - start_1)/1000000, (start_1a - start_1)/1000000, (end_1 - start_1a)/1000000); - System.out.printf("Approach 2: total %d, init: %d, iteration: %d\n", (end_2 - start_2)/1000000, (start_2a - start_2)/1000000, (end_2 - start_2a)/1000000); - System.out.printf("Approach 3: total %d\n", (end_3 - start_3)/1000000); + }); + final long end_3 = System.nanoTime(); + Assert.assertEquals(changesetToNodeid_1, changesetToNodeid_3); + System.out.printf("%s, %d revisions\n", fname, 1+latestRevision); + System.out.printf("Approach 0: total %d\n", (end_0 - start_0)/1000000); + System.out.printf("Approach 1: total %d, init: %d, iteration: %d\n", (end_1 - start_1)/1000000, (start_1a - start_1)/1000000, (end_1 - start_1a)/1000000); + System.out.printf("Approach 2: total %d, init: %d, iteration: %d\n", (end_2 - start_2)/1000000, (start_2a - start_2)/1000000, (end_2 - start_2a)/1000000); + System.out.printf("Approach 3: total %d\n", (end_3 - start_3)/1000000); + } } /* @@ -144,7 +182,7 @@ * each 2000'th revision, total 36 revision: 620 vs 270 * each 3000'th revision, total 24 revision: 410 vs 275 */ - private void revisionMap() throws Exception { + public void revisionMap() throws Exception { final HgRepository repository = new HgLookup().detect(new File("/temp/hg/cpython")); final HgChangelog clog = repository.getChangelog(); ArrayList revisions = new ArrayList(); @@ -160,7 +198,7 @@ } } System.out.printf("Direct lookup of %d revisions took %,d ns\n", revisions.size(), System.nanoTime() - s1); - HgChangelog.RevisionMap rmap = clog.new RevisionMap(); + HgRevisionMap rmap = new HgRevisionMap(clog); final long s2 = System.nanoTime(); rmap.init(); final long s3 = System.nanoTime(); @@ -173,7 +211,7 @@ System.out.printf("RevisionMap time: %d ms, of that init() %,d ns\n", (System.nanoTime() - s2) / 1000000, s3 - s2); } - private void changelogWalk() throws Exception { + public void changelogWalk() throws Exception { final HgRepository repository = new HgLookup().detect(new File("/temp/hg/cpython")); final long start = System.currentTimeMillis(); repository.getChangelog().all(new HgChangelog.Inspector() { @@ -193,7 +231,7 @@ System.out.printf("Free mem: %,d\n", Runtime.getRuntime().freeMemory()); } - private void manifestWalk() throws Exception { + public void manifestWalk() throws Exception { System.out.println(System.getProperty("java.version")); final long start = System.currentTimeMillis(); final HgRepository repository = new HgLookup().detect(new File("/temp/hg/cpython")); @@ -211,7 +249,7 @@ System.out.printf("Free mem: %,d\n", Runtime.getRuntime().freeMemory()); } - private int[] collectLocalTagRevisions(HgChangelog.RevisionMap clogrmap, TagInfo[] allTags, IntMap> tagLocalRev2TagInfo) { + private int[] collectLocalTagRevisions(HgRevisionMap clogrmap, TagInfo[] allTags, IntMap> tagLocalRev2TagInfo) { int[] tagLocalRevs = new int[allTags.length]; int x = 0; for (int i = 0; i < allTags.length; i++) { @@ -235,17 +273,17 @@ return tagLocalRevs; } - private void collectTagsPerFile() throws HgException, CancelledException { + public void collectTagsPerFile() throws HgException, CancelledException { final long start = System.currentTimeMillis(); - final HgRepository repository = new HgLookup().detect(new File("/temp/hg/cpython")); + final HgRepository repository = new HgLookup().detect(new File("/home/artem/hg/cpython")); final HgTags tags = repository.getTags(); // // build cache // - final TagInfo[] allTags = new TagInfo[tags.getTags().size()]; - tags.getTags().values().toArray(allTags); + final TagInfo[] allTags = new TagInfo[tags.getAllTags().size()]; + tags.getAllTags().values().toArray(allTags); // effective translation of changeset revisions to their local indexes - final HgChangelog.RevisionMap clogrmap = repository.getChangelog().new RevisionMap().init(); + final HgRevisionMap clogrmap = new HgRevisionMap(repository.getChangelog()).init(); // map to look up tag by changeset local number final IntMap> tagLocalRev2TagInfo = new IntMap>(allTags.length); System.out.printf("Collecting manifests for %d tags\n", allTags.length); @@ -258,18 +296,18 @@ System.out.printf("Total time: %d ms\n", System.currentTimeMillis() - start); System.out.println("\nApproach 2"); - collectTagsPerFile_Approach_2(repository, tagLocalRevs, tagLocalRev2TagInfo, allTags, targetPath); + collectTagsPerFile_Approach_2(repository, tagLocalRevs, tagLocalRev2TagInfo, targetPath); } // Approach 1. Build map with all files, their revisions and corresponding tags // - private void collectTagsPerFile_Approach_1(final HgChangelog.RevisionMap clogrmap, final int[] tagLocalRevs, final TagInfo[] allTags, Path targetPath) throws HgException { + private void collectTagsPerFile_Approach_1(final HgRevisionMap clogrmap, final int[] tagLocalRevs, final TagInfo[] allTags, Path targetPath) throws HgException { HgRepository repository = clogrmap.getRepo(); final long start = System.currentTimeMillis(); // file2rev2tag value is array of revisions, always of allTags.length. Revision index in the array // is index of corresponding TagInfo in allTags; final Map file2rev2tag = new HashMap(); - repository.getManifest().walk(new HgManifest.Inspector2() { + repository.getManifest().walk(new HgManifest.Inspector() { private int[] tagIndexAtRev = new int[4]; // it's unlikely there would be a lot of tags associated with a given cset public boolean begin(int mainfestRevision, Nodeid nid, int changelogRevision) { @@ -293,10 +331,6 @@ } return true; } - - public boolean next(Nodeid nid, String fname, String flags) { - throw new HgBadStateException(HgManifest.Inspector2.class.getName()); - } public boolean next(Nodeid nid, Path fname, HgManifest.Flags flags) { Nodeid[] m = file2rev2tag.get(fname); @@ -339,27 +373,57 @@ } } - private void collectTagsPerFile_Approach_2(HgRepository repository, final int[] tagLocalRevs, final IntMap> tagLocalRev2TagInfo, TagInfo[] allTags, Path targetPath) throws HgException { + private void collectTagsPerFile_Approach_2(HgRepository repository, final int[] tagLocalRevs, final IntMap> tagRevIndex2TagInfo, Path targetPath) throws HgException { // // Approach 2. No all-file map. Collect file revisions recorded at the time of tagging, // then for each file revision check if it is among those above, and if yes, take corresponding tags HgDataFile fileNode = repository.getFileNode(targetPath); final long start2 = System.nanoTime(); - final int lastRev = fileNode.getLastRevision(); - final Map fileRevisionAtTagRevision = repository.getManifest().getFileRevisions(targetPath, tagLocalRevs); + final Map fileRevisionAtTagRevision = new HashMap(); + final Map> fileRev2TagNames = new HashMap>(); + HgManifest.Inspector collectFileRevAtCset = new HgManifest.Inspector() { + + private int csetRevIndex; + + public boolean next(Nodeid nid, Path fname, Flags flags) { + fileRevisionAtTagRevision.put(csetRevIndex, nid); + if (tagRevIndex2TagInfo.containsKey(csetRevIndex)) { + List tags = fileRev2TagNames.get(nid); + if (tags == null) { + fileRev2TagNames.put(nid, tags = new ArrayList(3)); + } + for (TagInfo ti : tagRevIndex2TagInfo.get(csetRevIndex)) { + tags.add(ti.name()); + } + } + return true; + } + + public boolean end(int manifestRevision) { + return true; + } + + public boolean begin(int mainfestRevision, Nodeid nid, int changelogRevision) { + csetRevIndex = changelogRevision; + return true; + } + }; + repository.getManifest().walkFileRevisions(targetPath, collectFileRevAtCset,tagLocalRevs); + final long start2a = System.nanoTime(); - fileNode.walk(0, lastRev, new HgDataFile.RevisionInspector() { + fileNode.indexWalk(0, TIP, new HgDataFile.RevisionInspector() { public void next(int fileRevisionIndex, Nodeid fileRevision, int changesetRevisionIndex) { List associatedTags = new LinkedList(); - for (int taggetRevision : tagLocalRevs) { + + for (int taggedRevision : tagLocalRevs) { // current file revision can't appear in tags that point to earlier changelog revisions (they got own file revision) - if (taggetRevision >= changesetRevisionIndex) { + if (taggedRevision >= changesetRevisionIndex) { // z points to some changeset with tag - Nodeid wasKnownAs = fileRevisionAtTagRevision.get(taggetRevision); + Nodeid wasKnownAs = fileRevisionAtTagRevision.get(taggedRevision); if (wasKnownAs.equals(fileRevision)) { // has tag associated with changeset at index z - List tagsAtRev = tagLocalRev2TagInfo.get(taggetRevision); + List tagsAtRev = tagRevIndex2TagInfo.get(taggedRevision); assert tagsAtRev != null; for (TagInfo ti : tagsAtRev) { associatedTags.add(ti.name()); @@ -367,20 +431,24 @@ } } } + // System.out.printf("%3d%7d%s\n", fileRevisionIndex, changesetRevisionIndex, associatedTags); } }); + for (int i = 0, lastRev = fileNode.getLastRevision(); i <= lastRev; i++) { + Nodeid fileRevision = fileNode.getRevision(i); + List associatedTags2 = fileRev2TagNames.get(fileRevision); + int changesetRevIndex = fileNode.getChangesetRevisionIndex(i); + System.out.printf("%3d%7d%s\n", i, changesetRevIndex, associatedTags2 == null ? Collections.emptyList() : associatedTags2); + } System.out.printf("Alternative total time: %d ms, of that init: %d ms\n", (System.nanoTime() - start2)/1000000, (start2a-start2)/1000000); System.out.printf("Free mem: %,d\n", Runtime.getRuntime().freeMemory()); } - static class DoNothingManifestInspector implements HgManifest.Inspector2 { + static class DoNothingManifestInspector implements HgManifest.Inspector { public boolean begin(int mainfestRevision, Nodeid nid, int changelogRevision) { return true; } - public boolean next(Nodeid nid, String fname, String flags) { - throw new HgBadStateException(HgManifest.Inspector2.class.getName()); - } public boolean next(Nodeid nid, Path fname, Flags flags) { return true; } @@ -389,11 +457,11 @@ } } - public static void main2(String[] args) throws HgException, CancelledException { + public static void main2(String[] args) throws HgCallbackTargetException, HgException, CancelledException { final HgRepository repository = new HgLookup().detect(new File("/temp/hg/cpython")); final Path targetPath = Path.create("README"); final HgTags tags = repository.getTags(); - final Map tagToInfo = tags.getTags(); + final Map tagToInfo = tags.getAllTags(); final HgManifest manifest = repository.getManifest(); final Map> changeSetRevisionToTags = new HashMap>(); final HgDataFile fileNode = repository.getFileNode(targetPath); @@ -418,9 +486,9 @@ final HgLogCommand logCommand = new HgLogCommand(repository); logCommand.file(targetPath, true); logCommand.execute(new HgChangesetHandler() { - public void next(HgChangeset changeset) { + public void cset(HgChangeset changeset) { if (changeset.getAffectedFiles().contains(targetPath)) { - System.out.println(changeset.getRevision() + " " + changeSetRevisionToTags.get(changeset.getNodeid())); + System.out.println(changeset.getRevisionIndex() + " " + changeSetRevisionToTags.get(changeset.getNodeid())); } } }); diff -r 2078692eeb58 -r 7bcfbc255f48 test/org/tmatesoft/hg/test/StatusOutputParser.java --- a/test/org/tmatesoft/hg/test/StatusOutputParser.java Thu Jun 21 21:36:06 2012 +0200 +++ b/test/org/tmatesoft/hg/test/StatusOutputParser.java Wed Jul 11 20:40:47 2012 +0200 @@ -23,9 +23,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.tmatesoft.hg.internal.PathPool; import org.tmatesoft.hg.repo.HgStatusCollector; import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.PathPool; import org.tmatesoft.hg.util.PathRewrite; /** diff -r 2078692eeb58 -r 7bcfbc255f48 test/org/tmatesoft/hg/test/TestAuxUtilities.java --- a/test/org/tmatesoft/hg/test/TestAuxUtilities.java Thu Jun 21 21:36:06 2012 +0200 +++ b/test/org/tmatesoft/hg/test/TestAuxUtilities.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -17,17 +17,19 @@ package org.tmatesoft.hg.test; import static org.tmatesoft.hg.repo.HgRepository.TIP; +import static org.tmatesoft.hg.util.Path.CompareResult.*; import java.io.IOException; import java.nio.ByteBuffer; import org.junit.Assert; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.tmatesoft.hg.core.HgCatCommand; -import org.tmatesoft.hg.core.HgException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.internal.ArrayHelper; +import org.tmatesoft.hg.internal.PathScope; import org.tmatesoft.hg.repo.HgChangelog; import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; import org.tmatesoft.hg.repo.HgDataFile; @@ -51,6 +53,9 @@ */ public class TestAuxUtilities { + @Rule + public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); + @Test public void testArrayHelper() { String[] initial = {"d", "w", "k", "b", "c", "i", "a", "r", "e", "h" }; @@ -152,7 +157,7 @@ @Test public void testManifestCancelSupport() throws Exception { HgRepository repository = Configuration.get().find("branches-1"); // any repo with as many revisions as possible - class InspectorImplementsAdaptable implements HgManifest.Inspector2, Adaptable { + class InspectorImplementsAdaptable implements HgManifest.Inspector, Adaptable { public final int when2stop; public int lastVisitet = 0; private final CancelImpl cancelImpl = new CancelImpl(); @@ -168,10 +173,6 @@ return true; } - public boolean next(Nodeid nid, String fname, String flags) { - return true; - } - public boolean end(int manifestRevision) { return true; } @@ -222,26 +223,22 @@ } @Test - public void testRevlogInspectors() throws Exception { // FIXME move to better place + public void testRevlogInspectors() throws Exception { // TODO move to better place HgRepository repository = Configuration.get().find("branches-1"); // any repo - repository.getChangelog().walk(0, TIP, new HgChangelog.RevisionInspector() { + repository.getChangelog().indexWalk(0, TIP, new HgChangelog.RevisionInspector() { public void next(int localRevision, Nodeid revision, int linkedRevision) { Assert.assertEquals(localRevision, linkedRevision); } }); final HgDataFile fileNode = repository.getFileNode("file1"); - fileNode.walk(0, TIP, new HgDataFile.RevisionInspector() { + fileNode.indexWalk(0, TIP, new HgDataFile.RevisionInspector() { int i = 0; public void next(int localRevision, Nodeid revision, int linkedRevision) { - try { - Assert.assertEquals(i++, localRevision); - Assert.assertEquals(fileNode.getChangesetRevisionIndex(localRevision), linkedRevision); - Assert.assertEquals(fileNode.getRevision(localRevision), revision); - } catch (HgException ex) { - Assert.fail(ex.toString()); - } + Assert.assertEquals(i++, localRevision); + Assert.assertEquals(fileNode.getChangesetRevisionIndex(localRevision), linkedRevision); + Assert.assertEquals(fileNode.getRevision(localRevision), revision); } }); class ParentInspectorCheck implements HgDataFile.ParentInspector { @@ -277,10 +274,10 @@ } } }; - fileNode.walk(0, TIP, new ParentInspectorCheck(0, fileNode.getRevisionCount())); + fileNode.indexWalk(0, TIP, new ParentInspectorCheck(0, fileNode.getRevisionCount())); assert fileNode.getRevisionCount() > 2 : "prereq"; // need at least few revisions // there used to be a defect in #walk impl, assumption all parents come prior to a revision - fileNode.walk(1, 3, new ParentInspectorCheck(1, 3)); + fileNode.indexWalk(1, 3, new ParentInspectorCheck(1, 3)); } @Test @@ -315,6 +312,84 @@ Assert.assertTrue(s.equals(r2)); } + @Test + public void testPathScope() { + // XXX whether PathScope shall accept paths that are leading towards configured elements + Path[] scope = new Path[] { + Path.create("a/"), + Path.create("b/c"), + Path.create("d/e/f/") + }; + // + // accept specified path, with files and folders below + PathScope ps1 = new PathScope(true, scope); + // folders + errorCollector.assertTrue(ps1.accept(Path.create("a/"))); // == scope[0] + errorCollector.assertTrue(ps1.accept(Path.create("a/d/"))); // scope[0] is parent and recursiveDir = true + errorCollector.assertTrue(ps1.accept(Path.create("a/d/e/"))); // scope[0] is parent and recursiveDir = true + errorCollector.assertTrue(!ps1.accept(Path.create("b/d/"))); // unrelated to any preconfigured + errorCollector.assertTrue(ps1.accept(Path.create("b/"))); // arg is parent to scope[1] + errorCollector.assertTrue(ps1.accept(Path.create("d/"))); // arg is parent to scope[2] + errorCollector.assertTrue(ps1.accept(Path.create("d/e/"))); // arg is parent to scope[2] + errorCollector.assertTrue(!ps1.accept(Path.create("d/g/"))); // unrelated to any preconfigured + // files + errorCollector.assertTrue(ps1.accept(Path.create("a/d"))); // "a/" is parent + errorCollector.assertTrue(ps1.accept(Path.create("a/d/f"))); // "a/" is still a parent + errorCollector.assertTrue(ps1.accept(Path.create("b/c"))); // == + errorCollector.assertTrue(!ps1.accept(Path.create("b/d"))); // file, != + // + // accept only specified files, folders and their direct children, allow navigate to them from above (FileIterator contract) + PathScope ps2 = new PathScope(true, false, true, scope); + // folders + errorCollector.assertTrue(!ps2.accept(Path.create("a/b/c/"))); // recursiveDirs = false + errorCollector.assertTrue(ps2.accept(Path.create("b/"))); // arg is parent to scope[1] (IOW, scope[1] is nested under arg) + errorCollector.assertTrue(ps2.accept(Path.create("d/"))); // scope[2] is nested under arg + errorCollector.assertTrue(ps2.accept(Path.create("d/e/"))); // scope[2] is nested under arg + errorCollector.assertTrue(!ps2.accept(Path.create("d/f/"))); + errorCollector.assertTrue(!ps2.accept(Path.create("b/f/"))); + // files + errorCollector.assertTrue(!ps2.accept(Path.create("a/b/c"))); // file, no exact match + errorCollector.assertTrue(ps2.accept(Path.create("d/e/f/g"))); // file under scope[2] + errorCollector.assertTrue(!ps2.accept(Path.create("b/e"))); // unrelated file + + // matchParentDirs == false + PathScope ps3 = new PathScope(false, true, true, Path.create("a/b/")); // match any dir/file under a/b/, but not above + errorCollector.assertTrue(!ps3.accept(Path.create("a/"))); + errorCollector.assertTrue(ps3.accept(Path.create("a/b/c/d"))); + errorCollector.assertTrue(ps3.accept(Path.create("a/b/c"))); + errorCollector.assertTrue(!ps3.accept(Path.create("b/"))); + errorCollector.assertTrue(!ps3.accept(Path.create("d/"))); + errorCollector.assertTrue(!ps3.accept(Path.create("d/e/"))); + + // match nested but not direct dir + PathScope ps4 = new PathScope(false, true, false, Path.create("a/b/")); // match any dir/file *deep* under a/b/, + errorCollector.assertTrue(!ps4.accept(Path.create("a/"))); + errorCollector.assertTrue(!ps4.accept(Path.create("a/b/c"))); + errorCollector.assertTrue(ps4.accept(Path.create("a/b/c/d"))); + } + + @Test + public void testPathCompareWith() { + Path p1 = Path.create("a/b/"); + Path p2 = Path.create("a/b/c"); + Path p3 = Path.create("a/b"); // file with the same name as dir + Path p4 = Path.create("a/b/c/d/"); + Path p5 = Path.create("d/"); + + errorCollector.assertEquals(Same, p1.compareWith(p1)); + errorCollector.assertEquals(Same, p1.compareWith(Path.create(p1.toString()))); + errorCollector.assertEquals(Unrelated, p1.compareWith(null)); + errorCollector.assertEquals(Unrelated, p1.compareWith(p5)); + // + errorCollector.assertEquals(Parent, p1.compareWith(p4)); + errorCollector.assertEquals(Nested, p4.compareWith(p1)); + errorCollector.assertEquals(ImmediateParent, p1.compareWith(p2)); + errorCollector.assertEquals(ImmediateChild, p2.compareWith(p1)); + // + errorCollector.assertEquals(Unrelated, p2.compareWith(p3)); + errorCollector.assertEquals(Unrelated, p3.compareWith(p2)); + } + public static void main(String[] args) throws Exception { new TestAuxUtilities().testRepositoryConfig(); diff -r 2078692eeb58 -r 7bcfbc255f48 test/org/tmatesoft/hg/test/TestByteChannel.java --- a/test/org/tmatesoft/hg/test/TestByteChannel.java Thu Jun 21 21:36:06 2012 +0200 +++ b/test/org/tmatesoft/hg/test/TestByteChannel.java Wed Jul 11 20:40:47 2012 +0200 @@ -98,7 +98,7 @@ 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); + repo = new HgLookup(new BasicSessionContext(props, null)).detect(repoDir); File f1 = new File(repoDir, "file1"); final String c1 = "First", c2 = "Second", c3 = "Third"; ByteArrayChannel ch; diff -r 2078692eeb58 -r 7bcfbc255f48 test/org/tmatesoft/hg/test/TestHistory.java --- a/test/org/tmatesoft/hg/test/TestHistory.java Thu Jun 21 21:36:06 2012 +0200 +++ b/test/org/tmatesoft/hg/test/TestHistory.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -30,10 +30,10 @@ import org.junit.Rule; import org.junit.Test; import org.tmatesoft.hg.core.HgChangeset; +import org.tmatesoft.hg.core.HgChangesetHandler; import org.tmatesoft.hg.core.HgFileRevision; import org.tmatesoft.hg.core.HgLogCommand; import org.tmatesoft.hg.core.HgLogCommand.CollectHandler; -import org.tmatesoft.hg.core.HgLogCommand.FileHistoryHandler; import org.tmatesoft.hg.repo.HgLookup; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.test.LogOutputParser.Record; @@ -93,7 +93,7 @@ changelogParser.reset(); eh.run("hg", "log", "--debug", "--follow", f.toString()); - class H extends CollectHandler implements FileHistoryHandler { + class H extends CollectHandler implements HgChangesetHandler.WithCopyHistory { boolean copyReported = false; boolean fromMatched = false; public void copy(HgFileRevision from, HgFileRevision to) { @@ -113,7 +113,7 @@ final LinkedList sorted = new LinkedList(h.getChanges()); Collections.sort(sorted, new Comparator() { public int compare(HgChangeset cs1, HgChangeset cs2) { - return cs1.getRevision() < cs2.getRevision() ? 1 : -1; + return cs1.getRevisionIndex() < cs2.getRevisionIndex() ? 1 : -1; } }); report(what, sorted, false); @@ -137,14 +137,14 @@ break; } Record cr = consoleResultItr.next(); - int x = cs.getRevision() == cr.changesetIndex ? 0x1 : 0; + int x = cs.getRevisionIndex() == cr.changesetIndex ? 0x1 : 0; x |= cs.getDate().toString().equals(cr.date) ? 0x2 : 0; x |= cs.getNodeid().toString().equals(cr.changesetNodeid) ? 0x4 : 0; x |= cs.getUser().equals(cr.user) ? 0x8 : 0; // need to do trim() on comment because command-line template does, and there are // repositories that have couple of newlines in the end of the comment (e.g. hello sample repo from the book) x |= cs.getComment().trim().equals(cr.description) ? 0x10 : 0; - errorCollector.checkThat(String.format(what + ". Mismatch (0x%x) in %d hg4j rev comparing to %d cmdline's.", x, cs.getRevision(), cr.changesetIndex), x, equalTo(0x1f)); + errorCollector.checkThat(String.format(what + ". Mismatch (0x%x) in %d hg4j rev comparing to %d cmdline's.", x, cs.getRevisionIndex(), cr.changesetIndex), x, equalTo(0x1f)); consoleResultItr.remove(); } errorCollector.checkThat(what + ". Unprocessed results in console left (insufficient from hg4j)", consoleResultItr.hasNext(), equalTo(false)); @@ -167,6 +167,7 @@ @Test public void testOriginalTestLogRepo() throws Exception { + // tests fro mercurial distribution, test-log.t repo = Configuration.get().find("log-1"); HgLogCommand cmd = new HgLogCommand(repo); // funny enough, but hg log -vf a -R c:\temp\hg\test-log\a doesn't work, while --cwd works fine @@ -176,9 +177,13 @@ report("log a", cmd.file("a", false).execute(), true); // changelogParser.reset(); - eh.run("hg", "log", "--debug", "-f", "a", "--cwd", repo.getLocation()); - List r = cmd.file("a", true).execute(); - report("log -f a", r, true); + // fails with Mercurial 2.2.1, @see http://selenic.com/pipermail/mercurial-devel/2012-February/038249.html + // and http://www.selenic.com/hg/rev/60101427d618?rev= + // fix for the test (replacement) is available below +// eh.run("hg", "log", "--debug", "-f", "a", "--cwd", repo.getLocation()); +// List r = cmd.file("a", true).execute(); +// report("log -f a", r, true); + // changelogParser.reset(); eh.run("hg", "log", "--debug", "-f", "e", "--cwd", repo.getLocation()); @@ -189,13 +194,29 @@ report("log dir/b", cmd.file("dir/b", false).execute(), true); // changelogParser.reset(); - eh.run("hg", "log", "--debug", "-f", "dir/b", "--cwd", repo.getLocation()); - report("log -f dir/b", cmd.file("dir/b", true).execute(), false /*#1, below*/); +// +// Commented out for the same reason as above hg log -f a - newly introduced error message in Mercurial 2.2 +// when files are not part of the parent revision +// eh.run("hg", "log", "--debug", "-f", "dir/b", "--cwd", repo.getLocation()); +// report("log -f dir/b", cmd.file("dir/b", true).execute(), false /*#1, below*/); /* * #1: false works because presently commands dispatches history of the queried file, and then history * of it's origin. With history comprising of renames only, this effectively gives reversed (newest to oldest) * order of revisions. */ + + // commented tests from above updated to work in 2.2 - update repo to revision where files are present + eh.run("hg", "update", "-q", "-r", "2", "--cwd", repo.getLocation()); + changelogParser.reset(); + eh.run("hg", "log", "--debug", "-f", "a", "--cwd", repo.getLocation()); + List r = cmd.file("a", true).execute(); + report("log -f a", r, true); + changelogParser.reset(); + eh.run("hg", "log", "--debug", "-f", "dir/b", "--cwd", repo.getLocation()); + report("log -f dir/b", cmd.file("dir/b", true).execute(), false /*#1, below*/); + // + // get repo back into clear state, up to the tip + eh.run("hg", "update", "-q", "--cwd", repo.getLocation()); } @Test diff -r 2078692eeb58 -r 7bcfbc255f48 test/org/tmatesoft/hg/test/TestIncoming.java --- a/test/org/tmatesoft/hg/test/TestIncoming.java Thu Jun 21 21:36:06 2012 +0200 +++ b/test/org/tmatesoft/hg/test/TestIncoming.java Wed Jul 11 20:40:47 2012 +0200 @@ -135,7 +135,7 @@ static File initEmptyTempRepo(String dirName) throws IOException { File dest = createEmptyDir(dirName); - Internals implHelper = new Internals(new BasicSessionContext(null, null, null)); + Internals implHelper = new Internals(new BasicSessionContext(null)); implHelper.setStorageConfig(1, STORE | FNCACHE | DOTENCODE); implHelper.initEmptyRepository(new File(dest, ".hg")); return dest; diff -r 2078692eeb58 -r 7bcfbc255f48 test/org/tmatesoft/hg/test/TestIntMap.java --- a/test/org/tmatesoft/hg/test/TestIntMap.java Thu Jun 21 21:36:06 2012 +0200 +++ b/test/org/tmatesoft/hg/test/TestIntMap.java Wed Jul 11 20:40:47 2012 +0200 @@ -18,6 +18,11 @@ import static org.junit.Assert.assertEquals; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; + import org.junit.Test; import org.tmatesoft.hg.internal.IntMap; @@ -59,4 +64,25 @@ } assertEquals(m.size(), actualCount); } + + @Test + public void testIterators() { + IntMap m = new IntMap(20); + for (int i = 0; i <= 30; i+= 5) { + m.put(i, Boolean.TRUE); + } + HashMap hm = new HashMap(); + for (Iterator> it = m.entryIterator(); it.hasNext(); ) { + Entry next = it.next(); + hm.put(next.getKey(), next.getValue()); + } + assertEquals(m.size(), hm.size()); + for (int i = 0; i <= 30; i++) { + assertEquals(m.get(i), hm.get(i)); + } + // + HashMap hm2 = new HashMap(); + m.fill(hm2); + assertEquals(hm, hm2); + } } diff -r 2078692eeb58 -r 7bcfbc255f48 test/org/tmatesoft/hg/test/TestManifest.java --- a/test/org/tmatesoft/hg/test/TestManifest.java Thu Jun 21 21:36:06 2012 +0200 +++ b/test/org/tmatesoft/hg/test/TestManifest.java Wed Jul 11 20:40:47 2012 +0200 @@ -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 @@ -28,6 +28,7 @@ import org.junit.Rule; import org.junit.Test; +import org.tmatesoft.hg.core.HgManifestHandler; import org.tmatesoft.hg.core.HgFileRevision; import org.tmatesoft.hg.core.HgManifestCommand; import org.tmatesoft.hg.core.Nodeid; @@ -50,7 +51,7 @@ private ManifestOutputParser manifestParser; private ExecHelper eh; final LinkedList revisions = new LinkedList(); - private HgManifestCommand.Handler handler = new HgManifestCommand.Handler() { + private HgManifestHandler handler = new HgManifestHandler() { public void file(HgFileRevision fileRevision) { revisions.add(fileRevision); @@ -102,7 +103,7 @@ manifestParser.reset(); eh.run("hg", "manifest", "--debug", "--rev", String.valueOf(rev == TIP ? -1 : rev)); revisions.clear(); - new HgManifestCommand(repo).revision(rev).execute(handler); + new HgManifestCommand(repo).changeset(rev).execute(handler); report("manifest " + (rev == TIP ? "TIP:" : "--rev " + rev)); } diff -r 2078692eeb58 -r 7bcfbc255f48 test/org/tmatesoft/hg/test/TestStatus.java --- a/test/org/tmatesoft/hg/test/TestStatus.java Thu Jun 21 21:36:06 2012 +0200 +++ b/test/org/tmatesoft/hg/test/TestStatus.java Wed Jul 11 20:40:47 2012 +0200 @@ -21,6 +21,7 @@ import static org.junit.Assert.assertTrue; import static org.tmatesoft.hg.core.HgStatus.Kind.*; import static org.tmatesoft.hg.repo.HgRepository.TIP; +import static org.tmatesoft.hg.repo.HgRepository.WORKING_COPY; import java.io.File; import java.util.ArrayList; @@ -47,8 +48,7 @@ import org.tmatesoft.hg.repo.HgStatusCollector; import org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector; import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.Status; - +import org.tmatesoft.hg.util.Outcome; /** * @@ -63,6 +63,7 @@ private HgRepository repo; private StatusOutputParser statusParser; private ExecHelper eh; + private StatusReporter sr; public static void main(String[] args) throws Throwable { TestStatus test = new TestStatus(); @@ -78,7 +79,7 @@ t3.testDirstateParentOtherThanTipNoUpdate(); t3.errorCollector.verify(); } - + public TestStatus() throws Exception { this(new HgLookup().detectFromWorkingDir()); } @@ -88,41 +89,42 @@ Assume.assumeTrue(!repo.isInvalid()); statusParser = new StatusOutputParser(); eh = new ExecHelper(statusParser, hgRepo.getWorkingDir()); + sr = new StatusReporter(errorCollector, statusParser); } - + @Test public void testLowLevel() throws Exception { final HgWorkingCopyStatusCollector wcc = new HgWorkingCopyStatusCollector(repo); statusParser.reset(); eh.run("hg", "status", "-A"); HgStatusCollector.Record r = wcc.status(HgRepository.TIP); - report("hg status -A", r, statusParser); + sr.report("hg status -A", r); // statusParser.reset(); int revision = 3; eh.run("hg", "status", "-A", "--rev", String.valueOf(revision)); r = wcc.status(revision); - report("status -A --rev " + revision, r, statusParser); + sr.report("status -A --rev " + revision, r); // statusParser.reset(); eh.run("hg", "status", "-A", "--change", String.valueOf(revision)); r = new HgStatusCollector.Record(); new HgStatusCollector(repo).change(revision, r); - report("status -A --change " + revision, r, statusParser); + sr.report("status -A --change " + revision, r); // statusParser.reset(); int rev2 = 80; final String range = String.valueOf(revision) + ":" + String.valueOf(rev2); eh.run("hg", "status", "-A", "--rev", range); r = new HgStatusCollector(repo).status(revision, rev2); - report("Status -A -rev " + range, r, statusParser); + sr.report("Status -A -rev " + range, r); } /** * hg up --rev ; hg status * * To check if HgWorkingCopyStatusCollector respects actual working copy parent (takes from dirstate) - * and if status is calculated correctly + * and if status is calculated correctly */ @Test @Ignore("modifies test repository, needs careful configuration") @@ -144,16 +146,15 @@ // eh.run("hg", "status", "-A"); HgStatusCollector.Record r = wcc.status(HgRepository.TIP); - report("hg status -A", r, statusParser); + sr.report("hg status -A", r); // statusParser.reset(); int revision = 3; eh.run("hg", "status", "-A", "--rev", String.valueOf(revision)); r = wcc.status(revision); - report("status -A --rev " + revision, r, statusParser); + sr.report("status -A --rev " + revision, r); } - @Test public void testStatusCommand() throws Exception { final HgStatusCommand sc = new HgStatusCommand(repo).all(); @@ -161,33 +162,37 @@ statusParser.reset(); eh.run("hg", "status", "-A"); sc.execute(r = new StatusCollector()); - report("hg status -A", r); + sr.report("hg status -A", r); // statusParser.reset(); int revision = 3; eh.run("hg", "status", "-A", "--rev", String.valueOf(revision)); sc.base(revision).execute(r = new StatusCollector()); - report("status -A --rev " + revision, r); + sr.report("status -A --rev " + revision, r); // statusParser.reset(); eh.run("hg", "status", "-A", "--change", String.valueOf(revision)); sc.base(TIP).revision(revision).execute(r = new StatusCollector()); - report("status -A --change " + revision, r); - + sr.report("status -A --change " + revision, r); + // TODO check not -A, but defaults()/custom set of modifications } - - private static class StatusCollector implements HgStatusHandler { + + static class StatusCollector implements HgStatusHandler { private final Map> kind2names = new TreeMap>(); private final Map> name2kinds = new TreeMap>(); - private final Map name2error = new LinkedHashMap(); + private final Map name2error = new LinkedHashMap(); + private final Map new2oldName = new LinkedHashMap(); - public void handleStatus(HgStatus s) { + public void status(HgStatus s) { List l = kind2names.get(s.getKind()); if (l == null) { kind2names.put(s.getKind(), l = new LinkedList()); } l.add(s.getPath()); + if (s.isCopy()) { + new2oldName.put(s.getPath(), s.getOriginalPath()); + } // List k = name2kinds.get(s.getPath()); if (k == null) { @@ -195,24 +200,56 @@ } k.add(s.getKind()); } - - public void handleError(Path file, Status s) { + + public void error(Path file, Outcome s) { name2error.put(file, s); } - + public List get(Kind k) { List rv = kind2names.get(k); - return rv == null ? Collections.emptyList() : rv; + return rv == null ? Collections. emptyList() : rv; } - + public List get(Path p) { List rv = name2kinds.get(p); - return rv == null ? Collections.emptyList() : rv; + return rv == null ? Collections. emptyList() : rv; } - - public Map getErrors() { + + public Map getErrors() { return name2error; } + + public HgStatusCollector.Record asStatusRecord() { + HgStatusCollector.Record rv = new HgStatusCollector.Record(); + for (Path p : get(Modified)) { + rv.modified(p); + } + for (Path p : get(Added)) { + if (!new2oldName.containsKey(p)) { + // new files that are result of a copy get reported separately, below + rv.added(p); + } + } + for (Path p : get(Removed)) { + rv.removed(p); + } + for (Path p : get(Clean)) { + rv.clean(p); + } + for (Path p : get(Ignored)) { + rv.ignored(p); + } + for (Path p : get(Missing)) { + rv.missing(p); + } + for (Path p : get(Unknown)) { + rv.unknown(p); + } + for (Map.Entry e : new2oldName.entrySet()) { + rv.copied(e.getValue(), e.getKey()); + } + return rv; + } } /* @@ -231,7 +268,7 @@ // shall not be listed at all assertTrue(sc.get(file5).isEmpty()); } - + /* * status-1/file2 is tracked, but later .hgignore got entry to ignore it, file2 got modified * HG doesn't respect .hgignore for tracked files. @@ -252,8 +289,8 @@ /* * status/dir/file4, added in rev 3, has been scheduled for removal (hg remove -Af file4), but still there in the WC. - * Shall be reported as Removed, when comparing against rev 3 - * (despite both rev 3 and WC's parent has file4, there are different paths in the code for wc against parent and wc against rev) + * Shall be reported as Removed, when comparing against rev 3 + * (despite both rev 3 and WC's parent has file4, there are different paths in the code for wc against parent and wc against rev) */ @Test public void testMarkedRemovedButStillInWC() throws Exception { @@ -279,10 +316,10 @@ } /* - * status-1/dir/file3 tracked, listed in .hgignore since rev 4, removed (hg remove file3) from repo and WC + * status-1/dir/file3 tracked, listed in .hgignore since rev 4, removed (hg remove file3) from repo and WC * (but entry in .hgignore left) in revision 5, and new file3 got created in WC. * Shall be reported as ignored when comparing against WC's parent, - * and both ignored and removed when comparing against revision 3 + * and both ignored and removed when comparing against revision 3 */ @Test public void testRemovedIgnoredInWC() throws Exception { @@ -316,7 +353,7 @@ /* * status/file1 was removed in cset 2. New file with the same name in the WC. - * Shall report 2 statuses (as cmdline hg does): unknown and removed when comparing against that revision. + * Shall report 2 statuses (as cmdline hg does): unknown and removed when comparing against that revision. */ @Test public void testNewFileWithSameNameAsDeletedOld() throws Exception { @@ -338,7 +375,7 @@ assertTrue(sc.get(file1).contains(Unknown)); assertTrue(sc.get(file1).size() == 1); } - + @Test public void testSubTreeStatus() throws Exception { repo = Configuration.get().find("status-1"); @@ -372,8 +409,7 @@ assertTrue(sc.get(Ignored).size() == 1); assertTrue(sc.get(Removed).size() == 2); } - - + @Test public void testSpecificFileStatus() throws Exception { repo = Configuration.get().find("status-1"); @@ -382,7 +418,7 @@ final Path file3 = Path.create("dir/file3"); HgWorkingCopyStatusCollector sc = HgWorkingCopyStatusCollector.create(repo, file2, file3); HgStatusCollector.Record r = new HgStatusCollector.Record(); - sc.walk(TIP, r); + sc.walk(WORKING_COPY, r); assertTrue(r.getAdded().isEmpty()); assertTrue(r.getRemoved().isEmpty()); assertTrue(r.getUnknown().isEmpty()); @@ -397,11 +433,11 @@ final Path readme = Path.create("readme"); final Path dir = Path.create("dir/"); sc = HgWorkingCopyStatusCollector.create(repo, readme, dir); - sc.walk(TIP, r = new HgStatusCollector.Record()); + sc.walk(WORKING_COPY, r = new HgStatusCollector.Record()); assertTrue(r.getAdded().isEmpty()); assertTrue(r.getRemoved().size() == 2); for (Path p : r.getRemoved()) { - assertEquals(p.compareWith(dir), Path.CompareResult.Nested); + assertEquals(Path.CompareResult.ImmediateChild, p.compareWith(dir)); } assertTrue(r.getUnknown().isEmpty()); assertTrue(r.getClean().size() == 1); @@ -412,26 +448,26 @@ assertTrue(r.getIgnored().size() == 1); assertTrue(r.getModified().isEmpty()); } - + @Test public void testSameResultDirectPathVsMatcher() throws Exception { repo = Configuration.get().find("status-1"); final Path file3 = Path.create("dir/file3"); final Path file5 = Path.create("dir/file5"); - + HgWorkingCopyStatusCollector sc = HgWorkingCopyStatusCollector.create(repo, file3, file5); HgStatusCollector.Record r; - sc.walk(TIP, r = new HgStatusCollector.Record()); + sc.walk(WORKING_COPY, r = new HgStatusCollector.Record()); assertTrue(r.getRemoved().contains(file5)); assertTrue(r.getIgnored().contains(file3)); // // query for the same file, but with sc = HgWorkingCopyStatusCollector.create(repo, new PathGlobMatcher(file3.toString(), file5.toString())); - sc.walk(TIP, r = new HgStatusCollector.Record()); + sc.walk(WORKING_COPY, r = new HgStatusCollector.Record()); assertTrue(r.getRemoved().contains(file5)); assertTrue(r.getIgnored().contains(file3)); } - + @Test public void testScopeInHistoricalStatus() throws Exception { repo = Configuration.get().find("status-1"); @@ -458,7 +494,7 @@ assertTrue(sc.get(Added).size() == 1); } - + /** * Issue 22 */ @@ -472,34 +508,35 @@ // shall pass without exception assertTrue(sc.getErrors().isEmpty()); for (HgStatus.Kind k : HgStatus.Kind.values()) { - assertTrue("Kind " + k.name() + " shall be empty",sc.get(k).isEmpty()); + assertTrue("Kind " + k.name() + " shall be empty", sc.get(k).isEmpty()); } } - + /** * Issue 22, two subsequent commits that remove all repository files, each in a different branch. * Here's excerpt from my RevlogWriter utility: + * *
 	 * 		final List filesList = Collections.singletonList("file1");
-	 *	//
-	 *	file1.writeUncompressed(-1, -1, 0, 0, "garbage".getBytes());
-	 *	//
-	 *	ManifestBuilder mb = new ManifestBuilder();
-	 *	mb.reset().add("file1", file1.getRevision(0));
-	 *	manifest.writeUncompressed(-1, -1, 0, 0, mb.build()); // manifest revision 0
-	 *	final byte[] cset1 = buildChangelogEntry(manifest.getRevision(0), Collections.emptyMap(), filesList, "Add a file");
-	 *	changelog.writeUncompressed(-1, -1, 0, 0, cset1);
-	 *	//
-	 *	// pretend we delete all files in a branch 1
-	 *	manifest.writeUncompressed(0, -1, 1, 1, new byte[0]); // manifest revision 1
-	 *	final byte[] cset2 = buildChangelogEntry(manifest.getRevision(1), Collections.singletonMap("branch", "delete-all-1"), filesList, "Delete all files in a first branch");
-	 *	 changelog.writeUncompressed(0, -1, 1, 1, cset2);
-	 *	//
-	 *	// pretend we delete all files in a branch 2 (which is based on revision 0, same as branch 1)
-	 *	manifest.writeUncompressed(1, -1, 1 /*!!! here comes baseRevision != index * /, 2, new byte[0]); // manifest revision 2
-	 *	final byte[] cset3 = buildChangelogEntry(manifest.getRevision(2), Collections.singletonMap("branch", "delete-all-2"), filesList, "Again delete all files but in another branch");
-	 *	changelog.writeUncompressed(0, -1, 2, 2, cset3);
-	 * 
+ * // + * file1.writeUncompressed(-1, -1, 0, 0, "garbage".getBytes()); + * // + * ManifestBuilder mb = new ManifestBuilder(); + * mb.reset().add("file1", file1.getRevision(0)); + * manifest.writeUncompressed(-1, -1, 0, 0, mb.build()); // manifest revision 0 + * final byte[] cset1 = buildChangelogEntry(manifest.getRevision(0), Collections.emptyMap(), filesList, "Add a file"); + * changelog.writeUncompressed(-1, -1, 0, 0, cset1); + * // + * // pretend we delete all files in a branch 1 + * manifest.writeUncompressed(0, -1, 1, 1, new byte[0]); // manifest revision 1 + * final byte[] cset2 = buildChangelogEntry(manifest.getRevision(1), Collections.singletonMap("branch", "delete-all-1"), filesList, "Delete all files in a first branch"); + * changelog.writeUncompressed(0, -1, 1, 1, cset2); + * // + * // pretend we delete all files in a branch 2 (which is based on revision 0, same as branch 1) + * manifest.writeUncompressed(1, -1, 1 /*!!! here comes baseRevision != index * /, 2, new byte[0]); // manifest revision 2 + * final byte[] cset3 = buildChangelogEntry(manifest.getRevision(2), Collections.singletonMap("branch", "delete-all-2"), filesList, "Again delete all files but in another branch"); + * changelog.writeUncompressed(0, -1, 2, 2, cset3); + * */ @Test public void testOnEmptyRepositoryWithAllFilesDeletedInBranch() throws Exception { @@ -511,12 +548,12 @@ // shall pass without exception assertTrue(sc.getErrors().isEmpty()); for (HgStatus.Kind k : HgStatus.Kind.values()) { - assertTrue("Kind " + k.name() + " shall be empty",sc.get(k).isEmpty()); + assertTrue("Kind " + k.name() + " shall be empty", sc.get(k).isEmpty()); } } - + /** - * Issue 23: HgInvalidRevisionException for svn imported repository (changeset 0 references nullid manifest) + * Issue 23: HgInvalidRevisionException for svn imported repository (changeset 0 references nullid manifest) */ @Test public void testImportedRepoWithOddManifestRevisions() throws Exception { @@ -528,32 +565,32 @@ // shall pass without exception assertTrue(sc.getErrors().isEmpty()); } - + /** * Issue 24: IllegalArgumentException in FilterDataAccess * There were two related defects in RevlogStream - * a) for compressedLen == 0, a byte was read and FilterDataAccess (of length 0, but it didn't help too much) was created - first byte happen to be 0. - * Patch was not applied (userDataAccess.isEmpty() check thanks to Issue 22) - * b) That FilterDataAccess (with 0 size represents patch more or less relevantly, but didn't represent actual revision) get successfully - * reassigned as lastUserData for the next iteration. And at the next step attempt to apply patch recorded in the next revision failed - * because baseRevisionData is 0 length FilterDataAccess + * a) for compressedLen == 0, a byte was read and FilterDataAccess (of length 0, but it didn't help too much) was created - first byte happen to be 0. + * Patch was not applied (userDataAccess.isEmpty() check thanks to Issue 22) + * b) That FilterDataAccess (with 0 size represents patch more or less relevantly, but didn't represent actual revision) get successfully + * reassigned as lastUserData for the next iteration. And at the next step attempt to apply patch recorded in the next revision failed + * because baseRevisionData is 0 length FilterDataAccess * - * Same applies for + * Same applies for * Issue 25: IOException: Underflow. Rewind past end of the slice in InflaterDataAccess * with the difference in separate .i and .d (thus not 0 but 'x' first byte was read) - * + * * Sample: - * status-5/file1 has 3 revisions, second is zero-length patch: - * Index Offset Packed Actual Base Rev - * 0: 0 8 7 0 - * DATA - * 1: 8 0 7 0 - * NO DATA - * 2: 8 14 6 0 - * PATCH + * status-5/file1 has 3 revisions, second is zero-length patch: + * Index Offset Packed Actual Base Rev + * 0: 0 8 7 0 + * DATA + * 1: 8 0 7 0 + * NO DATA + * 2: 8 14 6 0 + * PATCH */ @Test - public void testZeroLengthPatchAgainstNonEmptyBaseRev() throws Exception{ + public void testZeroLengthPatchAgainstNonEmptyBaseRev() throws Exception { repo = Configuration.get().find("status-5"); // pretend we modified files in the working copy // for HgWorkingCopyStatusCollector to go and retrieve its content from repository @@ -568,7 +605,7 @@ cmd.execute(sc); // shall pass without exception // - for (Map.Entry e : sc.getErrors().entrySet()) { + for (Map.Entry e : sc.getErrors().entrySet()) { System.out.printf("%s : (%s %s)\n", e.getKey(), e.getValue().getKind(), e.getValue().getMessage()); } assertTrue(sc.getErrors().isEmpty()); @@ -578,13 +615,13 @@ * Issue 26: UnsupportedOperationException when patching empty base revision * * Sample: - * status-5/file2 has 3 revisions, second is patch (complete revision content in a form of the patch) for empty base revision: - * Index Offset Packed Actual Base Rev - * 0: 0 0 0 0 - * NO DATA - * 1: 0 20 7 0 - * PATCH: 0..0, 7:garbage - * 2: 20 16 7 0 + * status-5/file2 has 3 revisions, second is patch (complete revision content in a form of the patch) for empty base revision: + * Index Offset Packed Actual Base Rev + * 0: 0 0 0 0 + * NO DATA + * 1: 0 20 7 0 + * PATCH: 0..0, 7:garbage + * 2: 20 16 7 0 */ @Test public void testPatchZeroLengthBaseRevision() throws Exception { @@ -599,13 +636,29 @@ cmd.execute(sc); // shall pass without exception // - for (Map.Entry e : sc.getErrors().entrySet()) { + for (Map.Entry e : sc.getErrors().entrySet()) { System.out.printf("%s : (%s %s)\n", e.getKey(), e.getValue().getKind(), e.getValue().getMessage()); } assertTrue(sc.getErrors().isEmpty()); } + + @Test + public void testNestedRepositoriesAreNotWalkedIn() throws Exception { + repo = Configuration.get().find("status-nested-repo"); + File s2 = new File(repo.getWorkingDir(), "skip/s2/.hg/"); + File s1 = new File(repo.getWorkingDir(), "s1/.hg/"); + File s1b = new File(repo.getWorkingDir(), "s1/b"); + assertTrue("[sanity]", s1.exists() && s1.isDirectory()); + assertTrue("[sanity]", s1b.exists() && s1b.isFile()); + assertTrue("[sanity]", s2.exists() && s2.isDirectory()); + StatusCollector sc = new StatusCollector(); + new HgStatusCommand(repo).all().execute(sc); + List ignored = sc.get(Ignored); + assertEquals(1, ignored.size()); + assertEquals(Path.create("skip/a"), ignored.get(0)); + assertTrue(sc.get(Path.create("s1/b")).isEmpty()); + } - /* * With warm-up of previous tests, 10 runs, time in milliseconds * 'hg status -A': Native client total 953 (95 per run), Java client 94 (9) @@ -613,13 +666,13 @@ * 'hg log --debug', 10 runs: Native client total 1766 (176 per run), Java client 78 (7) * * 18.02.2011 - * 'hg status -A --rev 3:80', 10 runs: Native client total 2000 (200 per run), Java client 250 (25) + * 'hg status -A --rev 3:80', 10 runs: Native client total 2000 (200 per run), Java client 250 (25) * 'hg log --debug', 10 runs: Native client total 2297 (229 per run), Java client 125 (12) * * 9.3.2011 (DataAccess instead of byte[] in ReflogStream.Inspector - * 'hg status -A', 10 runs: Native client total 1516 (151 per run), Java client 219 (21) - * 'hg status -A --rev 3:80', 10 runs: Native client total 1875 (187 per run), Java client 3187 (318) (!!! ???) - * 'hg log --debug', 10 runs: Native client total 2484 (248 per run), Java client 344 (34) + * 'hg status -A', 10 runs: Native client total 1516 (151 per run), Java client 219 (21) + * 'hg status -A --rev 3:80', 10 runs: Native client total 1875 (187 per run), Java client 3187 (318) (!!! ???) + * 'hg log --debug', 10 runs: Native client total 2484 (248 per run), Java client 344 (34) */ public void testPerformance() throws Exception { final int runs = 10; @@ -634,67 +687,71 @@ new HgStatusCommand(repo).all().base(3).revision(80).execute(r); } final long end = System.currentTimeMillis(); - System.out.printf("'hg status -A --rev 3:80', %d runs: Native client total %d (%d per run), Java client %d (%d)\n", runs, start2-start1, (start2-start1)/runs, end-start2, (end-start2)/runs); + System.out.printf("'hg status -A --rev 3:80', %d runs: Native client total %d (%d per run), Java client %d (%d)\n", runs, start2 - start1, (start2 - start1) / runs, end - start2, + (end - start2) / runs); } - private void report(String what, StatusCollector r) { - assertTrue(r.getErrors().isEmpty()); - reportNotEqual(what + "#MODIFIED", r.get(Modified), statusParser.getModified()); - reportNotEqual(what + "#ADDED", r.get(Added), statusParser.getAdded()); - reportNotEqual(what + "#REMOVED", r.get(Removed), statusParser.getRemoved()); - reportNotEqual(what + "#CLEAN", r.get(Clean), statusParser.getClean()); - reportNotEqual(what + "#IGNORED", r.get(Ignored), statusParser.getIgnored()); - reportNotEqual(what + "#MISSING", r.get(Missing), statusParser.getMissing()); - reportNotEqual(what + "#UNKNOWN", r.get(Unknown), statusParser.getUnknown()); - // FIXME test copies - } + static class StatusReporter { + private final StatusOutputParser statusParser; + private final ErrorCollectorExt errorCollector; - private void report(String what, HgStatusCollector.Record r, StatusOutputParser statusParser) { - reportNotEqual(what + "#MODIFIED", r.getModified(), statusParser.getModified()); - reportNotEqual(what + "#ADDED", r.getAdded(), statusParser.getAdded()); - reportNotEqual(what + "#REMOVED", r.getRemoved(), statusParser.getRemoved()); - reportNotEqual(what + "#CLEAN", r.getClean(), statusParser.getClean()); - reportNotEqual(what + "#IGNORED", r.getIgnored(), statusParser.getIgnored()); - reportNotEqual(what + "#MISSING", r.getMissing(), statusParser.getMissing()); - reportNotEqual(what + "#UNKNOWN", r.getUnknown(), statusParser.getUnknown()); - List copiedKeyDiff = difference(r.getCopied().keySet(), statusParser.getCopied().keySet()); - HashMap copyDiff = new HashMap(); - if (copiedKeyDiff.isEmpty()) { - for (Path jk : r.getCopied().keySet()) { - Path jv = r.getCopied().get(jk); - if (statusParser.getCopied().containsKey(jk)) { - Path cmdv = statusParser.getCopied().get(jk); - if (!jv.equals(cmdv)) { - copyDiff.put(jk, jv + " instead of " + cmdv); + public StatusReporter(ErrorCollectorExt ec, StatusOutputParser sp) { + errorCollector = ec; + statusParser = sp; + } + + public void report(String what, StatusCollector r) { + errorCollector.assertTrue(what, r.getErrors().isEmpty()); + report(what, r.asStatusRecord()); + } + + public void report(String what, HgStatusCollector.Record r) { + reportNotEqual(what + "#MODIFIED", r.getModified(), statusParser.getModified()); + reportNotEqual(what + "#ADDED", r.getAdded(), statusParser.getAdded()); + reportNotEqual(what + "#REMOVED", r.getRemoved(), statusParser.getRemoved()); + reportNotEqual(what + "#CLEAN", r.getClean(), statusParser.getClean()); + reportNotEqual(what + "#IGNORED", r.getIgnored(), statusParser.getIgnored()); + reportNotEqual(what + "#MISSING", r.getMissing(), statusParser.getMissing()); + reportNotEqual(what + "#UNKNOWN", r.getUnknown(), statusParser.getUnknown()); + List copiedKeyDiff = difference(r.getCopied().keySet(), statusParser.getCopied().keySet()); + HashMap copyDiff = new HashMap(); + if (copiedKeyDiff.isEmpty()) { + for (Path jk : r.getCopied().keySet()) { + Path jv = r.getCopied().get(jk); + if (statusParser.getCopied().containsKey(jk)) { + Path cmdv = statusParser.getCopied().get(jk); + if (!jv.equals(cmdv)) { + copyDiff.put(jk, jv + " instead of " + cmdv); + } + } else { + copyDiff.put(jk, "ERRONEOUSLY REPORTED IN JAVA"); } - } else { - copyDiff.put(jk, "ERRONEOUSLY REPORTED IN JAVA"); } } + errorCollector.checkThat(what + "#Non-matching 'copied' keys: ", copiedKeyDiff, equalTo(Collections. emptyList())); + errorCollector.checkThat(what + "#COPIED", copyDiff, equalTo(Collections. emptyMap())); } - errorCollector.checkThat(what + "#Non-matching 'copied' keys: ", copiedKeyDiff, equalTo(Collections.emptyList())); - errorCollector.checkThat(what + "#COPIED", copyDiff, equalTo(Collections.emptyMap())); - } - - private > void reportNotEqual(String what, Collection l1, Collection l2) { -// List diff = difference(l1, l2); -// errorCollector.checkThat(what, diff, equalTo(Collections.emptyList())); - ArrayList sl1 = new ArrayList(l1); - Collections.sort(sl1); - ArrayList sl2 = new ArrayList(l2); - Collections.sort(sl2); - errorCollector.checkThat(what, sl1, equalTo(sl2)); - } - private static List difference(Collection l1, Collection l2) { - LinkedList result = new LinkedList(l2); - for (T t : l1) { - if (l2.contains(t)) { - result.remove(t); - } else { - result.add(t); + private > void reportNotEqual(String what, Collection l1, Collection l2) { + // List diff = difference(l1, l2); + // errorCollector.checkThat(what, diff, equalTo(Collections.emptyList())); + ArrayList sl1 = new ArrayList(l1); + Collections.sort(sl1); + ArrayList sl2 = new ArrayList(l2); + Collections.sort(sl2); + errorCollector.checkThat(what, sl1, equalTo(sl2)); + } + + public static List difference(Collection l1, Collection l2) { + LinkedList result = new LinkedList(l2); + for (T t : l1) { + if (l2.contains(t)) { + result.remove(t); + } else { + result.add(t); + } } + return result; } - return result; } } diff -r 2078692eeb58 -r 7bcfbc255f48 test/org/tmatesoft/hg/test/TestStorePath.java --- a/test/org/tmatesoft/hg/test/TestStorePath.java Thu Jun 21 21:36:06 2012 +0200 +++ b/test/org/tmatesoft/hg/test/TestStorePath.java Wed Jul 11 20:40:47 2012 +0200 @@ -54,7 +54,7 @@ public TestStorePath() { propertyOverrides.put("hg.consolelog.debug", true); - internals = new Internals(new BasicSessionContext(propertyOverrides, null, null)); + internals = new Internals(new BasicSessionContext(propertyOverrides, null)); internals.setStorageConfig(1, 0x7); storePathHelper = internals.buildDataFilesHelper(); } diff -r 2078692eeb58 -r 7bcfbc255f48 test/org/tmatesoft/hg/test/TestSubrepo.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/org/tmatesoft/hg/test/TestSubrepo.java Wed Jul 11 20:40:47 2012 +0200 @@ -0,0 +1,117 @@ +/* + * Copyright (c) 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 + * 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.test; + +import static org.junit.Assert.assertEquals; +import static org.tmatesoft.hg.repo.HgRepository.TIP; + +import java.io.File; +import java.util.List; + +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.tmatesoft.hg.core.HgStatusCommand; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgSubrepoLocation; +import org.tmatesoft.hg.repo.HgSubrepoLocation.Kind; +import org.tmatesoft.hg.util.Path; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class TestSubrepo { + + @Rule + public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); + + private HgRepository repo; + private StatusOutputParser statusParser; + private ExecHelper eh; + + /* + * Layout of status-subrepo: + * first/ regular subrepo + * dir/second/ subrepo nested under a tracked folder + * third/ subrepo with another one + * third/fourth 2nd level of subrepo nesting (registered in third/.hgsub) + * third/fourth/file4_1 A, added file + * third/fourth/file4_2 ?, untracked file + * fifth/ nested repository not yet registered in .hgsub + * fifth/file5 untracked file + * + * Curiously, fifth/ shall not be reported (neither 'hg status -AS' nor '-A' don't report + * anything for it, no '?' for the file5 in particular. Once fifth/.hg/ is removed, + * file5 gets its ? as one would expect) + */ + + @Test + public void testAccessAPI() throws Exception { + repo = Configuration.get().find("status-subrepo"); + List subrepositories = repo.getSubrepositories(); + assertEquals(3, subrepositories.size()); + checkHgSubrepo(Path.create("first/"), true, repo, subrepositories.get(0)); + checkHgSubrepo(Path.create("dir/second/"), true, repo, subrepositories.get(1)); + checkHgSubrepo(Path.create("third/"), false, repo, subrepositories.get(2)); + } + + private void checkHgSubrepo(Path expectedLocation, boolean isCommitted, HgRepository topRepo, HgSubrepoLocation l) throws Exception { + errorCollector.assertEquals(expectedLocation, l.getLocation()); + errorCollector.assertEquals(Kind.Hg, l.getType()); + if (isCommitted) { + errorCollector.assertTrue(l.isCommitted()); + errorCollector.assertTrue(l.getRevision() != null); + errorCollector.assertTrue(!l.getRevision().isNull()); + } else { + errorCollector.assertTrue(!l.isCommitted()); + errorCollector.assertTrue(l.getRevision() == null); + } + errorCollector.assertEquals(topRepo, l.getOwner()); + HgRepository r = l.getRepo(); + String expectedSubRepoLoc = new File(topRepo.getLocation(), expectedLocation.toString()).toString(); + errorCollector.assertEquals(expectedSubRepoLoc, r.getLocation()); + errorCollector.assertTrue(r.getChangelog().getRevisionCount() > 0); + if (isCommitted) { + errorCollector.assertEquals(r.getChangelog().getRevision(TIP), l.getRevision()); + } + } + + @Test + @Ignore("StatusCommand doesn't suport subrepositories yet") + public void testStatusCommand() throws Exception { + repo = Configuration.get().find("status-subrepo"); + statusParser = new StatusOutputParser(); + eh = new ExecHelper(statusParser, repo.getWorkingDir()); + TestStatus.StatusReporter sr = new TestStatus.StatusReporter(errorCollector, statusParser); + HgStatusCommand cmd = new HgStatusCommand(repo).all(); + TestStatus.StatusCollector sc; + + eh.run("hg", "status", "-A", "-S"); + cmd.subrepo(true); + cmd.execute(sc = new TestStatus.StatusCollector()); + sr.report("status -A -S", sc); + + eh.run("hg", "status", "-A", "-S"); + cmd.subrepo(false); + cmd.execute(sc = new TestStatus.StatusCollector()); + sr.report("status -A", sc); + + } + +}