# HG changeset patch # User Alexander Kitaev # Date 1305017573 -7200 # Node ID 6ec4af642ba8024edd636af15e672c97cc3294e4 # Parent edb2e2829352ea0ed5419cc9ec1f861314ada88c Project uses Gradle for build - actual changes diff -r edb2e2829352 -r 6ec4af642ba8 .classpath --- a/.classpath Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,9 +0,0 @@ - - - - - - - - - diff -r edb2e2829352 -r 6ec4af642ba8 .hgignore --- a/.hgignore Mon May 09 11:49:23 2011 +0200 +++ b/.hgignore Tue May 10 10:52:53 2011 +0200 @@ -1,4 +1,3 @@ -.gradle build syntax:glob bin diff -r edb2e2829352 -r 6ec4af642ba8 .project --- a/.project Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,17 +0,0 @@ - - - hg4j - - - - - - org.eclipse.jdt.core.javabuilder - - - - - - org.eclipse.jdt.core.javanature - - diff -r edb2e2829352 -r 6ec4af642ba8 .settings/org.eclipse.core.resources.prefs --- a/.settings/org.eclipse.core.resources.prefs Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -#Thu Feb 24 18:49:36 CET 2011 -eclipse.preferences.version=1 -encoding/=UTF-8 diff -r edb2e2829352 -r 6ec4af642ba8 .settings/org.eclipse.core.runtime.prefs --- a/.settings/org.eclipse.core.runtime.prefs Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,3 +0,0 @@ -#Thu Feb 24 18:49:36 CET 2011 -eclipse.preferences.version=1 -line.separator=\n diff -r edb2e2829352 -r 6ec4af642ba8 .settings/org.eclipse.jdt.core.prefs --- a/.settings/org.eclipse.jdt.core.prefs Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,12 +0,0 @@ -#Wed Dec 15 01:43:42 CET 2010 -eclipse.preferences.version=1 -org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 -org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.5 -org.eclipse.jdt.core.compiler.debug.lineNumber=generate -org.eclipse.jdt.core.compiler.debug.localVariable=generate -org.eclipse.jdt.core.compiler.debug.sourceFile=generate -org.eclipse.jdt.core.compiler.problem.assertIdentifier=error -org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.5 diff -r edb2e2829352 -r 6ec4af642ba8 COPYING --- a/COPYING Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,14 +0,0 @@ -Copyright (C) 2010-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 \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 LICENSE.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/LICENSE.txt Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,14 @@ +Copyright (C) 2010-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 \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 build.gradle --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/build.gradle Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,67 @@ +group = 'org.tmatesoft.hg4j' +version = '0.5.0' +target = '1.5' +release = false + +buildscript { + repositories { + mavenRepo(urls: [buildPluginRepositoryURL]) { + snapshotTimeout = org.gradle.api.internal.artifacts.ivyservice.GradleIBiblioResolver.ALWAYS + } + } + dependencies { classpath 'org.tmatesoft.build:build:0.9.7-SNAPSHOT' } +} + +task wrapper(type: Wrapper) {} + +def javaProjects() { + return [ project(':hg4j'), project(':hg4j-cli') ] +} + +allprojects { + apply plugin : 'base' + apply plugin : 'build' +} + +configure(javaProjects()) { + apply plugin : 'java' + + sourceCompatibility = target + targetCompatibility = target + + configurations { + sources + javadocs + } + + task sourcesJar(type: Jar) { + description = 'Builds Java Sources Jar' + from sourceSets.main.java.srcDirs + classifier = 'sources' + } + + jar { + metaInf { + from rootProject.file('LICENSE.txt') + } + } + + artifacts { sources sourcesJar } +} + +configure(javaProjects() + rootProject) { + apply plugin : 'idea' + apply plugin : 'eclipse' +} + +project(':hg4j') { + dependencies { + testCompile 'junit:junit:4.8.2' + } +} + +project(':hg4j-cli') { + dependencies { + compile project(':hg4j') + } +} \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 build.xml --- a/build.xml Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,154 +0,0 @@ - - - - - Build, test and showcase hg4j - Targets: - * build - compile and jar binary and source bundles - * tests - run tests with JUnit - * samples - few command-line counterparts to demonstrate basic capabiliites - * rebuild - clean build - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff -r edb2e2829352 -r 6ec4af642ba8 cmdline/org/tmatesoft/hg/console/Bundle.java --- a/cmdline/org/tmatesoft/hg/console/Bundle.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,96 +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.console; - -import java.io.File; - -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.repo.HgBundle; -import org.tmatesoft.hg.repo.HgChangelog; -import org.tmatesoft.hg.repo.HgLookup; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; - - -/** - * WORK IN PROGRESS, DO NOT USE - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class Bundle { - public static void main(String[] args) throws Exception { - Options cmdLineOpts = Options.parse(args); - final HgRepository hgRepo = cmdLineOpts.findRepository(); - if (hgRepo.isInvalid()) { - System.err.printf("Can't find repository in: %s\n", hgRepo.getLocation()); - return; - } - File bundleFile = new File("/temp/hg/hg-bundle-cpython.tmp"); - HgBundle hgBundle = new HgLookup().loadBundle(bundleFile); - hgBundle.inspectFiles(new HgBundle.Dump()); - if (Boolean.parseBoolean("true")) { - return; - } - /* pass -R , e.g. for bundle with tip=168 and -R \temp\hg4j-50 with tip:159 - +Changeset {User: ..., Comment: Integer ....} - +Changeset {User: ..., Comment: Approach with ...} - -Changeset {User: ..., Comment: Correct project name...} - -Changeset {User: ..., Comment: Record possible...} - */ - hgBundle.changes(hgRepo, new HgChangelog.Inspector() { - private final HgChangelog changelog = hgRepo.getChangelog(); - - public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { - if (changelog.isKnown(nodeid)) { - System.out.print("+"); - } else { - System.out.print("-"); - } - System.out.printf("%d:%s\n%s\n", revisionNumber, nodeid.shortNotation(), cset.toString()); - } - }); - } - -/* - * TODO EXPLAIN why DataAccess.java on merge from branch has P2 set, and P1 is NULL - * - * excerpt from dump('hg-bundle-00') output (node, p1, p2, cs): - src/org/tmatesoft/hg/internal/DataAccess.java - 186af94a2a7ddb34190e63ce556d0fa4dd24add2 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 6f1b88693d48422e98c3eaaa8428ffd4d4d98ca7; patches:1 - be8d0fdc4ff268bf5eb0a9120282ce6e63de1606 186af94a2a7ddb34190e63ce556d0fa4dd24add2 0000000000000000000000000000000000000000 a3a2e5deb320d7412ccbb59bdc44668d445bc4c4; patches:2 - 333d7bbd4a80a5d6fb4b44e54e39e290f50dc7f8 be8d0fdc4ff268bf5eb0a9120282ce6e63de1606 0000000000000000000000000000000000000000 e93101b97e4ab0a3f3402ec0e80b6e559237c7c8; patches:1 - 56e4523cb8b42630daf70511d73d29e0b375dfa5 0000000000000000000000000000000000000000 333d7bbd4a80a5d6fb4b44e54e39e290f50dc7f8 d5268ca7715b8d96204fc62abc632e8f55761547; patches:6 - f85b6d7ed3cc4b7c6f99444eb0a41b58793cc900 56e4523cb8b42630daf70511d73d29e0b375dfa5 0000000000000000000000000000000000000000 b413b16d10a50cc027f4c38e4df5a9fedd618a79; patches:4 - - RevlogDump for the file says: - Index Offset Flags Packed Actual Base Rev Link Rev Parent1 Parent2 nodeid - 0: 4295032832 0 1109 2465 0 74 -1 -1 186af94a2a7ddb34190e63ce556d0fa4dd24add2 - 1: 1109 0 70 2364 0 102 0 -1 be8d0fdc4ff268bf5eb0a9120282ce6e63de1606 - 2: 1179 0 63 2365 0 122 1 -1 333d7bbd4a80a5d6fb4b44e54e39e290f50dc7f8 - 3: 1242 0 801 3765 0 157 -1 2 56e4523cb8b42630daf70511d73d29e0b375dfa5 - 4: 2043 0 130 3658 0 158 3 -1 f85b6d7ed3cc4b7c6f99444eb0a41b58793cc900 - - Excerpt from changelog dump: - 155: 30541 0 155 195 155 155 154 -1 a4ec5e08701771b96057522188b16ed289e9e8fe - 156: 30696 0 154 186 155 156 155 -1 643ddec3be36246fc052cf22ece503fa60cafe22 - 157: 30850 0 478 1422 155 157 156 53 d5268ca7715b8d96204fc62abc632e8f55761547 - 158: 31328 0 247 665 155 158 157 -1 b413b16d10a50cc027f4c38e4df5a9fedd618a79 - - - */ -} diff -r edb2e2829352 -r 6ec4af642ba8 cmdline/org/tmatesoft/hg/console/Cat.java --- a/cmdline/org/tmatesoft/hg/console/Cat.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,73 +0,0 @@ -/* - * Copyright (c) 2010-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.console; - -import static org.tmatesoft.hg.repo.HgRepository.TIP; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; - -import org.tmatesoft.hg.repo.HgDataFile; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.ByteChannel; - - -/** - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class Cat { - - public static void main(String[] args) throws Exception { - Options cmdLineOpts = Options.parse(args); - HgRepository hgRepo = cmdLineOpts.findRepository(); - if (hgRepo.isInvalid()) { - System.err.printf("Can't find repository in: %s\n", hgRepo.getLocation()); - return; - } - int rev = cmdLineOpts.getSingleInt(TIP, "-r", "--rev"); - OutputStreamChannel out = new OutputStreamChannel(System.out); - for (String fname : cmdLineOpts.getList("")) { - System.out.println(fname); - HgDataFile fn = hgRepo.getFileNode(fname); - if (fn.exists()) { - fn.contentWithFilters(rev, out); - System.out.println(); - } else { - System.out.printf("%s not found!\n", fname); - } - } - } - - private static class OutputStreamChannel implements ByteChannel { - - private final OutputStream stream; - - public OutputStreamChannel(OutputStream out) { - stream = out; - } - - public int write(ByteBuffer buffer) throws IOException { - int count = buffer.remaining(); - while(buffer.hasRemaining()) { - stream.write(buffer.get()); - } - return count; - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 cmdline/org/tmatesoft/hg/console/ChangesetDumpHandler.java --- a/cmdline/org/tmatesoft/hg/console/ChangesetDumpHandler.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,165 +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.console; - -import java.util.Formatter; -import java.util.LinkedList; -import java.util.List; - -import org.tmatesoft.hg.core.HgChangeset; -import org.tmatesoft.hg.core.HgChangesetHandler; -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.core.HgLogCommand.FileRevision; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.Path; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class ChangesetDumpHandler implements HgChangesetHandler { - // params - private boolean complete = false; // roughly --debug - private boolean reverseOrder = false; - private boolean verbose = false; // roughly -v - // own - private LinkedList l = new LinkedList(); - private final HgRepository repo; - private final int tip; - - public ChangesetDumpHandler(HgRepository hgRepo) { - repo = hgRepo; - tip = hgRepo.getChangelog().getLastRevision(); - } - - public ChangesetDumpHandler complete(boolean b) { - complete = b; - return this; - } - - public ChangesetDumpHandler reversed(boolean b) { - reverseOrder = b; - return this; - } - - public ChangesetDumpHandler verbose(boolean b) { - verbose = b; - return this; - } - - public void next(HgChangeset changeset) { - final String s = print(changeset); - if (reverseOrder) { - // XXX in fact, need to insert s into l according to changeset.getRevision() - // because when file history is being followed, revisions of the original file (with smaller revNumber) - // are reported *after* revisions of present file and with addFirst appear above them - l.addFirst(s); - } else { - System.out.print(s); - } - } - - public void done() { - if (!reverseOrder) { - return; - } - for (String s : l) { - System.out.print(s); - } - l.clear(); - } - - private String print(HgChangeset cset) { - 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)) { - - sb.append("tag: "); - for (String t : repo.getTags().tags(csetNodeid)) { - sb.append(t); - sb.append(' '); - } - if (cset.getRevision() == tip) { - sb.append("tip"); - } - sb.append('\n'); - } - if (complete) { - Nodeid p1 = cset.getFirstParentRevision(); - Nodeid p2 = cset.getSecondParentRevision(); - int p1x = p1 == Nodeid.NULL ? -1 : repo.getChangelog().getLocalRevision(p1); - int p2x = p2 == Nodeid.NULL ? -1 : repo.getChangelog().getLocalRevision(p2); - int mx = repo.getManifest().getLocalRevision(cset.getManifestRevision()); - f.format("parent: %d:%s\nparent: %d:%s\nmanifest: %d:%s\n", p1x, p1, p2x, p2, mx, cset.getManifestRevision()); - } - f.format("user: %s\ndate: %s\n", cset.getUser(), cset.getDate().toString()); - if (!complete && verbose) { - final List files = cset.getAffectedFiles(); - sb.append("files: "); - for (Path s : files) { - sb.append(' '); - sb.append(s); - } - sb.append('\n'); - } - if (complete) { - if (!cset.getModifiedFiles().isEmpty()) { - sb.append("files: "); - for (FileRevision s : cset.getModifiedFiles()) { - sb.append(' '); - sb.append(s.getPath()); - } - sb.append('\n'); - } - if (!cset.getAddedFiles().isEmpty()) { - sb.append("files+: "); - for (FileRevision s : cset.getAddedFiles()) { - sb.append(' '); - sb.append(s.getPath()); - } - sb.append('\n'); - } - if (!cset.getRemovedFiles().isEmpty()) { - sb.append("files-: "); - for (Path s : cset.getRemovedFiles()) { - sb.append(' '); - sb.append(s); - } - sb.append('\n'); - } - // if (cset.extras() != null) { - // sb.append("extra: "); - // for (Map.Entry e : cset.extras().entrySet()) { - // sb.append(' '); - // sb.append(e.getKey()); - // sb.append('='); - // sb.append(e.getValue()); - // } - // sb.append('\n'); - // } - } - if (complete || verbose) { - f.format("description:\n%s\n\n", cset.getComment()); - } else { - f.format("summary: %s\n\n", cset.getComment()); - } - return sb.toString(); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 cmdline/org/tmatesoft/hg/console/Clone.java --- a/cmdline/org/tmatesoft/hg/console/Clone.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +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.console; - -import java.io.File; -import java.util.List; - -import org.tmatesoft.hg.core.HgCloneCommand; -import org.tmatesoft.hg.repo.HgLookup; -import org.tmatesoft.hg.repo.HgRemoteRepository; - -/** - * Initial clone of a repository. Creates a brand new repository and populates it from specified source. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class Clone { - - // ran with args: svnkit c:\temp\hg\test-clone - public static void main(String[] args) throws Exception { - Options cmdLineOpts = Options.parse(args); - List noOptsArgs = cmdLineOpts.getList(""); - if (noOptsArgs.isEmpty()) { - System.err.println("Need at least one argument pointing to remote server to pull changes from"); - return; - } - HgCloneCommand cmd = new HgCloneCommand(); - String remoteRepo = noOptsArgs.get(0); - HgRemoteRepository hgRemote = new HgLookup().detectRemote(remoteRepo, null); - if (hgRemote.isInvalid()) { - System.err.printf("Remote repository %s is not valid", hgRemote.getLocation()); - return; - } - cmd.source(hgRemote); - if (noOptsArgs.size() > 1) { - cmd.destination(new File(noOptsArgs.get(1))); - } else { - cmd.destination(new File(System.getProperty("user.dir"))); - } - cmd.execute(); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 cmdline/org/tmatesoft/hg/console/Incoming.java --- a/cmdline/org/tmatesoft/hg/console/Incoming.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,228 +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.console; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map.Entry; - -import org.tmatesoft.hg.core.HgIncomingCommand; -import org.tmatesoft.hg.core.HgRepoFacade; -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.repo.HgLookup; -import org.tmatesoft.hg.repo.HgRemoteRepository; - - -/** - * hg incoming counterpart - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class Incoming { - - public static void main(String[] args) throws Exception { - if (Boolean.FALSE.booleanValue()) { - new SequenceConstructor().test(); - return; - } - Options cmdLineOpts = Options.parse(args); - HgRepoFacade hgRepo = new HgRepoFacade(); - if (!hgRepo.init(cmdLineOpts.findRepository())) { - System.err.printf("Can't find repository in: %s\n", hgRepo.getRepository().getLocation()); - return; - } - HgRemoteRepository hgRemote = new HgLookup().detectRemote(cmdLineOpts.getSingle(""), hgRepo.getRepository()); - if (hgRemote.isInvalid()) { - System.err.printf("Remote repository %s is not valid", hgRemote.getLocation()); - return; - } - HgIncomingCommand cmd = hgRepo.createIncomingCommand(); - cmd.against(hgRemote); - // - List missing = cmd.executeLite(null); - Collections.reverse(missing); // useful to test output, from newer to older - Outgoing.dump("Nodes to fetch:", missing); - System.out.printf("Total: %d\n\n", missing.size()); - // - // Complete - final ChangesetDumpHandler h = new ChangesetDumpHandler(hgRepo.getRepository()); - h.complete(false); // this option looks up index of parent revision, done via repo.changelog (which doesn't have any of these new revisions) - // this can be fixed by tracking all nodeid->revision idx inside ChangesetDumpHandler, and refer to repo.changelog only when that mapping didn't work - h.verbose(cmdLineOpts.getBoolean("-v", "--verbose")); - cmd.executeFull(h); - } - - - /* - * This is for investigation purposes only - */ - private static class SequenceConstructor { - - private int[] between(int root, int head) { - if (head <= (root+1)) { - return new int[0]; - } - System.out.printf("[%d, %d]\t\t", root, head); - int size = 1 + (int) Math.floor(Math.log(head-root - 1) / Math.log(2)); - int[] rv = new int[size]; - for (int v = 1, i = 0; i < rv.length; i++) { - rv[i] = root + v; - v = v << 1; - } - System.out.println(Arrays.toString(rv)); - return rv; - } - - public void test() { - int root = 0, head = 126; - int[] data = between(root, head); // max number of elements to recover is 2**data.length-1, when head is exactly - // 2**data.length element of the branch. - // In such case, total number of elements in the branch (including head and root, would be 2**data.length+1 - int[] finalSequence = new int[1 + (1 << data.length >>> 5)]; // div 32 - total bits to integers, +1 for possible modulus - int exactNumberOfElements = -1; // exact number of meaningful bits in finalSequence - LinkedHashMap datas = new LinkedHashMap(); - datas.put(root, data); - int totalQueries = 1; - HashSet queried = new HashSet(); - int[] checkSequence = null; - while(!datas.isEmpty()) { - LinkedList toQuery = new LinkedList(); - do { - Iterator> it = datas.entrySet().iterator(); - Entry next = it.next(); - int r = next.getKey(); - data = next.getValue(); - it.remove(); - populate(r, head, data, finalSequence); - if (checkSequence != null) { - boolean match = true; -// System.out.println("Try to match:"); - for (int i = 0; i < checkSequence.length; i++) { -// System.out.println(i); -// System.out.println("control:" + toBinaryString(checkSequence[i], ' ')); -// System.out.println("present:" + toBinaryString(finalSequence[i], ' ')); - if (checkSequence[i] != finalSequence[i]) { - match = false; - } else { - match &= true; - } - } - System.out.println(match ? "Match, on query:" + totalQueries : "Didn't match"); - } - if (data.length > 1) { - /*queries for elements next to head is senseless, hence data.length check above and head-x below*/ - for (int x : data) { - if (!queried.contains(x) && head - x > 1) { - toQuery.add(new int[] {x, head}); - } - } - } - } while (!datas.isEmpty()) ; - if (!toQuery.isEmpty()) { - System.out.println(); - totalQueries++; - } - Collections.sort(toQuery, new Comparator() { - - public int compare(int[] o1, int[] o2) { - return o1[0] < o2[0] ? -1 : (o1[0] == o2[0] ? 0 : 1); - } - }); - for (int[] x : toQuery) { - if (!queried.contains(x[0])) { - queried.add(x[0]); - data = between(x[0], x[1]); - if (exactNumberOfElements == -1 && data.length == 1) { - exactNumberOfElements = x[0] + 1; - System.out.printf("On query %d found out exact number of missing elements: %d\n", totalQueries, exactNumberOfElements); - // get a bit sequence of exactNumberOfElements, 0111..110 - // to 'and' it with finalSequence later - int totalInts = (exactNumberOfElements + 2 /*heading and tailing zero bits*/) >>> 5; - int trailingBits = (exactNumberOfElements + 2) & 0x1f; - if (trailingBits != 0) { - totalInts++; - } - checkSequence = new int[totalInts]; - Arrays.fill(checkSequence, 0xffffffff); - checkSequence[0] &= 0x7FFFFFFF; - if (trailingBits == 0) { - checkSequence[totalInts-1] &= 0xFFFFFFFE; - } else if (trailingBits == 1) { - checkSequence[totalInts-1] = 0; - } else { - // trailingBits include heading and trailing zero bits - int mask = 0x80000000 >> trailingBits-2; // with sign! - checkSequence[totalInts - 1] &= mask; - } - for (int e : checkSequence) { - System.out.print(toBinaryString(e, ' ')); - } - System.out.println(); - } - datas.put(x[0], data); - } - } - } - - System.out.println("Total queries:" + totalQueries); - for (int x : finalSequence) { - System.out.print(toBinaryString(x, ' ')); - } - } - - private void populate(int root, int head, int[] data, int[] finalSequence) { - for (int i = 1, x = 0; root+i < head; i = i << 1, x++) { - int value = data[x]; - int value_check = root+i; - if (value != value_check) { - throw new IllegalStateException(); - } - int wordIx = (root + i) >>> 5; - int bitIx = (root + i) & 0x1f; - finalSequence[wordIx] |= 1 << (31-bitIx); - } - } - - private static String toBinaryString(int x, char byteSeparator) { - StringBuilder sb = new StringBuilder(4*8+4); - sb.append(toBinaryString((byte) (x >>> 24))); - sb.append(byteSeparator); - sb.append(toBinaryString((byte) ((x & 0x00ff0000) >>> 16))); - sb.append(byteSeparator); - sb.append(toBinaryString((byte) ((x & 0x00ff00) >>> 8))); - sb.append(byteSeparator); - sb.append(toBinaryString((byte) (x & 0x00ff))); - sb.append(byteSeparator); - return sb.toString(); - } - - private static String toBinaryString(byte b) { - final String nibbles = "0000000100100011010001010110011110001001101010111100110111101111"; - assert nibbles.length() == 16*4; - int x1 = (b >>> 4) & 0x0f, x2 = b & 0x0f; - x1 *= 4; x2 *= 4; // 4 characters per nibble - return nibbles.substring(x1, x1+4).concat(nibbles.substring(x2, x2+4)); - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 cmdline/org/tmatesoft/hg/console/Log.java --- a/cmdline/org/tmatesoft/hg/console/Log.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,122 +0,0 @@ -/* - * Copyright (c) 2010-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.console; - -import java.util.List; - -import org.tmatesoft.hg.core.HgLogCommand; -import org.tmatesoft.hg.core.HgLogCommand.FileRevision; -import org.tmatesoft.hg.repo.HgDataFile; -import org.tmatesoft.hg.repo.HgRepository; - - -/** - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class Log { - - // -agentlib:hprof=heap=sites,depth=10,etc might be handy to debug speed/memory issues - - public static void main(String[] args) throws Exception { - Options cmdLineOpts = Options.parse(args); - HgRepository hgRepo = cmdLineOpts.findRepository(); - if (hgRepo.isInvalid()) { - System.err.printf("Can't find repository in: %s\n", hgRepo.getLocation()); - return; - } - final Dump dump = new Dump(hgRepo); - dump.complete(cmdLineOpts.getBoolean("--debug")); - dump.verbose(cmdLineOpts.getBoolean("-v", "--verbose")); - final boolean reverseOrder = true; - dump.reversed(reverseOrder); - HgLogCommand cmd = new HgLogCommand(hgRepo); - for (String u : cmdLineOpts.getList("-u", "--user")) { - cmd.user(u); - } - for (String b : cmdLineOpts.getList("-b", "--branches")) { - cmd.branch(b); - } - int limit = cmdLineOpts.getSingleInt(-1, "-l", "--limit"); - if (limit != -1) { - cmd.limit(limit); - } - List files = cmdLineOpts.getList(""); - final long start = System.currentTimeMillis(); - if (files.isEmpty()) { - if (limit == -1) { - // no revisions and no limit - cmd.execute(dump); - } else { - // in fact, external (to dump inspector) --limit processing yelds incorrect results when other args - // e.g. -u or -b are used (i.e. with -u shall give csets with user, not check last csets for user - int[] r = new int[] { 0, hgRepo.getChangelog().getRevisionCount() }; - if (fixRange(r, reverseOrder, limit) == 0) { - System.out.println("No changes"); - return; - } - cmd.range(r[0], r[1]).execute(dump); - } - dump.done(); - } else { - for (String fname : files) { - HgDataFile f1 = hgRepo.getFileNode(fname); - System.out.println("History of the file: " + f1.getPath()); - if (limit == -1) { - cmd.file(f1.getPath(), true).execute(dump); - } else { - int[] r = new int[] { 0, f1.getRevisionCount() }; - if (fixRange(r, reverseOrder, limit) == 0) { - System.out.println("No changes"); - continue; - } - cmd.range(r[0], r[1]).file(f1.getPath(), true).execute(dump); - } - dump.done(); - } - } -// cmd = null; - System.out.println("Total time:" + (System.currentTimeMillis() - start)); -// Main.force_gc(); - } - - private static int fixRange(int[] start_end, boolean reverse, int limit) { - assert start_end.length == 2; - if (limit < start_end[1]) { - if (reverse) { - // adjust left boundary of the range - start_end[0] = start_end[1] - limit; - } else { - start_end[1] = limit; // adjust right boundary - } - } - int rv = start_end[1] - start_end[0]; - start_end[1]--; // range needs index, not length - return rv; - } - - private static final class Dump extends ChangesetDumpHandler implements HgLogCommand.FileHistoryHandler { - - public Dump(HgRepository hgRepo) { - super(hgRepo); - } - - public void copy(FileRevision from, FileRevision to) { - System.out.printf("Got notified that %s(%s) was originally known as %s(%s)\n", to.getPath(), to.getRevision(), from.getPath(), from.getRevision()); - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 cmdline/org/tmatesoft/hg/console/Main.java --- a/cmdline/org/tmatesoft/hg/console/Main.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,290 +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.console; - -import static org.tmatesoft.hg.repo.HgRepository.TIP; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import org.tmatesoft.hg.core.HgLogCommand.FileRevision; -import org.tmatesoft.hg.core.HgManifestCommand; -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.internal.ByteArrayChannel; -import org.tmatesoft.hg.internal.DigestHelper; -import org.tmatesoft.hg.repo.HgDataFile; -import org.tmatesoft.hg.repo.HgInternals; -import org.tmatesoft.hg.repo.HgManifest; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.repo.HgStatusCollector; -import org.tmatesoft.hg.repo.HgStatusInspector; -import org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector; -import org.tmatesoft.hg.util.Path; - -/** - * Various debug dumps. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class Main { - - private Options cmdLineOpts; - private HgRepository hgRepo; - - public Main(String[] args) throws Exception { - cmdLineOpts = Options.parse(args); - hgRepo = cmdLineOpts.findRepository(); - if (hgRepo.isInvalid()) { - System.err.printf("Can't find repository in: %s\n", hgRepo.getLocation()); - return; - } - System.out.println("REPO:" + hgRepo.getLocation()); - } - - public static void main(String[] args) throws Exception { - Main m = new Main(args); - m.inflaterLengthException(); -// m.dumpIgnored(); -// m.dumpDirstate(); -// m.testStatusInternals(); -// m.catCompleteHistory(); -// m.dumpCompleteManifestLow(); -// m.dumpCompleteManifestHigh(); -// m.bunchOfTests(); - } - - private void inflaterLengthException() throws Exception { - HgDataFile f1 = hgRepo.getFileNode("src/com/tmate/hgkit/console/Bundle.java"); - HgDataFile f2 = hgRepo.getFileNode("test-repos.jar"); - System.out.println(f1.isCopy()); - System.out.println(f2.isCopy()); - ByteArrayChannel bac = new ByteArrayChannel(); - f1.content(1, bac); // 0: 1151, 1: 1139 - System.out.println(bac.toArray().length); - f2.content(0, bac = new ByteArrayChannel()); // 0: 14269 - System.out.println(bac.toArray().length); - } - - private void dumpIgnored() { - HgInternals debug = new HgInternals(hgRepo); - String[] toCheck = new String[] {"design.txt", "src/com/tmate/hgkit/ll/Changelog.java", "src/Extras.java", "bin/com/tmate/hgkit/ll/Changelog.class"}; - boolean[] checkResult = debug.checkIgnored(toCheck); - for (int i = 0; i < toCheck.length; i++) { - System.out.println("Ignored " + toCheck[i] + ": " + checkResult[i]); - } - } - - private void dumpDirstate() { - new HgInternals(hgRepo).dumpDirstate(); - } - - - private void catCompleteHistory() throws Exception { - DigestHelper dh = new DigestHelper(); - for (String fname : cmdLineOpts.getList("")) { - System.out.println(fname); - HgDataFile fn = hgRepo.getFileNode(fname); - if (fn.exists()) { - int total = fn.getRevisionCount(); - System.out.printf("Total revisions: %d\n", total); - for (int i = 0; i < total; i++) { - ByteArrayChannel sink = new ByteArrayChannel(); - fn.content(i, sink); - System.out.println("==========>"); - byte[] content = sink.toArray(); - System.out.println(new String(content)); - int[] parentRevisions = new int[2]; - byte[] parent1 = new byte[20]; - byte[] parent2 = new byte[20]; - fn.parents(i, parentRevisions, parent1, parent2); - System.out.println(dh.sha1(parent1, parent2, content).asHexString()); - } - } else { - System.out.println(">>>Not found!"); - } - } - } - - private void dumpCompleteManifestLow() { - hgRepo.getManifest().walk(0, TIP, new ManifestDump()); - } - - public static final class ManifestDump implements HgManifest.Inspector { - public boolean begin(int revision, Nodeid nid) { - System.out.printf("%d : %s\n", revision, nid); - return true; - } - - public boolean next(Nodeid nid, String fname, String flags) { - System.out.println(nid + "\t" + fname + "\t\t" + flags); - return true; - } - - public boolean end(int revision) { - System.out.println(); - return true; - } - } - - private void dumpCompleteManifestHigh() { - new HgManifestCommand(hgRepo).dirs(true).execute(new HgManifestCommand.Handler() { - - public void begin(Nodeid manifestRevision) { - System.out.println(">> " + manifestRevision); - } - public void dir(Path p) { - System.out.println(p); - } - public void file(FileRevision fileRevision) { - System.out.print(fileRevision.getRevision());; - System.out.print(" "); - System.out.println(fileRevision.getPath()); - } - - public void end(Nodeid manifestRevision) { - System.out.println(); - } - }); - } - - private void bunchOfTests() throws Exception { - HgInternals debug = new HgInternals(hgRepo); - debug.dumpDirstate(); - final StatusDump dump = new StatusDump(); - dump.showIgnored = false; - dump.showClean = false; - HgStatusCollector sc = new HgStatusCollector(hgRepo); - final int r1 = 0, r2 = 3; - System.out.printf("Status for changes between revision %d and %d:\n", r1, r2); - sc.walk(r1, r2, dump); - // - System.out.println("\n\nSame, but sorted in the way hg status does:"); - HgStatusCollector.Record r = sc.status(r1, r2); - sortAndPrint('M', r.getModified(), null); - sortAndPrint('A', r.getAdded(), null); - sortAndPrint('R', r.getRemoved(), null); - // - System.out.println("\n\nTry hg status --change :"); - sc.change(0, dump); - System.out.println("\nStatus against working dir:"); - HgWorkingCopyStatusCollector wcc = new HgWorkingCopyStatusCollector(hgRepo); - wcc.walk(TIP, dump); - System.out.println(); - System.out.printf("Manifest of the revision %d:\n", r2); - hgRepo.getManifest().walk(r2, r2, new ManifestDump()); - System.out.println(); - System.out.printf("\nStatus of working dir against %d:\n", r2); - r = wcc.status(r2); - sortAndPrint('M', r.getModified(), null); - sortAndPrint('A', r.getAdded(), r.getCopied()); - sortAndPrint('R', r.getRemoved(), null); - sortAndPrint('?', r.getUnknown(), null); - sortAndPrint('I', r.getIgnored(), null); - sortAndPrint('C', r.getClean(), null); - sortAndPrint('!', r.getMissing(), null); - } - - private void sortAndPrint(char prefix, List ul, Map copies) { - ArrayList sortList = new ArrayList(ul); - Collections.sort(sortList); - for (Path s : sortList) { - System.out.print(prefix); - System.out.print(' '); - System.out.println(s); - if (copies != null && copies.containsKey(s)) { - System.out.println(" " + copies.get(s)); - } - } - } - - - private void testStatusInternals() { - HgDataFile n = hgRepo.getFileNode(Path.create("design.txt")); - for (String s : new String[] {"011dfd44417c72bd9e54cf89b82828f661b700ed", "e5529faa06d53e06a816e56d218115b42782f1ba", "c18e7111f1fc89a80a00f6a39d51288289a382fc"}) { - // expected: 359, 2123, 3079 - byte[] b = s.getBytes(); - final Nodeid nid = Nodeid.fromAscii(b, 0, b.length); - System.out.println(s + " : " + n.length(nid)); - } - } - - static void force_gc() { - Runtime.getRuntime().runFinalization(); - Runtime.getRuntime().gc(); - Thread.yield(); - Runtime.getRuntime().runFinalization(); - Runtime.getRuntime().gc(); - Thread.yield(); - } - - private static class StatusDump implements HgStatusInspector { - public boolean hideStatusPrefix = false; // hg status -n option - public boolean showCopied = true; // -C - public boolean showIgnored = true; // -i - public boolean showClean = true; // -c - - public void modified(Path fname) { - print('M', fname); - } - - public void added(Path fname) { - print('A', fname); - } - - public void copied(Path fnameOrigin, Path fnameAdded) { - added(fnameAdded); - if (showCopied) { - print(' ', fnameOrigin); - } - } - - public void removed(Path fname) { - print('R', fname); - } - - public void clean(Path fname) { - if (showClean) { - print('C', fname); - } - } - - public void missing(Path fname) { - print('!', fname); - } - - public void unknown(Path fname) { - print('?', fname); - } - - public void ignored(Path fname) { - if (showIgnored) { - print('I', fname); - } - } - - private void print(char status, Path fname) { - if (!hideStatusPrefix) { - System.out.print(status); - System.out.print(' '); - } - System.out.println(fname); - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 cmdline/org/tmatesoft/hg/console/Manifest.java --- a/cmdline/org/tmatesoft/hg/console/Manifest.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2010-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.console; - -import static org.tmatesoft.hg.repo.HgRepository.TIP; - -import org.tmatesoft.hg.core.HgLogCommand.FileRevision; -import org.tmatesoft.hg.core.HgManifestCommand; -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.Path; - - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class Manifest { - - public static void main(String[] args) throws Exception { - Options cmdLineOpts = Options.parse(args); - HgRepository hgRepo = cmdLineOpts.findRepository(); - if (hgRepo.isInvalid()) { - System.err.printf("Can't find repository in: %s\n", hgRepo.getLocation()); - return; - } - final boolean debug = cmdLineOpts.getBoolean("--debug"); - final boolean verbose = cmdLineOpts.getBoolean("-v", "--verbose"); - HgManifestCommand.Handler h = new HgManifestCommand.Handler() { - - public void begin(Nodeid manifestRevision) { - } - public void dir(Path p) { - } - public void file(FileRevision fileRevision) { - if (debug) { - System.out.print(fileRevision.getRevision());; - } - 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); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 cmdline/org/tmatesoft/hg/console/Options.java --- a/cmdline/org/tmatesoft/hg/console/Options.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,111 +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.console; - -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -import org.tmatesoft.hg.repo.HgLookup; -import org.tmatesoft.hg.repo.HgRepository; - -/** - * Parse command-line options. Primitive implementation that recognizes options with 0 or 1 argument. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -class Options { - - public final Map> opt2values = new HashMap>(); - - public boolean getBoolean(String... aliases) { - return getBoolean(false, aliases); - } - - public boolean getBoolean(boolean def, String... aliases) { - for (String s : aliases) { - if (opt2values.containsKey(s)) { - return true; - } - } - return def; - } - - public String getSingle(String... aliases) { - String rv = null; - for (String s : aliases) { - List values = opt2values.get(s); - if (values != null && values.size() > 0) { - rv = values.get(values.size() - 1); // take last one, most recent - } - } - return rv; - } - - public int getSingleInt(int def, String... aliases) { - String r = getSingle(aliases); - if (r == null) { - return def; - } - return Integer.parseInt(r); - } - - public List getList(String... aliases) { - LinkedList rv = new LinkedList(); - for (String s : aliases) { - List values = opt2values.get(s); - if (values != null) { - rv.addAll(values); - } - } - return rv; - } - - public HgRepository findRepository() throws Exception { - String repoLocation = getSingle("-R", "--repository"); - if (repoLocation != null) { - return new HgLookup().detect(repoLocation); - } - return new HgLookup().detectFromWorkingDir(); - } - - - public static Options parse(String[] commandLineArgs) { - Options rv = new Options(); - List values = new LinkedList(); - rv.opt2values.put("", values); // values with no options - for (String arg : commandLineArgs) { - if (arg.charAt(0) == '-') { - // option - if (arg.length() == 1) { - throw new IllegalArgumentException("Bad option: -"); - } - values = rv.opt2values.get(arg); - if (values == null) { - rv.opt2values.put(arg, values = new LinkedList()); - } - // next value, if any, gets into the values list for arg option. - } else { - values.add(arg); - values = rv.opt2values.get(""); - } - } - return rv; - } -} \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 cmdline/org/tmatesoft/hg/console/Outgoing.java --- a/cmdline/org/tmatesoft/hg/console/Outgoing.java Mon May 09 11:49:23 2011 +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.console; - -import java.util.Collection; -import java.util.List; - -import org.tmatesoft.hg.core.HgOutgoingCommand; -import org.tmatesoft.hg.core.HgRepoFacade; -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.repo.HgLookup; -import org.tmatesoft.hg.repo.HgRemoteRepository; - - -/** - * hg outgoing - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class Outgoing { - - public static void main(String[] args) throws Exception { - Options cmdLineOpts = Options.parse(args); - HgRepoFacade hgRepo = new HgRepoFacade(); - if (!hgRepo.init(cmdLineOpts.findRepository())) { - System.err.printf("Can't find repository in: %s\n", hgRepo.getRepository().getLocation()); - return; - } - // XXX perhaps, HgRepoFacade shall get detectRemote() analog (to get remote server with respect of facade's repo) - HgRemoteRepository hgRemote = new HgLookup().detectRemote(cmdLineOpts.getSingle(""), hgRepo.getRepository()); - if (hgRemote.isInvalid()) { - System.err.printf("Remote repository %s is not valid", hgRemote.getLocation()); - return; - } - // - HgOutgoingCommand cmd = hgRepo.createOutgoingCommand(); - cmd.against(hgRemote); - - // find all local children of commonKnown - List result = cmd.executeLite(null); - dump("Lite", result); - // - // - System.out.println("Full"); - // show all, starting from next to common - final ChangesetDumpHandler h = new ChangesetDumpHandler(hgRepo.getRepository()); - h.complete(cmdLineOpts.getBoolean("--debug")).verbose(cmdLineOpts.getBoolean("-v", "--verbose")); - cmd.executeFull(h); - h.done(); - } - -// public static class ChangesetFormatter { -// private final StringBuilder sb = new StringBuilder(1024); -// -// public CharSequence simple(int revisionNumber, Nodeid nodeid, RawChangeset cset) { -// sb.setLength(0); -// sb.append(String.format("changeset: %d:%s\n", revisionNumber, nodeid.toString())); -// sb.append(String.format("user: %s\n", cset.user())); -// sb.append(String.format("date: %s\n", cset.dateString())); -// sb.append(String.format("comment: %s\n\n", cset.comment())); -// return sb; -// } -// } - - - static void dump(String s, Collection c) { - System.out.println(s); - for (Nodeid n : c) { - System.out.println(n); - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 cmdline/org/tmatesoft/hg/console/Remote.java --- a/cmdline/org/tmatesoft/hg/console/Remote.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,183 +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.console; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.List; -import java.util.Map; -import java.util.prefs.Preferences; -import java.util.zip.InflaterInputStream; - -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; - -import org.tmatesoft.hg.internal.ConfigFile; -import org.tmatesoft.hg.internal.Internals; - -/** - * WORK IN PROGRESS, DO NOT USE - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class Remote { - - /* - * @see http://mercurial.selenic.com/wiki/WireProtocol - cmd=branches gives 4 nodeids (head, root, first parent, second parent) per line (few lines possible, per branch, perhaps?) - cmd=capabilities gives lookup ...subset and 3 compress methods - // lookup changegroupsubset unbundle=HG10GZ,HG10BZ,HG10UN - cmd=heads gives space-separated list of nodeids (or just one) - nodeids are in hex (printable) format, need to convert fromAscii() - cmd=branchmap - cmd=between needs argument pairs, with first element in the pair to be head(!), second to be root of the branch ( - i.e. (newer-older), not (older-newer) as one might expect. Returned list of nodes comes in reversed order (from newer - to older) as well - - cmd=branches&nodes=d6d2a630f4a6d670c90a5ca909150f2b426ec88f+ - head, root, first parent, second parent - received: d6d2a630f4a6d670c90a5ca909150f2b426ec88f dbd663faec1f0175619cf7668bddc6350548b8d6 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 - - Sequence, for actual state with merged/closed branch, where 157:d5268ca7715b8d96204fc62abc632e8f55761547 is merge revision of 156 and 53 - >branches, 170:71ddbf8603e8e09d54ac9c5fe4bb5ae824589f1d - 71ddbf8603e8e09d54ac9c5fe4bb5ae824589f1d d5268ca7715b8d96204fc62abc632e8f55761547 643ddec3be36246fc052cf22ece503fa60cafe22 a6f39e595b2b54f56304470269a936ead77f5725 - - >branches, 156:643ddec3be36246fc052cf22ece503fa60cafe22 - 643ddec3be36246fc052cf22ece503fa60cafe22 ade65afe0906febafbf8a2e41002052e0e446471 08754fce5778a3409476ecdb3cec6b5172c34367 40d04c4f771ebbd599eb229145252732a596740a - >branches, 53:a6f39e595b2b54f56304470269a936ead77f5725 - a6f39e595b2b54f56304470269a936ead77f5725 a6f39e595b2b54f56304470269a936ead77f5725 9429c7bd1920fab164a9d2b621d38d57bcb49ae0 30bd389788464287cee22ccff54c330a4b715de5 - - >branches, 84:08754fce5778a3409476ecdb3cec6b5172c34367 (p1:82) - 08754fce5778a3409476ecdb3cec6b5172c34367 dbd663faec1f0175619cf7668bddc6350548b8d6 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 - >branches, 83:40d04c4f771ebbd599eb229145252732a596740a (p1:80) - 40d04c4f771ebbd599eb229145252732a596740a dbd663faec1f0175619cf7668bddc6350548b8d6 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 - - >branches, 51:9429c7bd1920fab164a9d2b621d38d57bcb49ae0 (wrap-data-access branch) - 9429c7bd1920fab164a9d2b621d38d57bcb49ae0 dbd663faec1f0175619cf7668bddc6350548b8d6 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 - >branches, 52:30bd389788464287cee22ccff54c330a4b715de5 (p1:50) - 30bd389788464287cee22ccff54c330a4b715de5 dbd663faec1f0175619cf7668bddc6350548b8d6 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 - - - cmd=between&pairs=71ddbf8603e8e09d54ac9c5fe4bb5ae824589f1d-d5268ca7715b8d96204fc62abc632e8f55761547+40d04c4f771ebbd599eb229145252732a596740a-dbd663faec1f0175619cf7668bddc6350548b8d6 - 8c8e3f372fa1fbfcf92b004b6f2ada2dbaf60028 dd525ca65de8e78cb133919de57ea0a6e6454664 1d0654be1466d522994f8bead510e360fbeb8d79 c17a08095e4420202ac1b2d939ef6d5f8bebb569 - 4222b04f34ee885bc1ad547c7ef330e18a51afc1 5f9635c016819b322ae05a91b3378621b538c933 c677e159391925a50b9a23f557426b2246bc9c5d 0d279bcc44427cb5ae2f3407c02f21187ccc8aea e21df6259f8374ac136767321e837c0c6dd21907 b01500fe2604c2c7eadf44349cce9f438484474b 865bf07f381ff7d1b742453568def92576af80b6 - - Between two subsequent revisions (i.e. direct child in remote of a local root) - cmd=between&pairs=71ddbf8603e8e09d54ac9c5fe4bb5ae824589f1d-8c8e3f372fa1fbfcf92b004b6f2ada2dbaf60028 - empty result - */ - public static void main(String[] args) throws Exception { - ConfigFile cfg = new Internals().newConfigFile(); - cfg.addLocation(new File(System.getProperty("user.home"), ".hgrc")); - String svnkitServer = cfg.getSection("paths").get("svnkit"); -// URL url = new URL(svnkitServer + "?cmd=branches&nodes=30bd389788464287cee22ccff54c330a4b715de5"); -// URL url = new URL(svnkitServer + "?cmd=between"); - URL url = new URL(svnkitServer + "?cmd=changegroup&roots=71ddbf8603e8e09d54ac9c5fe4bb5ae824589f1d"); -// URL url = new URL("http://localhost:8000/" + "?cmd=between"); -// URL url = new URL(svnkitServer + "?cmd=stream_out"); - - SSLContext sslContext = SSLContext.getInstance("SSL"); - class TrustEveryone implements X509TrustManager { - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - System.out.println("checkClientTrusted " + authType); - } - public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - System.out.println("checkServerTrusted" + authType); - } - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - } - // Hack to get Base64-encoded credentials - Preferences tempNode = Preferences.userRoot().node("xxx"); - tempNode.putByteArray("xxx", url.getUserInfo().getBytes()); - String authInfo = tempNode.get("xxx", null); - tempNode.removeNode(); - // - sslContext.init(null, new TrustManager[] { new TrustEveryone() }, null); - HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); -// HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); - urlConnection.setRequestProperty("User-Agent", "jhg/0.1.0"); - urlConnection.setRequestProperty("Accept", "application/mercurial-0.1"); - urlConnection.setRequestProperty("Authorization", "Basic " + authInfo); - urlConnection.setSSLSocketFactory(sslContext.getSocketFactory()); -// byte[] body = "pairs=f5aed108754e817d2ca374d1a4f6daf1218dcc91-9429c7bd1920fab164a9d2b621d38d57bcb49ae0".getBytes(); -// urlConnection.setRequestMethod("POST"); -// urlConnection.setRequestProperty("Content-Length", String.valueOf(body.length)); -// urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); -// urlConnection.setDoOutput(true); -// urlConnection.setDoInput(true); - urlConnection.connect(); -// OutputStream os = urlConnection.getOutputStream(); -// os.write(body); -// os.flush(); -// os.close(); - System.out.println("Query:" + url.getQuery()); - System.out.println("Response headers:"); - final Map> headerFields = urlConnection.getHeaderFields(); - for (String s : headerFields.keySet()) { - System.out.printf("%s: %s\n", s, urlConnection.getHeaderField(s)); - } - System.out.printf("Content type is %s and its length is %d\n", urlConnection.getContentType(), urlConnection.getContentLength()); - InputStream is = urlConnection.getInputStream(); - // -// dump(is, -1); // simple dump, any cmd - writeBundle(is, false, "HG10GZ"); // cmd=changegroup - //writeBundle(is, true, "" or "HG10UN"); - // - urlConnection.disconnect(); - // - } - - private static void dump(InputStream is, int limit) throws IOException { - int b; - while ((b =is.read()) != -1) { - System.out.print((char) b); - if (limit != -1) { - if (--limit < 0) { - break; - } - } - } - System.out.println(); - } - - private static void writeBundle(InputStream is, boolean decompress, String header) throws IOException { - InputStream zipStream = decompress ? new InflaterInputStream(is) : is; - File tf = File.createTempFile("hg-bundle-", null); - FileOutputStream fos = new FileOutputStream(tf); - fos.write(header.getBytes()); - int r; - byte[] buf = new byte[8*1024]; - while ((r = zipStream.read(buf)) != -1) { - fos.write(buf, 0, r); - } - fos.close(); - zipStream.close(); - System.out.println(tf); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 cmdline/org/tmatesoft/hg/console/Status.java --- a/cmdline/org/tmatesoft/hg/console/Status.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2010-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.console; - -import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -import org.tmatesoft.hg.core.HgRepoFacade; -import org.tmatesoft.hg.core.HgStatus; -import org.tmatesoft.hg.core.HgStatus.Kind; -import org.tmatesoft.hg.core.HgStatusCommand; -import org.tmatesoft.hg.util.Path; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class Status { - - public static void main(String[] args) throws Exception { - Options cmdLineOpts = Options.parse(args); - HgRepoFacade hgRepo = new HgRepoFacade(); - if (!hgRepo.init(cmdLineOpts.findRepository())) { - System.err.printf("Can't find repository in: %s\n", hgRepo.getRepository().getLocation()); - return; - } - // - HgStatusCommand cmd = hgRepo.createStatusCommand(); - if (cmdLineOpts.getBoolean("-A", "--all")) { - cmd.all(); - } else { - // default: mardu - cmd.modified(cmdLineOpts.getBoolean(true, "-m", "--modified")); - cmd.added(cmdLineOpts.getBoolean(true, "-a", "--added")); - cmd.removed(cmdLineOpts.getBoolean(true, "-r", "--removed")); - cmd.deleted(cmdLineOpts.getBoolean(true, "-d", "--deleted")); - cmd.unknown(cmdLineOpts.getBoolean(true, "-u", "--unknonwn")); - cmd.clean(cmdLineOpts.getBoolean("-c", "--clean")); - cmd.ignored(cmdLineOpts.getBoolean("-i", "--ignored")); - } -// cmd.subrepo(cmdLineOpts.getBoolean("-S", "--subrepos")) - final boolean noStatusPrefix = cmdLineOpts.getBoolean("-n", "--no-status"); - final boolean showCopies = cmdLineOpts.getBoolean("-C", "--copies"); - class StatusHandler implements HgStatusCommand.Handler { - - final Map> data = new TreeMap>(); - final Map copies = showCopies ? new HashMap() : null; - - public void handleStatus(HgStatus s) { - List l = data.get(s.getKind()); - if (l == null) { - l = new LinkedList(); - data.put(s.getKind(), l); - } - l.add(s.getPath()); - if (s.isCopy() && showCopies) { - copies.put(s.getPath(), s.getOriginalPath()); - } - } - - public void dump() { - sortAndPrint('M', data.get(Kind.Modified), null); - sortAndPrint('A', data.get(Kind.Added), copies); - sortAndPrint('R', data.get(Kind.Removed), null); - sortAndPrint('?', data.get(Kind.Unknown), null); - sortAndPrint('I', data.get(Kind.Ignored), null); - sortAndPrint('C', data.get(Kind.Clean), null); - sortAndPrint('!', data.get(Kind.Missing), null); - } - - private void sortAndPrint(char prefix, List ul, Map copies) { - if (ul == null) { - return; - } - ArrayList sortList = new ArrayList(ul); - Collections.sort(sortList); - for (Path s : sortList) { - if (!noStatusPrefix) { - System.out.print(prefix); - System.out.print(' '); - } - System.out.println(s); - if (copies != null && copies.containsKey(s)) { - System.out.println(" " + copies.get(s)); - } - } - } - }; - - StatusHandler statusHandler = new StatusHandler(); - int changeRev = cmdLineOpts.getSingleInt(BAD_REVISION, "--change"); - if (changeRev != BAD_REVISION) { - cmd.change(changeRev); - } else { - List revisions = cmdLineOpts.getList("--rev"); - int size = revisions.size(); - if (size > 1) { - cmd.base(Integer.parseInt(revisions.get(size - 2))).revision(Integer.parseInt(revisions.get(size - 1))); - } else if (size > 0) { - cmd.base(Integer.parseInt(revisions.get(0))); - } - } - cmd.execute(statusHandler); - statusHandler.dump(); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 gradle/wrapper/gradle-wrapper.jar Binary file gradle/wrapper/gradle-wrapper.jar has changed diff -r edb2e2829352 -r 6ec4af642ba8 gradle/wrapper/gradle-wrapper.properties --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gradle/wrapper/gradle-wrapper.properties Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,6 @@ +#Fri Apr 22 16:45:48 CEST 2011 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://repo.gradle.org/gradle/distributions/gradle-1.0-milestone-3-bin.zip diff -r edb2e2829352 -r 6ec4af642ba8 gradlew --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gradlew Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,168 @@ +#!/bin/bash + +############################################################################## +## ## +## Gradle wrapper script for UN*X ## +## ## +############################################################################## + +# Uncomment those lines to set JVM options. GRADLE_OPTS and JAVA_OPTS can be used together. +# GRADLE_OPTS="$GRADLE_OPTS -Xmx512m" +# JAVA_OPTS="$JAVA_OPTS -Xmx512m" + +GRADLE_APP_NAME=Gradle + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set JAVA_HOME if it's not already set. +if [ -z "$JAVA_HOME" ] ; then + if $darwin ; then + [ -z "$JAVA_HOME" -a -d "/Library/Java/Home" ] && export JAVA_HOME="/Library/Java/Home" + [ -z "$JAVA_HOME" -a -d "/System/Library/Frameworks/JavaVM.framework/Home" ] && export JAVA_HOME="/System/Library/Frameworks/JavaVM.framework/Home" + else + javaExecutable="`which javac`" + [ -z "$javaExecutable" -o "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ] && die "JAVA_HOME not set and cannot find javac to deduce location, please set JAVA_HOME." + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + [ `expr "$readLink" : '\([^ ]*\)'` = "no" ] && die "JAVA_HOME not set and readlink not available, please set JAVA_HOME." + javaExecutable="`readlink -f \"$javaExecutable\"`" + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + export JAVA_HOME="$javaHome" + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVACMD" ] && JAVACMD=`cygpath --unix "$JAVACMD"` + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +STARTER_MAIN_CLASS=org.gradle.wrapper.GradleWrapperMain +CLASSPATH=`dirname "$0"`/gradle/wrapper/gradle-wrapper.jar +WRAPPER_PROPERTIES=`dirname "$0"`/gradle/wrapper/gradle-wrapper.properties +# Determine the Java command to use to start the JVM. +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="java" + fi +fi +if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi +if [ -z "$JAVA_HOME" ] ; then + warn "JAVA_HOME environment variable is not set" +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query businessSystem maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add GRADLE_APP_NAME to the JAVA_OPTS as -Xdock:name +if $darwin; then + JAVA_OPTS="$JAVA_OPTS -Xdock:name=$GRADLE_APP_NAME" +# we may also want to set -Xdock:image +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + JAVA_HOME=`cygpath --path --mixed "$JAVA_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +GRADLE_APP_BASE_NAME=`basename "$0"` + +exec "$JAVACMD" $JAVA_OPTS $GRADLE_OPTS \ + -classpath "$CLASSPATH" \ + -Dorg.gradle.appname="$GRADLE_APP_BASE_NAME" \ + -Dorg.gradle.wrapper.properties="$WRAPPER_PROPERTIES" \ + $STARTER_MAIN_CLASS \ + "$@" diff -r edb2e2829352 -r 6ec4af642ba8 gradlew.bat --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gradlew.bat Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,82 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem ## +@rem Gradle startup script for Windows ## +@rem ## +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Uncomment those lines to set JVM options. GRADLE_OPTS and JAVA_OPTS can be used together. +@rem set GRADLE_OPTS=%GRADLE_OPTS% -Xmx512m +@rem set JAVA_OPTS=%JAVA_OPTS% -Xmx512m + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=.\ + +@rem Find java.exe +set JAVA_EXE=java.exe +if not defined JAVA_HOME goto init + +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. +echo. +goto end + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set STARTER_MAIN_CLASS=org.gradle.wrapper.GradleWrapperMain +set CLASSPATH=%DIRNAME%\gradle\wrapper\gradle-wrapper.jar +set WRAPPER_PROPERTIES=%DIRNAME%\gradle\wrapper\gradle-wrapper.properties + +set GRADLE_OPTS=%JAVA_OPTS% %GRADLE_OPTS% -Dorg.gradle.wrapper.properties="%WRAPPER_PROPERTIES%" + +@rem Execute Gradle +"%JAVA_EXE%" %GRADLE_OPTS% -classpath "%CLASSPATH%" %STARTER_MAIN_CLASS% %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +if not "%OS%"=="Windows_NT" echo 1 > nul | choice /n /c:1 + +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit "%ERRORLEVEL%" +exit /b "%ERRORLEVEL%" + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 hg4j-cli/src/main/java/org/tmatesoft/hg/console/Bundle.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j-cli/src/main/java/org/tmatesoft/hg/console/Bundle.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,96 @@ +/* + * 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.console; + +import java.io.File; + +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.HgBundle; +import org.tmatesoft.hg.repo.HgChangelog; +import org.tmatesoft.hg.repo.HgLookup; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; + + +/** + * WORK IN PROGRESS, DO NOT USE + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class Bundle { + public static void main(String[] args) throws Exception { + Options cmdLineOpts = Options.parse(args); + final HgRepository hgRepo = cmdLineOpts.findRepository(); + if (hgRepo.isInvalid()) { + System.err.printf("Can't find repository in: %s\n", hgRepo.getLocation()); + return; + } + File bundleFile = new File("/temp/hg/hg-bundle-cpython.tmp"); + HgBundle hgBundle = new HgLookup().loadBundle(bundleFile); + hgBundle.inspectFiles(new HgBundle.Dump()); + if (Boolean.parseBoolean("true")) { + return; + } + /* pass -R , e.g. for bundle with tip=168 and -R \temp\hg4j-50 with tip:159 + +Changeset {User: ..., Comment: Integer ....} + +Changeset {User: ..., Comment: Approach with ...} + -Changeset {User: ..., Comment: Correct project name...} + -Changeset {User: ..., Comment: Record possible...} + */ + hgBundle.changes(hgRepo, new HgChangelog.Inspector() { + private final HgChangelog changelog = hgRepo.getChangelog(); + + public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { + if (changelog.isKnown(nodeid)) { + System.out.print("+"); + } else { + System.out.print("-"); + } + System.out.printf("%d:%s\n%s\n", revisionNumber, nodeid.shortNotation(), cset.toString()); + } + }); + } + +/* + * TODO EXPLAIN why DataAccess.java on merge from branch has P2 set, and P1 is NULL + * + * excerpt from dump('hg-bundle-00') output (node, p1, p2, cs): + src/org/tmatesoft/hg/internal/DataAccess.java + 186af94a2a7ddb34190e63ce556d0fa4dd24add2 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 6f1b88693d48422e98c3eaaa8428ffd4d4d98ca7; patches:1 + be8d0fdc4ff268bf5eb0a9120282ce6e63de1606 186af94a2a7ddb34190e63ce556d0fa4dd24add2 0000000000000000000000000000000000000000 a3a2e5deb320d7412ccbb59bdc44668d445bc4c4; patches:2 + 333d7bbd4a80a5d6fb4b44e54e39e290f50dc7f8 be8d0fdc4ff268bf5eb0a9120282ce6e63de1606 0000000000000000000000000000000000000000 e93101b97e4ab0a3f3402ec0e80b6e559237c7c8; patches:1 + 56e4523cb8b42630daf70511d73d29e0b375dfa5 0000000000000000000000000000000000000000 333d7bbd4a80a5d6fb4b44e54e39e290f50dc7f8 d5268ca7715b8d96204fc62abc632e8f55761547; patches:6 + f85b6d7ed3cc4b7c6f99444eb0a41b58793cc900 56e4523cb8b42630daf70511d73d29e0b375dfa5 0000000000000000000000000000000000000000 b413b16d10a50cc027f4c38e4df5a9fedd618a79; patches:4 + + RevlogDump for the file says: + Index Offset Flags Packed Actual Base Rev Link Rev Parent1 Parent2 nodeid + 0: 4295032832 0 1109 2465 0 74 -1 -1 186af94a2a7ddb34190e63ce556d0fa4dd24add2 + 1: 1109 0 70 2364 0 102 0 -1 be8d0fdc4ff268bf5eb0a9120282ce6e63de1606 + 2: 1179 0 63 2365 0 122 1 -1 333d7bbd4a80a5d6fb4b44e54e39e290f50dc7f8 + 3: 1242 0 801 3765 0 157 -1 2 56e4523cb8b42630daf70511d73d29e0b375dfa5 + 4: 2043 0 130 3658 0 158 3 -1 f85b6d7ed3cc4b7c6f99444eb0a41b58793cc900 + + Excerpt from changelog dump: + 155: 30541 0 155 195 155 155 154 -1 a4ec5e08701771b96057522188b16ed289e9e8fe + 156: 30696 0 154 186 155 156 155 -1 643ddec3be36246fc052cf22ece503fa60cafe22 + 157: 30850 0 478 1422 155 157 156 53 d5268ca7715b8d96204fc62abc632e8f55761547 + 158: 31328 0 247 665 155 158 157 -1 b413b16d10a50cc027f4c38e4df5a9fedd618a79 + + + */ +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j-cli/src/main/java/org/tmatesoft/hg/console/Cat.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j-cli/src/main/java/org/tmatesoft/hg/console/Cat.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2010-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.console; + +import static org.tmatesoft.hg.repo.HgRepository.TIP; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +import org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.ByteChannel; + + +/** + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class Cat { + + public static void main(String[] args) throws Exception { + Options cmdLineOpts = Options.parse(args); + HgRepository hgRepo = cmdLineOpts.findRepository(); + if (hgRepo.isInvalid()) { + System.err.printf("Can't find repository in: %s\n", hgRepo.getLocation()); + return; + } + int rev = cmdLineOpts.getSingleInt(TIP, "-r", "--rev"); + OutputStreamChannel out = new OutputStreamChannel(System.out); + for (String fname : cmdLineOpts.getList("")) { + System.out.println(fname); + HgDataFile fn = hgRepo.getFileNode(fname); + if (fn.exists()) { + fn.contentWithFilters(rev, out); + System.out.println(); + } else { + System.out.printf("%s not found!\n", fname); + } + } + } + + private static class OutputStreamChannel implements ByteChannel { + + private final OutputStream stream; + + public OutputStreamChannel(OutputStream out) { + stream = out; + } + + public int write(ByteBuffer buffer) throws IOException { + int count = buffer.remaining(); + while(buffer.hasRemaining()) { + stream.write(buffer.get()); + } + return count; + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j-cli/src/main/java/org/tmatesoft/hg/console/ChangesetDumpHandler.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j-cli/src/main/java/org/tmatesoft/hg/console/ChangesetDumpHandler.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,165 @@ +/* + * 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.console; + +import java.util.Formatter; +import java.util.LinkedList; +import java.util.List; + +import org.tmatesoft.hg.core.HgChangeset; +import org.tmatesoft.hg.core.HgChangesetHandler; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.core.HgLogCommand.FileRevision; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.Path; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class ChangesetDumpHandler implements HgChangesetHandler { + // params + private boolean complete = false; // roughly --debug + private boolean reverseOrder = false; + private boolean verbose = false; // roughly -v + // own + private LinkedList l = new LinkedList(); + private final HgRepository repo; + private final int tip; + + public ChangesetDumpHandler(HgRepository hgRepo) { + repo = hgRepo; + tip = hgRepo.getChangelog().getLastRevision(); + } + + public ChangesetDumpHandler complete(boolean b) { + complete = b; + return this; + } + + public ChangesetDumpHandler reversed(boolean b) { + reverseOrder = b; + return this; + } + + public ChangesetDumpHandler verbose(boolean b) { + verbose = b; + return this; + } + + public void next(HgChangeset changeset) { + final String s = print(changeset); + if (reverseOrder) { + // XXX in fact, need to insert s into l according to changeset.getRevision() + // because when file history is being followed, revisions of the original file (with smaller revNumber) + // are reported *after* revisions of present file and with addFirst appear above them + l.addFirst(s); + } else { + System.out.print(s); + } + } + + public void done() { + if (!reverseOrder) { + return; + } + for (String s : l) { + System.out.print(s); + } + l.clear(); + } + + private String print(HgChangeset cset) { + 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)) { + + sb.append("tag: "); + for (String t : repo.getTags().tags(csetNodeid)) { + sb.append(t); + sb.append(' '); + } + if (cset.getRevision() == tip) { + sb.append("tip"); + } + sb.append('\n'); + } + if (complete) { + Nodeid p1 = cset.getFirstParentRevision(); + Nodeid p2 = cset.getSecondParentRevision(); + int p1x = p1 == Nodeid.NULL ? -1 : repo.getChangelog().getLocalRevision(p1); + int p2x = p2 == Nodeid.NULL ? -1 : repo.getChangelog().getLocalRevision(p2); + int mx = repo.getManifest().getLocalRevision(cset.getManifestRevision()); + f.format("parent: %d:%s\nparent: %d:%s\nmanifest: %d:%s\n", p1x, p1, p2x, p2, mx, cset.getManifestRevision()); + } + f.format("user: %s\ndate: %s\n", cset.getUser(), cset.getDate().toString()); + if (!complete && verbose) { + final List files = cset.getAffectedFiles(); + sb.append("files: "); + for (Path s : files) { + sb.append(' '); + sb.append(s); + } + sb.append('\n'); + } + if (complete) { + if (!cset.getModifiedFiles().isEmpty()) { + sb.append("files: "); + for (FileRevision s : cset.getModifiedFiles()) { + sb.append(' '); + sb.append(s.getPath()); + } + sb.append('\n'); + } + if (!cset.getAddedFiles().isEmpty()) { + sb.append("files+: "); + for (FileRevision s : cset.getAddedFiles()) { + sb.append(' '); + sb.append(s.getPath()); + } + sb.append('\n'); + } + if (!cset.getRemovedFiles().isEmpty()) { + sb.append("files-: "); + for (Path s : cset.getRemovedFiles()) { + sb.append(' '); + sb.append(s); + } + sb.append('\n'); + } + // if (cset.extras() != null) { + // sb.append("extra: "); + // for (Map.Entry e : cset.extras().entrySet()) { + // sb.append(' '); + // sb.append(e.getKey()); + // sb.append('='); + // sb.append(e.getValue()); + // } + // sb.append('\n'); + // } + } + if (complete || verbose) { + f.format("description:\n%s\n\n", cset.getComment()); + } else { + f.format("summary: %s\n\n", cset.getComment()); + } + return sb.toString(); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j-cli/src/main/java/org/tmatesoft/hg/console/Clone.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j-cli/src/main/java/org/tmatesoft/hg/console/Clone.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,57 @@ +/* + * 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.console; + +import java.io.File; +import java.util.List; + +import org.tmatesoft.hg.core.HgCloneCommand; +import org.tmatesoft.hg.repo.HgLookup; +import org.tmatesoft.hg.repo.HgRemoteRepository; + +/** + * Initial clone of a repository. Creates a brand new repository and populates it from specified source. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class Clone { + + // ran with args: svnkit c:\temp\hg\test-clone + public static void main(String[] args) throws Exception { + Options cmdLineOpts = Options.parse(args); + List noOptsArgs = cmdLineOpts.getList(""); + if (noOptsArgs.isEmpty()) { + System.err.println("Need at least one argument pointing to remote server to pull changes from"); + return; + } + HgCloneCommand cmd = new HgCloneCommand(); + String remoteRepo = noOptsArgs.get(0); + HgRemoteRepository hgRemote = new HgLookup().detectRemote(remoteRepo, null); + if (hgRemote.isInvalid()) { + System.err.printf("Remote repository %s is not valid", hgRemote.getLocation()); + return; + } + cmd.source(hgRemote); + if (noOptsArgs.size() > 1) { + cmd.destination(new File(noOptsArgs.get(1))); + } else { + cmd.destination(new File(System.getProperty("user.dir"))); + } + cmd.execute(); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j-cli/src/main/java/org/tmatesoft/hg/console/Incoming.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j-cli/src/main/java/org/tmatesoft/hg/console/Incoming.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,228 @@ +/* + * 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.console; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map.Entry; + +import org.tmatesoft.hg.core.HgIncomingCommand; +import org.tmatesoft.hg.core.HgRepoFacade; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.HgLookup; +import org.tmatesoft.hg.repo.HgRemoteRepository; + + +/** + * hg incoming counterpart + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class Incoming { + + public static void main(String[] args) throws Exception { + if (Boolean.FALSE.booleanValue()) { + new SequenceConstructor().test(); + return; + } + Options cmdLineOpts = Options.parse(args); + HgRepoFacade hgRepo = new HgRepoFacade(); + if (!hgRepo.init(cmdLineOpts.findRepository())) { + System.err.printf("Can't find repository in: %s\n", hgRepo.getRepository().getLocation()); + return; + } + HgRemoteRepository hgRemote = new HgLookup().detectRemote(cmdLineOpts.getSingle(""), hgRepo.getRepository()); + if (hgRemote.isInvalid()) { + System.err.printf("Remote repository %s is not valid", hgRemote.getLocation()); + return; + } + HgIncomingCommand cmd = hgRepo.createIncomingCommand(); + cmd.against(hgRemote); + // + List missing = cmd.executeLite(null); + Collections.reverse(missing); // useful to test output, from newer to older + Outgoing.dump("Nodes to fetch:", missing); + System.out.printf("Total: %d\n\n", missing.size()); + // + // Complete + final ChangesetDumpHandler h = new ChangesetDumpHandler(hgRepo.getRepository()); + h.complete(false); // this option looks up index of parent revision, done via repo.changelog (which doesn't have any of these new revisions) + // this can be fixed by tracking all nodeid->revision idx inside ChangesetDumpHandler, and refer to repo.changelog only when that mapping didn't work + h.verbose(cmdLineOpts.getBoolean("-v", "--verbose")); + cmd.executeFull(h); + } + + + /* + * This is for investigation purposes only + */ + private static class SequenceConstructor { + + private int[] between(int root, int head) { + if (head <= (root+1)) { + return new int[0]; + } + System.out.printf("[%d, %d]\t\t", root, head); + int size = 1 + (int) Math.floor(Math.log(head-root - 1) / Math.log(2)); + int[] rv = new int[size]; + for (int v = 1, i = 0; i < rv.length; i++) { + rv[i] = root + v; + v = v << 1; + } + System.out.println(Arrays.toString(rv)); + return rv; + } + + public void test() { + int root = 0, head = 126; + int[] data = between(root, head); // max number of elements to recover is 2**data.length-1, when head is exactly + // 2**data.length element of the branch. + // In such case, total number of elements in the branch (including head and root, would be 2**data.length+1 + int[] finalSequence = new int[1 + (1 << data.length >>> 5)]; // div 32 - total bits to integers, +1 for possible modulus + int exactNumberOfElements = -1; // exact number of meaningful bits in finalSequence + LinkedHashMap datas = new LinkedHashMap(); + datas.put(root, data); + int totalQueries = 1; + HashSet queried = new HashSet(); + int[] checkSequence = null; + while(!datas.isEmpty()) { + LinkedList toQuery = new LinkedList(); + do { + Iterator> it = datas.entrySet().iterator(); + Entry next = it.next(); + int r = next.getKey(); + data = next.getValue(); + it.remove(); + populate(r, head, data, finalSequence); + if (checkSequence != null) { + boolean match = true; +// System.out.println("Try to match:"); + for (int i = 0; i < checkSequence.length; i++) { +// System.out.println(i); +// System.out.println("control:" + toBinaryString(checkSequence[i], ' ')); +// System.out.println("present:" + toBinaryString(finalSequence[i], ' ')); + if (checkSequence[i] != finalSequence[i]) { + match = false; + } else { + match &= true; + } + } + System.out.println(match ? "Match, on query:" + totalQueries : "Didn't match"); + } + if (data.length > 1) { + /*queries for elements next to head is senseless, hence data.length check above and head-x below*/ + for (int x : data) { + if (!queried.contains(x) && head - x > 1) { + toQuery.add(new int[] {x, head}); + } + } + } + } while (!datas.isEmpty()) ; + if (!toQuery.isEmpty()) { + System.out.println(); + totalQueries++; + } + Collections.sort(toQuery, new Comparator() { + + public int compare(int[] o1, int[] o2) { + return o1[0] < o2[0] ? -1 : (o1[0] == o2[0] ? 0 : 1); + } + }); + for (int[] x : toQuery) { + if (!queried.contains(x[0])) { + queried.add(x[0]); + data = between(x[0], x[1]); + if (exactNumberOfElements == -1 && data.length == 1) { + exactNumberOfElements = x[0] + 1; + System.out.printf("On query %d found out exact number of missing elements: %d\n", totalQueries, exactNumberOfElements); + // get a bit sequence of exactNumberOfElements, 0111..110 + // to 'and' it with finalSequence later + int totalInts = (exactNumberOfElements + 2 /*heading and tailing zero bits*/) >>> 5; + int trailingBits = (exactNumberOfElements + 2) & 0x1f; + if (trailingBits != 0) { + totalInts++; + } + checkSequence = new int[totalInts]; + Arrays.fill(checkSequence, 0xffffffff); + checkSequence[0] &= 0x7FFFFFFF; + if (trailingBits == 0) { + checkSequence[totalInts-1] &= 0xFFFFFFFE; + } else if (trailingBits == 1) { + checkSequence[totalInts-1] = 0; + } else { + // trailingBits include heading and trailing zero bits + int mask = 0x80000000 >> trailingBits-2; // with sign! + checkSequence[totalInts - 1] &= mask; + } + for (int e : checkSequence) { + System.out.print(toBinaryString(e, ' ')); + } + System.out.println(); + } + datas.put(x[0], data); + } + } + } + + System.out.println("Total queries:" + totalQueries); + for (int x : finalSequence) { + System.out.print(toBinaryString(x, ' ')); + } + } + + private void populate(int root, int head, int[] data, int[] finalSequence) { + for (int i = 1, x = 0; root+i < head; i = i << 1, x++) { + int value = data[x]; + int value_check = root+i; + if (value != value_check) { + throw new IllegalStateException(); + } + int wordIx = (root + i) >>> 5; + int bitIx = (root + i) & 0x1f; + finalSequence[wordIx] |= 1 << (31-bitIx); + } + } + + private static String toBinaryString(int x, char byteSeparator) { + StringBuilder sb = new StringBuilder(4*8+4); + sb.append(toBinaryString((byte) (x >>> 24))); + sb.append(byteSeparator); + sb.append(toBinaryString((byte) ((x & 0x00ff0000) >>> 16))); + sb.append(byteSeparator); + sb.append(toBinaryString((byte) ((x & 0x00ff00) >>> 8))); + sb.append(byteSeparator); + sb.append(toBinaryString((byte) (x & 0x00ff))); + sb.append(byteSeparator); + return sb.toString(); + } + + private static String toBinaryString(byte b) { + final String nibbles = "0000000100100011010001010110011110001001101010111100110111101111"; + assert nibbles.length() == 16*4; + int x1 = (b >>> 4) & 0x0f, x2 = b & 0x0f; + x1 *= 4; x2 *= 4; // 4 characters per nibble + return nibbles.substring(x1, x1+4).concat(nibbles.substring(x2, x2+4)); + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j-cli/src/main/java/org/tmatesoft/hg/console/Log.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j-cli/src/main/java/org/tmatesoft/hg/console/Log.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2010-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.console; + +import java.util.List; + +import org.tmatesoft.hg.core.HgLogCommand; +import org.tmatesoft.hg.core.HgLogCommand.FileRevision; +import org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgRepository; + + +/** + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class Log { + + // -agentlib:hprof=heap=sites,depth=10,etc might be handy to debug speed/memory issues + + public static void main(String[] args) throws Exception { + Options cmdLineOpts = Options.parse(args); + HgRepository hgRepo = cmdLineOpts.findRepository(); + if (hgRepo.isInvalid()) { + System.err.printf("Can't find repository in: %s\n", hgRepo.getLocation()); + return; + } + final Dump dump = new Dump(hgRepo); + dump.complete(cmdLineOpts.getBoolean("--debug")); + dump.verbose(cmdLineOpts.getBoolean("-v", "--verbose")); + final boolean reverseOrder = true; + dump.reversed(reverseOrder); + HgLogCommand cmd = new HgLogCommand(hgRepo); + for (String u : cmdLineOpts.getList("-u", "--user")) { + cmd.user(u); + } + for (String b : cmdLineOpts.getList("-b", "--branches")) { + cmd.branch(b); + } + int limit = cmdLineOpts.getSingleInt(-1, "-l", "--limit"); + if (limit != -1) { + cmd.limit(limit); + } + List files = cmdLineOpts.getList(""); + final long start = System.currentTimeMillis(); + if (files.isEmpty()) { + if (limit == -1) { + // no revisions and no limit + cmd.execute(dump); + } else { + // in fact, external (to dump inspector) --limit processing yelds incorrect results when other args + // e.g. -u or -b are used (i.e. with -u shall give csets with user, not check last csets for user + int[] r = new int[] { 0, hgRepo.getChangelog().getRevisionCount() }; + if (fixRange(r, reverseOrder, limit) == 0) { + System.out.println("No changes"); + return; + } + cmd.range(r[0], r[1]).execute(dump); + } + dump.done(); + } else { + for (String fname : files) { + HgDataFile f1 = hgRepo.getFileNode(fname); + System.out.println("History of the file: " + f1.getPath()); + if (limit == -1) { + cmd.file(f1.getPath(), true).execute(dump); + } else { + int[] r = new int[] { 0, f1.getRevisionCount() }; + if (fixRange(r, reverseOrder, limit) == 0) { + System.out.println("No changes"); + continue; + } + cmd.range(r[0], r[1]).file(f1.getPath(), true).execute(dump); + } + dump.done(); + } + } +// cmd = null; + System.out.println("Total time:" + (System.currentTimeMillis() - start)); +// Main.force_gc(); + } + + private static int fixRange(int[] start_end, boolean reverse, int limit) { + assert start_end.length == 2; + if (limit < start_end[1]) { + if (reverse) { + // adjust left boundary of the range + start_end[0] = start_end[1] - limit; + } else { + start_end[1] = limit; // adjust right boundary + } + } + int rv = start_end[1] - start_end[0]; + start_end[1]--; // range needs index, not length + return rv; + } + + private static final class Dump extends ChangesetDumpHandler implements HgLogCommand.FileHistoryHandler { + + public Dump(HgRepository hgRepo) { + super(hgRepo); + } + + public void copy(FileRevision from, FileRevision to) { + System.out.printf("Got notified that %s(%s) was originally known as %s(%s)\n", to.getPath(), to.getRevision(), from.getPath(), from.getRevision()); + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j-cli/src/main/java/org/tmatesoft/hg/console/Main.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j-cli/src/main/java/org/tmatesoft/hg/console/Main.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,290 @@ +/* + * 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.console; + +import static org.tmatesoft.hg.repo.HgRepository.TIP; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.tmatesoft.hg.core.HgLogCommand.FileRevision; +import org.tmatesoft.hg.core.HgManifestCommand; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.ByteArrayChannel; +import org.tmatesoft.hg.internal.DigestHelper; +import org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgInternals; +import org.tmatesoft.hg.repo.HgManifest; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgStatusCollector; +import org.tmatesoft.hg.repo.HgStatusInspector; +import org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector; +import org.tmatesoft.hg.util.Path; + +/** + * Various debug dumps. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class Main { + + private Options cmdLineOpts; + private HgRepository hgRepo; + + public Main(String[] args) throws Exception { + cmdLineOpts = Options.parse(args); + hgRepo = cmdLineOpts.findRepository(); + if (hgRepo.isInvalid()) { + System.err.printf("Can't find repository in: %s\n", hgRepo.getLocation()); + return; + } + System.out.println("REPO:" + hgRepo.getLocation()); + } + + public static void main(String[] args) throws Exception { + Main m = new Main(args); + m.inflaterLengthException(); +// m.dumpIgnored(); +// m.dumpDirstate(); +// m.testStatusInternals(); +// m.catCompleteHistory(); +// m.dumpCompleteManifestLow(); +// m.dumpCompleteManifestHigh(); +// m.bunchOfTests(); + } + + private void inflaterLengthException() throws Exception { + HgDataFile f1 = hgRepo.getFileNode("src/com/tmate/hgkit/console/Bundle.java"); + HgDataFile f2 = hgRepo.getFileNode("test-repos.jar"); + System.out.println(f1.isCopy()); + System.out.println(f2.isCopy()); + ByteArrayChannel bac = new ByteArrayChannel(); + f1.content(1, bac); // 0: 1151, 1: 1139 + System.out.println(bac.toArray().length); + f2.content(0, bac = new ByteArrayChannel()); // 0: 14269 + System.out.println(bac.toArray().length); + } + + private void dumpIgnored() { + HgInternals debug = new HgInternals(hgRepo); + String[] toCheck = new String[] {"design.txt", "src/com/tmate/hgkit/ll/Changelog.java", "src/Extras.java", "bin/com/tmate/hgkit/ll/Changelog.class"}; + boolean[] checkResult = debug.checkIgnored(toCheck); + for (int i = 0; i < toCheck.length; i++) { + System.out.println("Ignored " + toCheck[i] + ": " + checkResult[i]); + } + } + + private void dumpDirstate() { + new HgInternals(hgRepo).dumpDirstate(); + } + + + private void catCompleteHistory() throws Exception { + DigestHelper dh = new DigestHelper(); + for (String fname : cmdLineOpts.getList("")) { + System.out.println(fname); + HgDataFile fn = hgRepo.getFileNode(fname); + if (fn.exists()) { + int total = fn.getRevisionCount(); + System.out.printf("Total revisions: %d\n", total); + for (int i = 0; i < total; i++) { + ByteArrayChannel sink = new ByteArrayChannel(); + fn.content(i, sink); + System.out.println("==========>"); + byte[] content = sink.toArray(); + System.out.println(new String(content)); + int[] parentRevisions = new int[2]; + byte[] parent1 = new byte[20]; + byte[] parent2 = new byte[20]; + fn.parents(i, parentRevisions, parent1, parent2); + System.out.println(dh.sha1(parent1, parent2, content).asHexString()); + } + } else { + System.out.println(">>>Not found!"); + } + } + } + + private void dumpCompleteManifestLow() { + hgRepo.getManifest().walk(0, TIP, new ManifestDump()); + } + + public static final class ManifestDump implements HgManifest.Inspector { + public boolean begin(int revision, Nodeid nid) { + System.out.printf("%d : %s\n", revision, nid); + return true; + } + + public boolean next(Nodeid nid, String fname, String flags) { + System.out.println(nid + "\t" + fname + "\t\t" + flags); + return true; + } + + public boolean end(int revision) { + System.out.println(); + return true; + } + } + + private void dumpCompleteManifestHigh() { + new HgManifestCommand(hgRepo).dirs(true).execute(new HgManifestCommand.Handler() { + + public void begin(Nodeid manifestRevision) { + System.out.println(">> " + manifestRevision); + } + public void dir(Path p) { + System.out.println(p); + } + public void file(FileRevision fileRevision) { + System.out.print(fileRevision.getRevision());; + System.out.print(" "); + System.out.println(fileRevision.getPath()); + } + + public void end(Nodeid manifestRevision) { + System.out.println(); + } + }); + } + + private void bunchOfTests() throws Exception { + HgInternals debug = new HgInternals(hgRepo); + debug.dumpDirstate(); + final StatusDump dump = new StatusDump(); + dump.showIgnored = false; + dump.showClean = false; + HgStatusCollector sc = new HgStatusCollector(hgRepo); + final int r1 = 0, r2 = 3; + System.out.printf("Status for changes between revision %d and %d:\n", r1, r2); + sc.walk(r1, r2, dump); + // + System.out.println("\n\nSame, but sorted in the way hg status does:"); + HgStatusCollector.Record r = sc.status(r1, r2); + sortAndPrint('M', r.getModified(), null); + sortAndPrint('A', r.getAdded(), null); + sortAndPrint('R', r.getRemoved(), null); + // + System.out.println("\n\nTry hg status --change :"); + sc.change(0, dump); + System.out.println("\nStatus against working dir:"); + HgWorkingCopyStatusCollector wcc = new HgWorkingCopyStatusCollector(hgRepo); + wcc.walk(TIP, dump); + System.out.println(); + System.out.printf("Manifest of the revision %d:\n", r2); + hgRepo.getManifest().walk(r2, r2, new ManifestDump()); + System.out.println(); + System.out.printf("\nStatus of working dir against %d:\n", r2); + r = wcc.status(r2); + sortAndPrint('M', r.getModified(), null); + sortAndPrint('A', r.getAdded(), r.getCopied()); + sortAndPrint('R', r.getRemoved(), null); + sortAndPrint('?', r.getUnknown(), null); + sortAndPrint('I', r.getIgnored(), null); + sortAndPrint('C', r.getClean(), null); + sortAndPrint('!', r.getMissing(), null); + } + + private void sortAndPrint(char prefix, List ul, Map copies) { + ArrayList sortList = new ArrayList(ul); + Collections.sort(sortList); + for (Path s : sortList) { + System.out.print(prefix); + System.out.print(' '); + System.out.println(s); + if (copies != null && copies.containsKey(s)) { + System.out.println(" " + copies.get(s)); + } + } + } + + + private void testStatusInternals() { + HgDataFile n = hgRepo.getFileNode(Path.create("design.txt")); + for (String s : new String[] {"011dfd44417c72bd9e54cf89b82828f661b700ed", "e5529faa06d53e06a816e56d218115b42782f1ba", "c18e7111f1fc89a80a00f6a39d51288289a382fc"}) { + // expected: 359, 2123, 3079 + byte[] b = s.getBytes(); + final Nodeid nid = Nodeid.fromAscii(b, 0, b.length); + System.out.println(s + " : " + n.length(nid)); + } + } + + static void force_gc() { + Runtime.getRuntime().runFinalization(); + Runtime.getRuntime().gc(); + Thread.yield(); + Runtime.getRuntime().runFinalization(); + Runtime.getRuntime().gc(); + Thread.yield(); + } + + private static class StatusDump implements HgStatusInspector { + public boolean hideStatusPrefix = false; // hg status -n option + public boolean showCopied = true; // -C + public boolean showIgnored = true; // -i + public boolean showClean = true; // -c + + public void modified(Path fname) { + print('M', fname); + } + + public void added(Path fname) { + print('A', fname); + } + + public void copied(Path fnameOrigin, Path fnameAdded) { + added(fnameAdded); + if (showCopied) { + print(' ', fnameOrigin); + } + } + + public void removed(Path fname) { + print('R', fname); + } + + public void clean(Path fname) { + if (showClean) { + print('C', fname); + } + } + + public void missing(Path fname) { + print('!', fname); + } + + public void unknown(Path fname) { + print('?', fname); + } + + public void ignored(Path fname) { + if (showIgnored) { + print('I', fname); + } + } + + private void print(char status, Path fname) { + if (!hideStatusPrefix) { + System.out.print(status); + System.out.print(' '); + } + System.out.println(fname); + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j-cli/src/main/java/org/tmatesoft/hg/console/Manifest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j-cli/src/main/java/org/tmatesoft/hg/console/Manifest.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010-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.console; + +import static org.tmatesoft.hg.repo.HgRepository.TIP; + +import org.tmatesoft.hg.core.HgLogCommand.FileRevision; +import org.tmatesoft.hg.core.HgManifestCommand; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.Path; + + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class Manifest { + + public static void main(String[] args) throws Exception { + Options cmdLineOpts = Options.parse(args); + HgRepository hgRepo = cmdLineOpts.findRepository(); + if (hgRepo.isInvalid()) { + System.err.printf("Can't find repository in: %s\n", hgRepo.getLocation()); + return; + } + final boolean debug = cmdLineOpts.getBoolean("--debug"); + final boolean verbose = cmdLineOpts.getBoolean("-v", "--verbose"); + HgManifestCommand.Handler h = new HgManifestCommand.Handler() { + + public void begin(Nodeid manifestRevision) { + } + public void dir(Path p) { + } + public void file(FileRevision fileRevision) { + if (debug) { + System.out.print(fileRevision.getRevision());; + } + 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); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j-cli/src/main/java/org/tmatesoft/hg/console/Options.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j-cli/src/main/java/org/tmatesoft/hg/console/Options.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,111 @@ +/* + * 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.console; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.tmatesoft.hg.repo.HgLookup; +import org.tmatesoft.hg.repo.HgRepository; + +/** + * Parse command-line options. Primitive implementation that recognizes options with 0 or 1 argument. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +class Options { + + public final Map> opt2values = new HashMap>(); + + public boolean getBoolean(String... aliases) { + return getBoolean(false, aliases); + } + + public boolean getBoolean(boolean def, String... aliases) { + for (String s : aliases) { + if (opt2values.containsKey(s)) { + return true; + } + } + return def; + } + + public String getSingle(String... aliases) { + String rv = null; + for (String s : aliases) { + List values = opt2values.get(s); + if (values != null && values.size() > 0) { + rv = values.get(values.size() - 1); // take last one, most recent + } + } + return rv; + } + + public int getSingleInt(int def, String... aliases) { + String r = getSingle(aliases); + if (r == null) { + return def; + } + return Integer.parseInt(r); + } + + public List getList(String... aliases) { + LinkedList rv = new LinkedList(); + for (String s : aliases) { + List values = opt2values.get(s); + if (values != null) { + rv.addAll(values); + } + } + return rv; + } + + public HgRepository findRepository() throws Exception { + String repoLocation = getSingle("-R", "--repository"); + if (repoLocation != null) { + return new HgLookup().detect(repoLocation); + } + return new HgLookup().detectFromWorkingDir(); + } + + + public static Options parse(String[] commandLineArgs) { + Options rv = new Options(); + List values = new LinkedList(); + rv.opt2values.put("", values); // values with no options + for (String arg : commandLineArgs) { + if (arg.charAt(0) == '-') { + // option + if (arg.length() == 1) { + throw new IllegalArgumentException("Bad option: -"); + } + values = rv.opt2values.get(arg); + if (values == null) { + rv.opt2values.put(arg, values = new LinkedList()); + } + // next value, if any, gets into the values list for arg option. + } else { + values.add(arg); + values = rv.opt2values.get(""); + } + } + return rv; + } +} \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 hg4j-cli/src/main/java/org/tmatesoft/hg/console/Outgoing.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j-cli/src/main/java/org/tmatesoft/hg/console/Outgoing.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,87 @@ +/* + * 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.console; + +import java.util.Collection; +import java.util.List; + +import org.tmatesoft.hg.core.HgOutgoingCommand; +import org.tmatesoft.hg.core.HgRepoFacade; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.HgLookup; +import org.tmatesoft.hg.repo.HgRemoteRepository; + + +/** + * hg outgoing + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class Outgoing { + + public static void main(String[] args) throws Exception { + Options cmdLineOpts = Options.parse(args); + HgRepoFacade hgRepo = new HgRepoFacade(); + if (!hgRepo.init(cmdLineOpts.findRepository())) { + System.err.printf("Can't find repository in: %s\n", hgRepo.getRepository().getLocation()); + return; + } + // XXX perhaps, HgRepoFacade shall get detectRemote() analog (to get remote server with respect of facade's repo) + HgRemoteRepository hgRemote = new HgLookup().detectRemote(cmdLineOpts.getSingle(""), hgRepo.getRepository()); + if (hgRemote.isInvalid()) { + System.err.printf("Remote repository %s is not valid", hgRemote.getLocation()); + return; + } + // + HgOutgoingCommand cmd = hgRepo.createOutgoingCommand(); + cmd.against(hgRemote); + + // find all local children of commonKnown + List result = cmd.executeLite(null); + dump("Lite", result); + // + // + System.out.println("Full"); + // show all, starting from next to common + final ChangesetDumpHandler h = new ChangesetDumpHandler(hgRepo.getRepository()); + h.complete(cmdLineOpts.getBoolean("--debug")).verbose(cmdLineOpts.getBoolean("-v", "--verbose")); + cmd.executeFull(h); + h.done(); + } + +// public static class ChangesetFormatter { +// private final StringBuilder sb = new StringBuilder(1024); +// +// public CharSequence simple(int revisionNumber, Nodeid nodeid, RawChangeset cset) { +// sb.setLength(0); +// sb.append(String.format("changeset: %d:%s\n", revisionNumber, nodeid.toString())); +// sb.append(String.format("user: %s\n", cset.user())); +// sb.append(String.format("date: %s\n", cset.dateString())); +// sb.append(String.format("comment: %s\n\n", cset.comment())); +// return sb; +// } +// } + + + static void dump(String s, Collection c) { + System.out.println(s); + for (Nodeid n : c) { + System.out.println(n); + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j-cli/src/main/java/org/tmatesoft/hg/console/Remote.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j-cli/src/main/java/org/tmatesoft/hg/console/Remote.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,183 @@ +/* + * 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.console; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.List; +import java.util.Map; +import java.util.prefs.Preferences; +import java.util.zip.InflaterInputStream; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.tmatesoft.hg.internal.ConfigFile; +import org.tmatesoft.hg.internal.Internals; + +/** + * WORK IN PROGRESS, DO NOT USE + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class Remote { + + /* + * @see http://mercurial.selenic.com/wiki/WireProtocol + cmd=branches gives 4 nodeids (head, root, first parent, second parent) per line (few lines possible, per branch, perhaps?) + cmd=capabilities gives lookup ...subset and 3 compress methods + // lookup changegroupsubset unbundle=HG10GZ,HG10BZ,HG10UN + cmd=heads gives space-separated list of nodeids (or just one) + nodeids are in hex (printable) format, need to convert fromAscii() + cmd=branchmap + cmd=between needs argument pairs, with first element in the pair to be head(!), second to be root of the branch ( + i.e. (newer-older), not (older-newer) as one might expect. Returned list of nodes comes in reversed order (from newer + to older) as well + + cmd=branches&nodes=d6d2a630f4a6d670c90a5ca909150f2b426ec88f+ + head, root, first parent, second parent + received: d6d2a630f4a6d670c90a5ca909150f2b426ec88f dbd663faec1f0175619cf7668bddc6350548b8d6 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 + + Sequence, for actual state with merged/closed branch, where 157:d5268ca7715b8d96204fc62abc632e8f55761547 is merge revision of 156 and 53 + >branches, 170:71ddbf8603e8e09d54ac9c5fe4bb5ae824589f1d + 71ddbf8603e8e09d54ac9c5fe4bb5ae824589f1d d5268ca7715b8d96204fc62abc632e8f55761547 643ddec3be36246fc052cf22ece503fa60cafe22 a6f39e595b2b54f56304470269a936ead77f5725 + + >branches, 156:643ddec3be36246fc052cf22ece503fa60cafe22 + 643ddec3be36246fc052cf22ece503fa60cafe22 ade65afe0906febafbf8a2e41002052e0e446471 08754fce5778a3409476ecdb3cec6b5172c34367 40d04c4f771ebbd599eb229145252732a596740a + >branches, 53:a6f39e595b2b54f56304470269a936ead77f5725 + a6f39e595b2b54f56304470269a936ead77f5725 a6f39e595b2b54f56304470269a936ead77f5725 9429c7bd1920fab164a9d2b621d38d57bcb49ae0 30bd389788464287cee22ccff54c330a4b715de5 + + >branches, 84:08754fce5778a3409476ecdb3cec6b5172c34367 (p1:82) + 08754fce5778a3409476ecdb3cec6b5172c34367 dbd663faec1f0175619cf7668bddc6350548b8d6 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 + >branches, 83:40d04c4f771ebbd599eb229145252732a596740a (p1:80) + 40d04c4f771ebbd599eb229145252732a596740a dbd663faec1f0175619cf7668bddc6350548b8d6 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 + + >branches, 51:9429c7bd1920fab164a9d2b621d38d57bcb49ae0 (wrap-data-access branch) + 9429c7bd1920fab164a9d2b621d38d57bcb49ae0 dbd663faec1f0175619cf7668bddc6350548b8d6 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 + >branches, 52:30bd389788464287cee22ccff54c330a4b715de5 (p1:50) + 30bd389788464287cee22ccff54c330a4b715de5 dbd663faec1f0175619cf7668bddc6350548b8d6 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 + + + cmd=between&pairs=71ddbf8603e8e09d54ac9c5fe4bb5ae824589f1d-d5268ca7715b8d96204fc62abc632e8f55761547+40d04c4f771ebbd599eb229145252732a596740a-dbd663faec1f0175619cf7668bddc6350548b8d6 + 8c8e3f372fa1fbfcf92b004b6f2ada2dbaf60028 dd525ca65de8e78cb133919de57ea0a6e6454664 1d0654be1466d522994f8bead510e360fbeb8d79 c17a08095e4420202ac1b2d939ef6d5f8bebb569 + 4222b04f34ee885bc1ad547c7ef330e18a51afc1 5f9635c016819b322ae05a91b3378621b538c933 c677e159391925a50b9a23f557426b2246bc9c5d 0d279bcc44427cb5ae2f3407c02f21187ccc8aea e21df6259f8374ac136767321e837c0c6dd21907 b01500fe2604c2c7eadf44349cce9f438484474b 865bf07f381ff7d1b742453568def92576af80b6 + + Between two subsequent revisions (i.e. direct child in remote of a local root) + cmd=between&pairs=71ddbf8603e8e09d54ac9c5fe4bb5ae824589f1d-8c8e3f372fa1fbfcf92b004b6f2ada2dbaf60028 + empty result + */ + public static void main(String[] args) throws Exception { + ConfigFile cfg = new Internals().newConfigFile(); + cfg.addLocation(new File(System.getProperty("user.home"), ".hgrc")); + String svnkitServer = cfg.getSection("paths").get("svnkit"); +// URL url = new URL(svnkitServer + "?cmd=branches&nodes=30bd389788464287cee22ccff54c330a4b715de5"); +// URL url = new URL(svnkitServer + "?cmd=between"); + URL url = new URL(svnkitServer + "?cmd=changegroup&roots=71ddbf8603e8e09d54ac9c5fe4bb5ae824589f1d"); +// URL url = new URL("http://localhost:8000/" + "?cmd=between"); +// URL url = new URL(svnkitServer + "?cmd=stream_out"); + + SSLContext sslContext = SSLContext.getInstance("SSL"); + class TrustEveryone implements X509TrustManager { + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + System.out.println("checkClientTrusted " + authType); + } + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + System.out.println("checkServerTrusted" + authType); + } + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + } + // Hack to get Base64-encoded credentials + Preferences tempNode = Preferences.userRoot().node("xxx"); + tempNode.putByteArray("xxx", url.getUserInfo().getBytes()); + String authInfo = tempNode.get("xxx", null); + tempNode.removeNode(); + // + sslContext.init(null, new TrustManager[] { new TrustEveryone() }, null); + HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); +// HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + urlConnection.setRequestProperty("User-Agent", "jhg/0.1.0"); + urlConnection.setRequestProperty("Accept", "application/mercurial-0.1"); + urlConnection.setRequestProperty("Authorization", "Basic " + authInfo); + urlConnection.setSSLSocketFactory(sslContext.getSocketFactory()); +// byte[] body = "pairs=f5aed108754e817d2ca374d1a4f6daf1218dcc91-9429c7bd1920fab164a9d2b621d38d57bcb49ae0".getBytes(); +// urlConnection.setRequestMethod("POST"); +// urlConnection.setRequestProperty("Content-Length", String.valueOf(body.length)); +// urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); +// urlConnection.setDoOutput(true); +// urlConnection.setDoInput(true); + urlConnection.connect(); +// OutputStream os = urlConnection.getOutputStream(); +// os.write(body); +// os.flush(); +// os.close(); + System.out.println("Query:" + url.getQuery()); + System.out.println("Response headers:"); + final Map> headerFields = urlConnection.getHeaderFields(); + for (String s : headerFields.keySet()) { + System.out.printf("%s: %s\n", s, urlConnection.getHeaderField(s)); + } + System.out.printf("Content type is %s and its length is %d\n", urlConnection.getContentType(), urlConnection.getContentLength()); + InputStream is = urlConnection.getInputStream(); + // +// dump(is, -1); // simple dump, any cmd + writeBundle(is, false, "HG10GZ"); // cmd=changegroup + //writeBundle(is, true, "" or "HG10UN"); + // + urlConnection.disconnect(); + // + } + + private static void dump(InputStream is, int limit) throws IOException { + int b; + while ((b =is.read()) != -1) { + System.out.print((char) b); + if (limit != -1) { + if (--limit < 0) { + break; + } + } + } + System.out.println(); + } + + private static void writeBundle(InputStream is, boolean decompress, String header) throws IOException { + InputStream zipStream = decompress ? new InflaterInputStream(is) : is; + File tf = File.createTempFile("hg-bundle-", null); + FileOutputStream fos = new FileOutputStream(tf); + fos.write(header.getBytes()); + int r; + byte[] buf = new byte[8*1024]; + while ((r = zipStream.read(buf)) != -1) { + fos.write(buf, 0, r); + } + fos.close(); + zipStream.close(); + System.out.println(tf); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j-cli/src/main/java/org/tmatesoft/hg/console/Status.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j-cli/src/main/java/org/tmatesoft/hg/console/Status.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2010-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.console; + +import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.tmatesoft.hg.core.HgRepoFacade; +import org.tmatesoft.hg.core.HgStatus; +import org.tmatesoft.hg.core.HgStatus.Kind; +import org.tmatesoft.hg.core.HgStatusCommand; +import org.tmatesoft.hg.util.Path; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class Status { + + public static void main(String[] args) throws Exception { + Options cmdLineOpts = Options.parse(args); + HgRepoFacade hgRepo = new HgRepoFacade(); + if (!hgRepo.init(cmdLineOpts.findRepository())) { + System.err.printf("Can't find repository in: %s\n", hgRepo.getRepository().getLocation()); + return; + } + // + HgStatusCommand cmd = hgRepo.createStatusCommand(); + if (cmdLineOpts.getBoolean("-A", "--all")) { + cmd.all(); + } else { + // default: mardu + cmd.modified(cmdLineOpts.getBoolean(true, "-m", "--modified")); + cmd.added(cmdLineOpts.getBoolean(true, "-a", "--added")); + cmd.removed(cmdLineOpts.getBoolean(true, "-r", "--removed")); + cmd.deleted(cmdLineOpts.getBoolean(true, "-d", "--deleted")); + cmd.unknown(cmdLineOpts.getBoolean(true, "-u", "--unknonwn")); + cmd.clean(cmdLineOpts.getBoolean("-c", "--clean")); + cmd.ignored(cmdLineOpts.getBoolean("-i", "--ignored")); + } +// cmd.subrepo(cmdLineOpts.getBoolean("-S", "--subrepos")) + final boolean noStatusPrefix = cmdLineOpts.getBoolean("-n", "--no-status"); + final boolean showCopies = cmdLineOpts.getBoolean("-C", "--copies"); + class StatusHandler implements HgStatusCommand.Handler { + + final Map> data = new TreeMap>(); + final Map copies = showCopies ? new HashMap() : null; + + public void handleStatus(HgStatus s) { + List l = data.get(s.getKind()); + if (l == null) { + l = new LinkedList(); + data.put(s.getKind(), l); + } + l.add(s.getPath()); + if (s.isCopy() && showCopies) { + copies.put(s.getPath(), s.getOriginalPath()); + } + } + + public void dump() { + sortAndPrint('M', data.get(Kind.Modified), null); + sortAndPrint('A', data.get(Kind.Added), copies); + sortAndPrint('R', data.get(Kind.Removed), null); + sortAndPrint('?', data.get(Kind.Unknown), null); + sortAndPrint('I', data.get(Kind.Ignored), null); + sortAndPrint('C', data.get(Kind.Clean), null); + sortAndPrint('!', data.get(Kind.Missing), null); + } + + private void sortAndPrint(char prefix, List ul, Map copies) { + if (ul == null) { + return; + } + ArrayList sortList = new ArrayList(ul); + Collections.sort(sortList); + for (Path s : sortList) { + if (!noStatusPrefix) { + System.out.print(prefix); + System.out.print(' '); + } + System.out.println(s); + if (copies != null && copies.containsKey(s)) { + System.out.println(" " + copies.get(s)); + } + } + } + }; + + StatusHandler statusHandler = new StatusHandler(); + int changeRev = cmdLineOpts.getSingleInt(BAD_REVISION, "--change"); + if (changeRev != BAD_REVISION) { + cmd.change(changeRev); + } else { + List revisions = cmdLineOpts.getList("--rev"); + int size = revisions.size(); + if (size > 1) { + cmd.base(Integer.parseInt(revisions.get(size - 2))).revision(Integer.parseInt(revisions.get(size - 1))); + } else if (size > 0) { + cmd.base(Integer.parseInt(revisions.get(0))); + } + } + cmd.execute(statusHandler); + statusHandler.dump(); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/ChangesetTransformer.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/ChangesetTransformer.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,65 @@ +/* + * 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.util.Set; + +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.util.PathPool; +import org.tmatesoft.hg.util.PathRewrite; + +/** + * Bridges {@link HgChangelog.RawChangeset} with high-level {@link HgChangeset} API + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +/*package-local*/ class ChangesetTransformer implements HgChangelog.Inspector { + private final HgChangesetHandler handler; + private final HgChangeset changeset; + private Set branches; + + // repo and delegate can't be null, parent walker can + public ChangesetTransformer(HgRepository hgRepo, HgChangesetHandler delegate, HgChangelog.ParentWalker pw) { + if (hgRepo == null || delegate == null) { + throw new IllegalArgumentException(); + } + HgStatusCollector statusCollector = new HgStatusCollector(hgRepo); + // files listed in a changeset don't need their names to be rewritten (they are normalized already) + PathPool pp = new PathPool(new PathRewrite.Empty()); + statusCollector.setPathPool(pp); + changeset = new HgChangeset(statusCollector, pp); + changeset.setParentHelper(pw); + handler = delegate; + } + + public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { + if (branches != null && !branches.contains(cset.branch())) { + return; + } + + changeset.init(revisionNumber, nodeid, cset); + handler.next(changeset); + } + + public void limitBranches(Set branches) { + this.branches = branches; + } +} \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgBadArgumentException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgBadArgumentException.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,30 @@ +/* + * 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; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@SuppressWarnings("serial") +public class HgBadArgumentException extends HgException { + + public HgBadArgumentException(String message, Throwable cause) { + super(message, cause); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgBadStateException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgBadStateException.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,40 @@ +/* + * 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 edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgCatCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgCatCommand.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,123 @@ +/* + * 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 static org.tmatesoft.hg.repo.HgInternals.wrongLocalRevision; +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 org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.ByteChannel; +import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.util.Path; + +/** + * Command to obtain content of a file, 'hg cat' counterpart. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgCatCommand { + + private final HgRepository repo; + private Path file; + private int localRevision = TIP; + private Nodeid revision; + + public HgCatCommand(HgRepository hgRepo) { + repo = hgRepo; + } + + /** + * File to read, required parameter + * @param fname path to a repository file, can't be null + * @return this for convenience + * @throws IllegalArgumentException if supplied fname is null or points to directory + */ + public HgCatCommand file(Path fname) { + if (fname == null || fname.isDirectory()) { + throw new IllegalArgumentException(String.valueOf(fname)); + } + file = fname; + return this; + } + + /** + * Invocation of this method clears revision set with {@link #revision(Nodeid)} or {@link #revision(int)} earlier. + * XXX rev can't be WORKING_COPY (if allowed, need to implement in #execute()) + * @param rev local revision number, non-negative, or one of predefined constants. Note, use of {@link HgRepository#BAD_REVISION}, + * although possible, makes little sense (command would fail if executed). + * @return this for convenience + */ + public HgCatCommand revision(int rev) { + if (wrongLocalRevision(rev)) { + throw new IllegalArgumentException(String.valueOf(rev)); + } + localRevision = rev; + revision = null; + return this; + } + + /** + * Select revision to read. Invocation of this method clears revision set with {@link #revision(int)} or {@link #revision(Nodeid)} earlier. + * + * @param nodeid - unique revision identifier, Note, use of null or {@link Nodeid#NULL} is senseless + * @return this for convenience + */ + public HgCatCommand revision(Nodeid nodeid) { + if (nodeid != null && nodeid.isNull()) { + nodeid = null; + } + revision = nodeid; + localRevision = BAD_REVISION; + return this; + } + + /** + * 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 IllegalArgumentException when command arguments are incomplete or wrong + */ + public void execute(ByteChannel sink) throws HgDataStreamException, IOException, CancelledException { + if (localRevision == BAD_REVISION && revision == null) { + throw new IllegalArgumentException("Either local file revision number or nodeid shall be specified"); + } + if (file == null) { + throw new IllegalArgumentException("Name of the file is missing"); + } + if (sink == null) { + throw new IllegalArgumentException("Need an output channel"); + } + HgDataFile dataFile = repo.getFileNode(file); + if (!dataFile.exists()) { + throw new HgDataStreamException(file.toString(), new FileNotFoundException(file.toString())); + } + int revToExtract; + if (revision != null) { + revToExtract = dataFile.getLocalRevision(revision); + } else { + revToExtract = localRevision; + } + dataFile.contentWithFilters(revToExtract, sink); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgChangeset.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgChangeset.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,215 @@ +/* + * 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 static org.tmatesoft.hg.core.Nodeid.NULL; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.tmatesoft.hg.core.HgLogCommand.FileRevision; +import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; +import org.tmatesoft.hg.repo.HgChangelog; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgStatusCollector; +import org.tmatesoft.hg.util.Path; + + +/** + * Record in the Mercurial changelog, describing single commit. + * + * Not thread-safe, don't try to read from different threads + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgChangeset implements Cloneable { + private final HgStatusCollector statusHelper; + private final Path.Source pathHelper; + + private HgChangelog.ParentWalker parentHelper; + + // + private RawChangeset changeset; + private Nodeid nodeid; + + // + private List modifiedFiles, addedFiles; + private List deletedFiles; + private int revNumber; + private byte[] parent1, parent2; + + // XXX consider CommandContext with StatusCollector, PathPool etc. Commands optionally get CC through a cons or create new + // and pass it around + /*package-local*/HgChangeset(HgStatusCollector statusCollector, Path.Source pathFactory) { + statusHelper = statusCollector; + pathHelper = pathFactory; + } + + /*package-local*/ void init(int localRevNumber, Nodeid nid, RawChangeset rawChangeset) { + revNumber = localRevNumber; + nodeid = nid; + changeset = rawChangeset.clone(); + modifiedFiles = addedFiles = null; + deletedFiles = null; + parent1 = parent2 = null; + // keep references to parentHelper, statusHelper and pathHelper + } + + /*package-local*/ void setParentHelper(HgChangelog.ParentWalker pw) { + parentHelper = pw; + if (parentHelper != null) { + if (parentHelper.getRepo() != statusHelper.getRepo()) { + throw new IllegalArgumentException(); + } + } + } + + public int getRevision() { + return revNumber; + } + public Nodeid getNodeid() { + return nodeid; + } + public String getUser() { + return changeset.user(); + } + public String getComment() { + return changeset.comment(); + } + public String getBranch() { + return changeset.branch(); + } + + /** + * @return used to be String, now {@link HgDate}, use {@link HgDate#toString()} to get same result as before + */ + public HgDate getDate() { + return new HgDate(changeset.date().getTime(), changeset.timezone()); + } + public Nodeid getManifestRevision() { + return changeset.manifest(); + } + + 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 + // #getModifiedFiles() would return list with merged file(s) (because it uses status to get 'em, not + // what #files() gives). + ArrayList rv = new ArrayList(changeset.files().size()); + for (String name : changeset.files()) { + rv.add(pathHelper.path(name)); + } + return rv; + } + + public List getModifiedFiles() { + if (modifiedFiles == null) { + initFileChanges(); + } + return modifiedFiles; + } + + public List getAddedFiles() { + if (addedFiles == null) { + initFileChanges(); + } + return addedFiles; + } + + public List getRemovedFiles() { + if (deletedFiles == null) { + initFileChanges(); + } + return deletedFiles; + } + + public boolean isMerge() { + // p1 == -1 and p2 != -1 is legitimate case + return !NULL.equals(getFirstParentRevision()) && !NULL.equals(getSecondParentRevision()); + } + + public Nodeid getFirstParentRevision() { + if (parentHelper != null) { + return parentHelper.safeFirstParent(nodeid); + } + // read once for both p1 and p2 + if (parent1 == null) { + parent1 = new byte[20]; + parent2 = new byte[20]; + statusHelper.getRepo().getChangelog().parents(revNumber, new int[2], parent1, parent2); + } + return Nodeid.fromBinary(parent1, 0); + } + + public Nodeid getSecondParentRevision() { + if (parentHelper != null) { + return parentHelper.safeSecondParent(nodeid); + } + if (parent2 == null) { + parent1 = new byte[20]; + parent2 = new byte[20]; + statusHelper.getRepo().getChangelog().parents(revNumber, new int[2], parent1, parent2); + } + return Nodeid.fromBinary(parent2, 0); + } + + @Override + public HgChangeset clone() { + try { + HgChangeset copy = (HgChangeset) super.clone(); + // copy.changeset references this.changeset, doesn't need own copy + return copy; + } catch (CloneNotSupportedException ex) { + throw new InternalError(ex.toString()); + } + } + + private /*synchronized*/ void initFileChanges() { + ArrayList deleted = new ArrayList(); + ArrayList modified = new ArrayList(); + ArrayList added = new ArrayList(); + HgStatusCollector.Record r = new HgStatusCollector.Record(); + statusHelper.change(revNumber, r); + final HgRepository repo = statusHelper.getRepo(); + for (Path s : r.getModified()) { + Nodeid nid = r.nodeidAfterChange(s); + if (nid == null) { + throw new HgBadStateException(); + } + modified.add(new FileRevision(repo, nid, s)); + } + for (Path s : r.getAdded()) { + Nodeid nid = r.nodeidAfterChange(s); + if (nid == null) { + throw new HgBadStateException(); + } + added.add(new FileRevision(repo, nid, s)); + } + for (Path s : r.getRemoved()) { + // with Path from getRemoved, may just copy + deleted.add(s); + } + modified.trimToSize(); + added.trimToSize(); + deleted.trimToSize(); + modifiedFiles = Collections.unmodifiableList(modified); + addedFiles = Collections.unmodifiableList(added); + deletedFiles = Collections.unmodifiableList(deleted); + } +} \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgChangesetHandler.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgChangesetHandler.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,30 @@ +/* + * 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; + +/** + * Callback to process {@link HgChangeset changesets}. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public interface HgChangesetHandler { + /** + * @param changeset not necessarily a distinct instance each time, {@link HgChangeset#clone() clone()} if need a copy. + */ + void next(HgChangeset changeset); +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgCloneCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgCloneCommand.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,333 @@ +/* + * 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 static org.tmatesoft.hg.core.Nodeid.NULL; +import static org.tmatesoft.hg.internal.RequiresFile.*; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.TreeMap; +import java.util.zip.DeflaterOutputStream; + +import org.tmatesoft.hg.internal.ByteArrayDataAccess; +import org.tmatesoft.hg.internal.DataAccess; +import org.tmatesoft.hg.internal.DigestHelper; +import org.tmatesoft.hg.internal.Internals; +import org.tmatesoft.hg.repo.HgBundle; +import org.tmatesoft.hg.repo.HgBundle.GroupElement; +import org.tmatesoft.hg.repo.HgLookup; +import org.tmatesoft.hg.repo.HgRemoteRepository; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.util.PathRewrite; + +/** + * WORK IN PROGRESS, DO NOT USE + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgCloneCommand { + + private File destination; + private HgRemoteRepository srcRepo; + + public HgCloneCommand() { + } + + /** + * @param folder location to become root of the repository (i.e. where .hg folder would reside). Either + * shall not exist or be empty otherwise. + * @return this for convenience + */ + public HgCloneCommand destination(File folder) { + destination = folder; + return this; + } + + public HgCloneCommand source(HgRemoteRepository hgRemote) { + srcRepo = hgRemote; + return this; + } + + public HgRepository execute() throws HgException, CancelledException { + if (destination == null) { + throw new HgBadArgumentException("Destination not set", null); + } + if (srcRepo == null || srcRepo.isInvalid()) { + throw new HgBadArgumentException("Bad source repository", null); + } + if (destination.exists()) { + if (!destination.isDirectory()) { + throw new HgBadArgumentException(String.format("%s is not a directory", destination), null); + } else if (destination.list().length > 0) { + throw new HgBadArgumentException(String.format("% shall be empty", destination), null); + } + } else { + destination.mkdirs(); + } + // if cloning remote repo, which can stream and no revision is specified - + // can use 'stream_out' wireproto + // + // pull all changes from the very beginning + // XXX consult getContext() if by any chance has a bundle ready, if not, then read and register + HgBundle completeChanges = srcRepo.getChanges(Collections.singletonList(NULL)); + WriteDownMate mate = new WriteDownMate(destination); + try { + // instantiate new repo in the destdir + mate.initEmptyRepository(); + // pull changes + completeChanges.inspectAll(mate); + mate.complete(); + } catch (IOException ex) { + throw new HgException(ex); + } finally { + completeChanges.unlink(); + } + return new HgLookup().detect(destination); + } + + + // 1. process changelog, memorize nodeids to index + // 2. process manifest, using map from step 3, collect manifest nodeids + // 3. process every file, using map from 3, and consult set from step 4 to ensure repo is correct + private static class WriteDownMate implements HgBundle.Inspector { + private final File hgDir; + private final PathRewrite storagePathHelper; + private FileOutputStream indexFile; + private String filename; // human-readable name of the file being written, for log/exception purposes + + private final TreeMap changelogIndexes = new TreeMap(); + private boolean collectChangelogIndexes = false; + + private int base = -1; + private long offset = 0; + private DataAccess prevRevContent; + private final DigestHelper dh = new DigestHelper(); + private final ArrayList revisionSequence = new ArrayList(); // last visited nodes first + + private final LinkedList fncacheFiles = new LinkedList(); + private Internals implHelper; + + public WriteDownMate(File destDir) { + hgDir = new File(destDir, ".hg"); + implHelper = new Internals(); + implHelper.setStorageConfig(1, STORE | FNCACHE | DOTENCODE); + storagePathHelper = implHelper.buildDataFilesHelper(); + } + + public void initEmptyRepository() throws IOException { + implHelper.initEmptyRepository(hgDir); + } + + public void complete() throws IOException { + FileOutputStream fncacheFile = new FileOutputStream(new File(hgDir, "store/fncache")); + for (String s : fncacheFiles) { + fncacheFile.write(s.getBytes()); + fncacheFile.write(0x0A); // http://mercurial.selenic.com/wiki/fncacheRepoFormat + } + fncacheFile.close(); + } + + public void changelogStart() { + try { + base = -1; + offset = 0; + revisionSequence.clear(); + indexFile = new FileOutputStream(new File(hgDir, filename = "store/00changelog.i")); + collectChangelogIndexes = true; + } catch (IOException ex) { + throw new HgBadStateException(ex); + } + } + + public void changelogEnd() { + try { + if (prevRevContent != null) { + prevRevContent.done(); + prevRevContent = null; + } + collectChangelogIndexes = false; + indexFile.close(); + indexFile = null; + filename = null; + } catch (IOException ex) { + throw new HgBadStateException(ex); + } + } + + public void manifestStart() { + try { + base = -1; + offset = 0; + revisionSequence.clear(); + indexFile = new FileOutputStream(new File(hgDir, filename = "store/00manifest.i")); + } catch (IOException ex) { + throw new HgBadStateException(ex); + } + } + + public void manifestEnd() { + try { + if (prevRevContent != null) { + prevRevContent.done(); + prevRevContent = null; + } + indexFile.close(); + indexFile = null; + filename = null; + } catch (IOException ex) { + throw new HgBadStateException(ex); + } + } + + public void fileStart(String name) { + try { + base = -1; + offset = 0; + revisionSequence.clear(); + fncacheFiles.add("data/" + name + ".i"); // FIXME this is pure guess, + // need to investigate more how filenames are kept in fncache + File file = new File(hgDir, filename = storagePathHelper.rewrite(name)); + file.getParentFile().mkdirs(); + indexFile = new FileOutputStream(file); + } catch (IOException ex) { + throw new HgBadStateException(ex); + } + } + + public void fileEnd(String name) { + try { + if (prevRevContent != null) { + prevRevContent.done(); + prevRevContent = null; + } + indexFile.close(); + indexFile = null; + filename = null; + } catch (IOException ex) { + throw new HgBadStateException(ex); + } + } + + private int knownRevision(Nodeid p) { + if (NULL.equals(p)) { + return -1; + } else { + for (int i = revisionSequence.size() - 1; i >= 0; i--) { + if (revisionSequence.get(i).equals(p)) { + return i; + } + } + } + throw new HgBadStateException(String.format("Can't find index of %s for file %s", p.shortNotation(), filename)); + } + + public boolean element(GroupElement ge) { + try { + assert indexFile != null; + boolean writeComplete = false; + Nodeid p1 = ge.firstParent(); + Nodeid p2 = ge.secondParent(); + if (NULL.equals(p1) && NULL.equals(p2) /* or forced flag, does REVIDX_PUNCHED_FLAG indicate that? */) { + prevRevContent = new ByteArrayDataAccess(new byte[0]); + writeComplete = true; + } + byte[] content = ge.apply(prevRevContent); + 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)); + } + final int link; + if (collectChangelogIndexes) { + changelogIndexes.put(node, revisionSequence.size()); + link = revisionSequence.size(); + } 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)); + } + link = csRev.intValue(); + } + final int p1Rev = knownRevision(p1), p2Rev = knownRevision(p2); + DataAccess patchContent = ge.rawData(); + writeComplete = writeComplete || patchContent.length() >= (/* 3/4 of actual */content.length - (content.length >>> 2)); + if (writeComplete) { + base = revisionSequence.size(); + } + final byte[] sourceData = writeComplete ? content : patchContent.byteArray(); + final byte[] data; + ByteArrayOutputStream bos = new ByteArrayOutputStream(content.length); + DeflaterOutputStream dos = new DeflaterOutputStream(bos); + dos.write(sourceData); + dos.close(); + final byte[] compressedData = bos.toByteArray(); + dos = null; + bos = null; + final Byte dataPrefix; + if (compressedData.length >= (sourceData.length - (sourceData.length >>> 2))) { + // compression wasn't too effective, + data = sourceData; + dataPrefix = 'u'; + } else { + data = compressedData; + dataPrefix = null; + } + + ByteBuffer header = ByteBuffer.allocate(64 /* REVLOGV1_RECORD_SIZE */); + if (offset == 0) { + final int INLINEDATA = 1 << 16; + header.putInt(1 /* RevlogNG */ | INLINEDATA); + header.putInt(0); + } else { + header.putLong(offset << 16); + } + final int compressedLen = data.length + (dataPrefix == null ? 0 : 1); + header.putInt(compressedLen); + header.putInt(content.length); + header.putInt(base); + header.putInt(link); + header.putInt(p1Rev); + header.putInt(p2Rev); + header.put(node.toByteArray()); + // assume 12 bytes left are zeros + indexFile.write(header.array()); + if (dataPrefix != null) { + indexFile.write(dataPrefix.byteValue()); + } + indexFile.write(data); + // + offset += compressedLen; + revisionSequence.add(node); + prevRevContent.done(); + prevRevContent = new ByteArrayDataAccess(content); + } catch (IOException ex) { + throw new HgBadStateException(ex); + } + return true; + } + } + +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgDataStreamException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgDataStreamException.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,33 @@ +/* + * 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; + +/** + * Any erroneous state with @link {@link HgDataFile} input/output, read/write operations + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@SuppressWarnings("serial") +public class HgDataStreamException extends HgException { + + public HgDataStreamException(String message, Throwable cause) { + super(message, cause); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgDate.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgDate.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,111 @@ +/* + * 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.util.Calendar; +import java.util.Formatter; +import java.util.Locale; +import java.util.TimeZone; + +/** + * Compound object to keep time and time zone of a change. Time zone is not too useful unless you'd like to indicate where + * the change was made (original hg shows date of a change in its original time zone) + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public final class HgDate implements Comparable, Cloneable { + private final long time; + private final TimeZone tzone; + + + /** + * @param millis UTC, milliseconds + * @param timezone zone offset in seconds, UTC - local == timezone. I.e. positive in the Western Hemisphere. + */ + public HgDate(long millis, int timezone) { + time = millis; + // @see http://pydoc.org/2.5.1/time.html time.timezone -- difference in seconds between UTC and local standard time + // UTC - local = timezone. local = UTC - timezone + // In Java, timezone is positive right of Greenwich, UTC+timezone = local + String[] available = TimeZone.getAvailableIDs(-timezone * 1000); + assert available != null && available.length > 0 : String.valueOf(timezone); + // this is sort of hach, I don't know another way how to get + // abbreviated name from zone offset (other than to have own mapping) + // And I can't use any id, because e.g. zone with id "US/Mountain" + // gives incorrect (according to hg cmdline) result, unlike MST or US/Arizona (all ids for zone -0700) + // use 1125044450000L to see the difference + String shortID = TimeZone.getTimeZone(available[0]).getDisplayName(false, TimeZone.SHORT); + // XXX in fact, might need to handle daylight saving time, but not sure how, + // getTimeZone(GMT-timezone*1000).inDaylightTime()? + TimeZone tz = TimeZone.getTimeZone(shortID); + tzone = tz; + } + + public long getRawTime() { + return time; + } + + /** + * @return zone object by reference, do not alter it (make own copy by {@link TimeZone#clone()}, to modify). + */ + public TimeZone getTimeZone() { + return tzone; + } + + @Override + public String toString() { + // format the same way hg does + return toString(Locale.US); + } + + public String toString(Locale l) { + Calendar c = Calendar.getInstance(getTimeZone()); + c.setTimeInMillis(getRawTime()); + Formatter f = new Formatter(new StringBuilder(), l); + f.format("%ta %> 32); + } + + @Override + protected Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException ex) { + throw new InternalError(ex.toString()); + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgException.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,45 @@ +/* + * 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; + +/** + * Root class for all hg4j exceptions. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@SuppressWarnings("serial") +public class HgException extends Exception { + + public HgException(String reason) { + super(reason); + } + + public HgException(String reason, Throwable cause) { + super(reason, cause); + } + + public HgException(Throwable cause) { + super(cause); + } + +// /* XXX CONSIDER capability to pass extra information about errors */ +// public static class Status { +// public Status(String message, Throwable cause, int errorCode, Object extraData) { +// } +// } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgIncomingCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgIncomingCommand.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,199 @@ +/* + * 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.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import org.tmatesoft.hg.internal.RepositoryComparator; +import org.tmatesoft.hg.internal.RepositoryComparator.BranchChain; +import org.tmatesoft.hg.repo.HgBundle; +import org.tmatesoft.hg.repo.HgChangelog; +import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; +import org.tmatesoft.hg.repo.HgRemoteRepository; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.CancelledException; + +/** + * Command to find out changes available in a remote repository, missing locally. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgIncomingCommand { + + private final HgRepository localRepo; + private HgRemoteRepository remoteRepo; + @SuppressWarnings("unused") + private boolean includeSubrepo; + private RepositoryComparator comparator; + private List missingBranches; + private HgChangelog.ParentWalker parentHelper; + private Set branches; + + public HgIncomingCommand(HgRepository hgRepo) { + localRepo = hgRepo; + } + + public HgIncomingCommand against(HgRemoteRepository hgRemote) { + remoteRepo = hgRemote; + comparator = null; + missingBranches = null; + return this; + } + + /** + * 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. + * + * @param branch - branch name, case-sensitive, non-null. + * @return this for convenience + * @throws IllegalArgumentException when branch argument is null + */ + public HgIncomingCommand branch(String branch) { + if (branch == null) { + throw new IllegalArgumentException(); + } + if (branches == null) { + branches = new TreeSet(); + } + branches.add(branch); + return this; + } + + /** + * PLACEHOLDER, NOT IMPLEMENTED YET. + * + * Whether to include sub-repositories when collecting changes, default is true XXX or false? + * @return this for convenience + */ + public HgIncomingCommand subrepo(boolean include) { + includeSubrepo = include; + throw HgRepository.notImplemented(); + } + + /** + * Lightweight check for incoming changes, gives only list of revisions to pull. + * Reported changes are from any branch (limits set by {@link #branch(String)} are not taken into account. + * + * @param context anything hg4j can use to get progress and/or cancel support + * @return list of nodes present at remote and missing locally + * @throws HgException + * @throws CancelledException + */ + public List executeLite(Object context) throws HgException, CancelledException { + LinkedHashSet result = new LinkedHashSet(); + RepositoryComparator repoCompare = getComparator(context); + for (BranchChain bc : getMissingBranches(context)) { + 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; + } + + /** + * Full information about incoming changes + * + * @throws HgException + * @throws CancelledException + */ + public void executeFull(final HgChangesetHandler handler) throws HgException, CancelledException { + if (handler == null) { + throw new IllegalArgumentException("Delegate can't be null"); + } + final List common = getCommon(handler); + HgBundle changegroup = remoteRepo.getChanges(common); + try { + changegroup.changes(localRepo, new HgChangelog.Inspector() { + private int localIndex; + private final HgChangelog.ParentWalker parentHelper; + private final ChangesetTransformer transformer; + + { + transformer = new ChangesetTransformer(localRepo, handler, getParentHelper()); + transformer.limitBranches(branches); + parentHelper = getParentHelper(); + // new revisions, if any, would be added after all existing, and would get numbered started with last+1 + localIndex = localRepo.getChangelog().getRevisionCount(); + } + + 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"); + } + return; + } + transformer.next(localIndex++, nodeid, cset); + } + }); + } catch (IOException ex) { + throw new HgException(ex); + } + } + + private RepositoryComparator getComparator(Object context) throws HgException, CancelledException { + if (remoteRepo == null) { + throw new HgBadArgumentException("Shall specify remote repository to compare against", null); + } + if (comparator == null) { + comparator = new RepositoryComparator(getParentHelper(), remoteRepo); +// comparator.compare(context); // XXX meanwhile we use distinct path to calculate common + } + return comparator; + } + + private HgChangelog.ParentWalker getParentHelper() { + if (parentHelper == null) { + parentHelper = localRepo.getChangelog().new ParentWalker(); + parentHelper.init(); + } + return parentHelper; + } + + private List getMissingBranches(Object context) throws HgException, CancelledException { + if (missingBranches == null) { + missingBranches = getComparator(context).calculateMissingBranches(); + } + return missingBranches; + } + + private List getCommon(Object context) throws HgException, CancelledException { +// return getComparator(context).getCommon(); + final LinkedHashSet common = new LinkedHashSet(); + // XXX common can be obtained from repoCompare, but at the moment it would almost duplicate work of calculateMissingBranches + // once I refactor latter, common shall be taken from repoCompare. + RepositoryComparator repoCompare = getComparator(context); + for (BranchChain bc : getMissingBranches(context)) { + repoCompare.collectKnownRoots(bc, common); + } + return new LinkedList(common); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgLogCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgLogCommand.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,325 @@ +/* + * 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 static org.tmatesoft.hg.repo.HgRepository.TIP; + +import java.io.IOException; +import java.util.Calendar; +import java.util.Collections; +import java.util.ConcurrentModificationException; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; + +import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; +import org.tmatesoft.hg.repo.HgChangelog; +import org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.ByteChannel; +import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.util.Path; + + +/** + * Access to changelog, 'hg log' command counterpart. + * + *
+ * Usage:
+ *   new LogCommand().limit(20).branch("maintenance-2.1").user("me").execute(new MyHandler());
+ * 
+ * Not thread-safe (each thread has to use own {@link HgLogCommand} instance). + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgLogCommand implements HgChangelog.Inspector { + + private final HgRepository repo; + private Set users; + private Set branches; + private int limit = 0, count = 0; + private int startRev = 0, endRev = TIP; + private Calendar date; + private Path file; + private boolean followHistory; // makes sense only when file != null + private ChangesetTransformer csetTransform; + private HgChangelog.ParentWalker parentHelper; + + public HgLogCommand(HgRepository hgRepo) { + repo = hgRepo; + } + + /** + * Limit search to specified user. Multiple user names may be specified. Once set, user names can't be + * cleared, use new command instance in such cases. + * @param user - full or partial name of the user, case-insensitive, non-null. + * @return this instance for convenience + * @throws IllegalArgumentException when argument is null + */ + public HgLogCommand user(String user) { + if (user == null) { + throw new IllegalArgumentException(); + } + if (users == null) { + users = new TreeSet(); + } + users.add(user.toLowerCase()); + return this; + } + + /** + * Limit search to specified branch. Multiple branch specification possible (changeset from any of these + * would be included in result). If unspecified, all branches are considered. There's no way to clean branch selection + * once set, create fresh new command instead. + * @param branch - branch name, case-sensitive, non-null. + * @return this instance for convenience + * @throws IllegalArgumentException when branch argument is null + */ + public HgLogCommand branch(String branch) { + if (branch == null) { + throw new IllegalArgumentException(); + } + if (branches == null) { + branches = new TreeSet(); + } + branches.add(branch); + return this; + } + + // limit search to specific date + // multiple? + public HgLogCommand date(Calendar date) { + this.date = date; + // FIXME implement + // isSet(field) - false => don't use in detection of 'same date' + throw HgRepository.notImplemented(); + } + + /** + * + * @param num - number of changeset to produce. Pass 0 to clear the limit. + * @return this instance for convenience + */ + public HgLogCommand limit(int num) { + limit = num; + return this; + } + + /** + * Limit to specified subset of Changelog, [min(rev1,rev2), max(rev1,rev2)], inclusive. + * Revision may be specified with {@link HgRepository#TIP} + * @param rev1 - local revision number + * @param rev2 - local revision number + * @return this instance for convenience + */ + public HgLogCommand range(int rev1, int rev2) { + if (rev1 != TIP && rev2 != TIP) { + startRev = rev2 < rev1 ? rev2 : rev1; + endRev = startRev == rev2 ? rev1 : rev2; + } else if (rev1 == TIP && rev2 != TIP) { + startRev = rev2; + endRev = rev1; + } else { + startRev = rev1; + endRev = rev2; + } + return this; + } + + /** + * Visit history of a given file only. + * @param file path relative to repository root. Pass null to reset. + * @param followCopyRename true to report changesets of the original file(-s), if copy/rename ever occured to the file. + */ + public HgLogCommand file(Path file, boolean followCopyRename) { + // multiple? Bad idea, would need to include extra method into Handler to tell start of next file + this.file = file; + followHistory = followCopyRename; + return this; + } + + /** + * Handy analog of {@link #file(Path, boolean)} when clients' paths come from filesystem and need conversion to repository's + */ + public HgLogCommand file(String file, boolean followCopyRename) { + return file(Path.create(repo.getToRepoPathHelper().rewrite(file)), followCopyRename); + } + + /** + * Similar to {@link #execute(org.tmatesoft.hg.repo.RawChangeset.Inspector)}, collects and return result as a list. + */ + public List execute() throws HgException { + CollectHandler collector = new CollectHandler(); + execute(collector); + return collector.getChanges(); + } + + /** + * + * @param handler callback to process changesets. + * @throws IllegalArgumentException when inspector argument is null + * @throws ConcurrentModificationException if this log command instance is already running + */ + public void execute(HgChangesetHandler handler) throws HgException { + if (handler == null) { + throw new IllegalArgumentException(); + } + if (csetTransform != null) { + throw new ConcurrentModificationException(); + } + try { + count = 0; + HgChangelog.ParentWalker pw = parentHelper; // leave it uninitialized unless we iterate whole repo + if (file == null) { + pw = getParentHelper(); + } + // 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); + if (file == null) { + repo.getChangelog().range(startRev, endRev, this); + } else { + HgDataFile fileNode = repo.getFileNode(file); + fileNode.history(startRev, endRev, this); + if (fileNode.isCopy()) { + // even if we do not follow history, report file rename + do { + if (handler instanceof FileHistoryHandler) { + FileRevision src = new FileRevision(repo, fileNode.getCopySourceRevision(), fileNode.getCopySourceName()); + FileRevision dst = new FileRevision(repo, fileNode.getRevision(0), fileNode.getPath()); + ((FileHistoryHandler) handler).copy(src, dst); + } + if (limit > 0 && count >= limit) { + // if limit reach, follow is useless. + break; + } + if (followHistory) { + fileNode = repo.getFileNode(fileNode.getCopySourceName()); + fileNode.history(this); + } + } while (followHistory && fileNode.isCopy()); + } + } + } finally { + csetTransform = null; + } + } + + // + + public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { + if (limit > 0 && count >= limit) { + return; + } + if (branches != null && !branches.contains(cset.branch())) { + return; + } + if (users != null) { + String csetUser = cset.user().toLowerCase(); + boolean found = false; + for (String u : users) { + if (csetUser.indexOf(u) != -1) { + found = true; + break; + } + } + if (!found) { + return; + } + } + if (date != null) { + // FIXME + } + count++; + csetTransform.next(revisionNumber, nodeid, cset); + } + + private HgChangelog.ParentWalker getParentHelper() { + if (parentHelper == null) { + parentHelper = repo.getChangelog().new ParentWalker(); + parentHelper.init(); + } + return parentHelper; + } + + + /** + * @deprecated Use {@link HgChangesetHandler} instead. This interface is left temporarily for compatibility. + */ + @Deprecated() + public interface Handler extends HgChangesetHandler { + } + + /** + * 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(FileRevision, FileRevision)} 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(FileRevision, FileRevision)} 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 { + // XXX perhaps, should distinguish copy from rename? And what about merged revisions and following them? + void copy(FileRevision from, FileRevision to); + } + + public static class CollectHandler implements HgChangesetHandler { + private final List result = new LinkedList(); + + public List getChanges() { + return Collections.unmodifiableList(result); + } + + public void next(HgChangeset changeset) { + result.add(changeset.clone()); + } + } + + public static final class FileRevision { + private final HgRepository repo; + private final Nodeid revision; + private final Path path; + + /*package-local*/FileRevision(HgRepository hgRepo, Nodeid rev, 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 HgBadStateException(); + } + repo = hgRepo; + revision = rev; + path = p; + } + + public Path getPath() { + return path; + } + public Nodeid getRevision() { + return revision; + } + public void putContentTo(ByteChannel sink) throws HgDataStreamException, IOException, CancelledException { + HgDataFile fn = repo.getFileNode(path); + int localRevision = fn.getLocalRevision(revision); + fn.contentWithFilters(localRevision, sink); + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgManifestCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgManifestCommand.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,196 @@ +/* + * 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 static org.tmatesoft.hg.repo.HgRepository.*; +import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; +import static org.tmatesoft.hg.repo.HgRepository.TIP; + +import java.util.ConcurrentModificationException; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; + +import org.tmatesoft.hg.core.HgLogCommand.FileRevision; +import org.tmatesoft.hg.repo.HgManifest; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.Path; +import org.tmatesoft.hg.util.PathPool; +import org.tmatesoft.hg.util.PathRewrite; + + +/** + * Gives access to list of files in each revision (Mercurial manifest information), 'hg manifest' counterpart. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgManifestCommand { + + private final HgRepository repo; + private Path.Matcher matcher; + private int startRev = 0, endRev = TIP; + private Handler visitor; + private boolean needDirs = false; + + private final Mediator mediator = new Mediator(); + + public HgManifestCommand(HgRepository hgRepo) { + repo = hgRepo; + } + + /** + * Parameterize command to visit revisions [rev1..rev2]. + * @param rev1 - local revision number to start from. Non-negative. May be {@link HgRepository#TIP} (rev2 argument shall be {@link HgRepository#TIP} as well, then) + * @param rev2 - local revision number to end with, inclusive. Non-negative, greater or equal to rev1. May be {@link HgRepository#TIP}. + * @return this for convenience. + * @throws IllegalArgumentException if revision arguments are incorrect (see above). + */ + public HgManifestCommand range(int rev1, int rev2) { + // XXX if manifest range is different from that of changelog, need conversion utils (external?) + boolean badArgs = rev1 == BAD_REVISION || rev2 == BAD_REVISION || rev1 == WORKING_COPY || rev2 == WORKING_COPY; + badArgs |= rev2 != TIP && rev2 < rev1; // range(3, 1); + badArgs |= rev1 == TIP && rev2 != TIP; // range(TIP, 2), although this may be legitimate when TIP points to 2 + if (badArgs) { + throw new IllegalArgumentException(String.format("Bad range: [%d, %d]", rev1, rev2)); + } + startRev = rev1; + endRev = rev2; + return this; + } + + public HgManifestCommand revision(int rev) { + startRev = endRev = rev; + return this; + } + + public HgManifestCommand dirs(boolean include) { + // XXX whether directories with directories only are include or not + // now lists only directories with files + needDirs = include; + return this; + } + + /** + * Limit manifest walk to a subset of files. + * @param pathMatcher - filter, pass null to clear. + * @return this instance for convenience + */ + public HgManifestCommand match(Path.Matcher pathMatcher) { + matcher = pathMatcher; + return this; + } + + /** + * Runs the command. + * @param handler - callback to get the outcome + * @throws IllegalArgumentException if handler is null + * @throws ConcurrentModificationException if this command is already in use (running) + */ + public void execute(Handler handler) { + if (handler == null) { + throw new IllegalArgumentException(); + } + if (visitor != null) { + throw new ConcurrentModificationException(); + } + try { + visitor = handler; + mediator.start(); + repo.getManifest().walk(startRev, endRev, mediator); + } 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(FileRevision 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.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; + + public void start() { + // Manifest keeps normalized paths + pathPool = new PathPool(new PathRewrite.Empty()); + } + + public void done() { + manifestContent = null; + pathPool = null; + } + + public boolean begin(int revision, Nodeid nid) { + if (needDirs && manifestContent == null) { + manifestContent = new LinkedList(); + } + visitor.begin(manifestNodeid = nid); + return true; + } + public boolean end(int revision) { + if (needDirs) { + LinkedHashMap> breakDown = new LinkedHashMap>(); + for (FileRevision 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); + } + for (Path dir : breakDown.keySet()) { + visitor.dir(dir); + for (FileRevision fr : breakDown.get(dir)) { + visitor.file(fr); + } + } + manifestContent.clear(); + } + visitor.end(manifestNodeid); + manifestNodeid = null; + return true; + } + public boolean next(Nodeid nid, String fname, String flags) { + Path p = pathPool.path(fname); + if (matcher != null && !matcher.accept(p)) { + return true; + } + FileRevision fr = new FileRevision(repo, nid, p); + if (needDirs) { + manifestContent.add(fr); + } else { + visitor.file(fr); + } + return true; + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgOutgoingCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgOutgoingCommand.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,133 @@ +/* + * 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.util.List; +import java.util.Set; +import java.util.TreeSet; + +import org.tmatesoft.hg.internal.RepositoryComparator; +import org.tmatesoft.hg.repo.HgChangelog; +import org.tmatesoft.hg.repo.HgRemoteRepository; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.CancelledException; + +/** + * Command to find out changes made in a local repository and missing at remote repository. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgOutgoingCommand { + + private final HgRepository localRepo; + private HgRemoteRepository remoteRepo; + @SuppressWarnings("unused") + private boolean includeSubrepo; + private RepositoryComparator comparator; + private HgChangelog.ParentWalker parentHelper; + private Set branches; + + public HgOutgoingCommand(HgRepository hgRepo) { + localRepo = hgRepo; + } + + /** + * @param hgRemote remoteRepository to compare against + * @return this for convenience + */ + public HgOutgoingCommand against(HgRemoteRepository hgRemote) { + remoteRepo = hgRemote; + comparator = null; + return this; + } + + /** + * 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. + * + * @param branch - branch name, case-sensitive, non-null. + * @return this for convenience + * @throws IllegalArgumentException when branch argument is null + */ + public HgOutgoingCommand branch(String branch) { + if (branch == null) { + throw new IllegalArgumentException(); + } + if (branches == null) { + branches = new TreeSet(); + } + branches.add(branch); + return this; + } + + /** + * PLACEHOLDER, NOT IMPLEMENTED YET. + * + * @return this for convenience + */ + public HgOutgoingCommand subrepo(boolean include) { + includeSubrepo = include; + throw HgRepository.notImplemented(); + } + + /** + * Lightweight check for outgoing changes. + * Reported changes are from any branch (limits set by {@link #branch(String)} are not taken into account. + * + * @param context + * @return list on local nodes known to be missing at remote server + */ + public List executeLite(Object context) throws HgException, CancelledException { + return getComparator(context).getLocalOnlyRevisions(); + } + + /** + * Complete information about outgoing changes + * + * @param handler delegate to process changes + */ + public void executeFull(final HgChangesetHandler handler) throws HgException, CancelledException { + if (handler == null) { + throw new IllegalArgumentException("Delegate can't be null"); + } + ChangesetTransformer inspector = new ChangesetTransformer(localRepo, handler, getParentHelper()); + inspector.limitBranches(branches); + getComparator(handler).visitLocalOnlyRevisions(inspector); + } + + private RepositoryComparator getComparator(Object context) throws HgException, CancelledException { + if (remoteRepo == null) { + throw new IllegalArgumentException("Shall specify remote repository to compare against"); + } + if (comparator == null) { + comparator = new RepositoryComparator(getParentHelper(), remoteRepo); + comparator.compare(context); + } + return comparator; + } + + private HgChangelog.ParentWalker getParentHelper() { + if (parentHelper == null) { + parentHelper = localRepo.getChangelog().new ParentWalker(); + parentHelper.init(); + } + return parentHelper; + } + +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgRepoFacade.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgRepoFacade.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,113 @@ +/* + * 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.repo.HgRepository; +import org.tmatesoft.hg.repo.HgLookup; + +/** + * Starting point for the library. + *

Sample use: + *

+ *  HgRepoFacade f = new HgRepoFacade();
+ *  f.initFrom(System.getenv("whatever.repo.location"));
+ *  HgStatusCommand statusCmd = f.createStatusCommand();
+ *  HgStatusCommand.Handler handler = ...;
+ *  statusCmd.execute(handler);
+ * 
+ * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgRepoFacade { + private HgRepository repo; + + public HgRepoFacade() { + } + + /** + * @param hgRepo + * @return true on successful initialization + * @throws IllegalArgumentException when argument is null + */ + public boolean init(HgRepository hgRepo) { + if (hgRepo == null) { + throw new IllegalArgumentException(); + } + repo = hgRepo; + return !repo.isInvalid(); + } + + /** + * Tries to find repository starting from the current working directory. + * @return true if found valid repository + * @throws HgException in case of errors during repository initialization + */ + public boolean init() throws HgException { + repo = new HgLookup().detectFromWorkingDir(); + return repo != null && !repo.isInvalid(); + } + + /** + * Looks up Mercurial repository starting from specified location and up to filesystem root. + * + * @param repoLocation path to any folder within structure of a Mercurial repository. + * @return true if found valid repository + * @throws HgException if there are errors accessing specified location + * @throws IllegalArgumentException if argument is null + */ + public boolean initFrom(File repoLocation) throws HgException { + if (repoLocation == null) { + throw new IllegalArgumentException(); + } + repo = new HgLookup().detect(repoLocation); + return repo != null && !repo.isInvalid(); + } + + public HgRepository getRepository() { + if (repo == null) { + throw new IllegalStateException("Call any of #init*() methods first first"); + } + return repo; + } + + public HgLogCommand createLogCommand() { + return new HgLogCommand(repo/*, getCommandContext()*/); + } + + public HgStatusCommand createStatusCommand() { + return new HgStatusCommand(repo/*, getCommandContext()*/); + } + + public HgCatCommand createCatCommand() { + return new HgCatCommand(repo); + } + + public HgManifestCommand createManifestCommand() { + return new HgManifestCommand(repo); + } + + public HgOutgoingCommand createOutgoingCommand() { + return new HgOutgoingCommand(repo); + } + + public HgIncomingCommand createIncomingCommand() { + return new HgIncomingCommand(repo); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgStatus.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgStatus.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,95 @@ +/* + * 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.util.Date; + +import org.tmatesoft.hg.internal.ChangelogHelper; +import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; +import org.tmatesoft.hg.util.Path; + +/** + * Repository file status and extra handy information. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgStatus { + + public enum Kind { + Modified, Added, Removed, Missing, Unknown, Clean, Ignored + // I'd refrain from changing order of these constants, just in case someone (erroneously, of course ;), uses .ordinal() + }; + + private final HgStatus.Kind kind; + private final Path path; + private final Path origin; + private final ChangelogHelper logHelper; + + HgStatus(HgStatus.Kind kind, Path path, ChangelogHelper changelogHelper) { + this(kind, path, null, changelogHelper); + } + + HgStatus(HgStatus.Kind kind, Path path, Path copyOrigin, ChangelogHelper changelogHelper) { + this.kind = kind; + this.path = path; + origin = copyOrigin; + logHelper = changelogHelper; + } + + public HgStatus.Kind getKind() { + return kind; + } + + public Path getPath() { + return path; + } + + public Path getOriginalPath() { + return origin; + } + + public boolean isCopy() { + return origin != null; + } + + /** + * @return null if author for the change can't be deduced (e.g. for clean files it's senseless) + */ + public String getModificationAuthor() { + RawChangeset cset = logHelper.findLatestChangeWith(path); + if (cset == null) { + if (kind == Kind.Modified || kind == Kind.Added || kind == Kind.Removed /*&& RightBoundary is TIP*/) { + // perhaps, also for Kind.Missing? + return logHelper.getNextCommitUsername(); + } + } else { + return cset.user(); + } + return null; + } + + public Date getModificationDate() { + RawChangeset cset = logHelper.findLatestChangeWith(path); + if (cset == null) { + // FIXME check dirstate and/or local file for tstamp + return new Date(); // what's correct + } else { + return cset.date(); + } + } +} \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/HgStatusCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/HgStatusCommand.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,285 @@ +/* + * 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 static org.tmatesoft.hg.core.HgStatus.Kind.*; +import static org.tmatesoft.hg.repo.HgInternals.wrongLocalRevision; +import static org.tmatesoft.hg.repo.HgRepository.*; + +import java.util.ConcurrentModificationException; + +import org.tmatesoft.hg.internal.ChangelogHelper; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgStatusCollector; +import org.tmatesoft.hg.repo.HgStatusInspector; +import org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector; +import org.tmatesoft.hg.util.Path; +import org.tmatesoft.hg.util.Path.Matcher; + +/** + * Command to obtain file status information, 'hg status' counterpart. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgStatusCommand { + private final HgRepository repo; + + private int startRevision = TIP; + private int endRevision = WORKING_COPY; + + private final Mediator mediator = new Mediator(); + + public HgStatusCommand(HgRepository hgRepo) { + repo = hgRepo; + defaults(); + } + + public HgStatusCommand defaults() { + final Mediator m = mediator; + m.needModified = m.needAdded = m.needRemoved = m.needUnknown = m.needMissing = true; + m.needCopies = m.needClean = m.needIgnored = false; + return this; + } + public HgStatusCommand all() { + final Mediator m = mediator; + m.needModified = m.needAdded = m.needRemoved = m.needUnknown = m.needMissing = true; + m.needCopies = m.needClean = m.needIgnored = true; + return this; + } + + + public HgStatusCommand modified(boolean include) { + mediator.needModified = include; + return this; + } + public HgStatusCommand added(boolean include) { + mediator.needAdded = include; + return this; + } + public HgStatusCommand removed(boolean include) { + mediator.needRemoved = include; + return this; + } + public HgStatusCommand deleted(boolean include) { + mediator.needMissing = include; + return this; + } + public HgStatusCommand unknown(boolean include) { + mediator.needUnknown = include; + return this; + } + public HgStatusCommand clean(boolean include) { + mediator.needClean = include; + return this; + } + public HgStatusCommand ignored(boolean include) { + mediator.needIgnored = include; + return this; + } + + /** + * If set, either base:revision or base:workingdir + * to unset, pass {@link HgRepository#TIP} or {@link HgRepository#BAD_REVISION} + * @param revision - local revision number to base status from + * @return this for convenience + * @throws IllegalArgumentException when revision is negative or {@link HgRepository#WORKING_COPY} + */ + public HgStatusCommand base(int revision) { + if (revision == WORKING_COPY || wrongLocalRevision(revision)) { + throw new IllegalArgumentException(String.valueOf(revision)); + } + if (revision == BAD_REVISION) { + revision = TIP; + } + startRevision = revision; + return this; + } + + /** + * Revision without base == --change + * Pass {@link HgRepository#WORKING_COPY} or {@link HgRepository#BAD_REVISION} to reset + * @param revision - non-negative local revision number, or any of {@link HgRepository#BAD_REVISION}, {@link HgRepository#WORKING_COPY} or {@link HgRepository#TIP} + * @return this for convenience + * @throws IllegalArgumentException if local revision number doesn't specify legitimate revision. + */ + public HgStatusCommand revision(int revision) { + if (revision == BAD_REVISION) { + revision = WORKING_COPY; + } + if (wrongLocalRevision(revision)) { + throw new IllegalArgumentException(String.valueOf(revision)); + } + endRevision = revision; + return this; + } + + /** + * Shorthand for {@link #base(int) cmd.base(BAD_REVISION)}{@link #change(int) .revision(revision)} + * + * @param revision compare given revision against its parent + * @return this for convenience + */ + public HgStatusCommand change(int revision) { + base(BAD_REVISION); + return revision(revision); + } + + /** + * Limit status operation to certain sub-tree. + * + * @param pathMatcher - matcher to use, pass null/ to reset + * @return this for convenience + */ + public HgStatusCommand match(Path.Matcher pathMatcher) { + mediator.matcher = pathMatcher; + return this; + } + + public HgStatusCommand subrepo(boolean visit) { + throw HgRepository.notImplemented(); + } + + /** + * Perform status operation according to parameters set. + * + * @param handler callback to get status information + * @throws IllegalArgumentException if handler is null + * @throws ConcurrentModificationException if this command already runs (i.e. being used from another thread) + */ + public void execute(Handler statusHandler) { + if (statusHandler == null) { + throw new IllegalArgumentException(); + } + if (mediator.busy()) { + throw new ConcurrentModificationException(); + } + HgStatusCollector sc = new HgStatusCollector(repo); // TODO from CommandContext +// PathPool pathHelper = new PathPool(repo.getPathHelper()); // TODO from CommandContext + 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)); + if (endRevision == WORKING_COPY) { + HgWorkingCopyStatusCollector wcsc = new HgWorkingCopyStatusCollector(repo); + wcsc.setBaseRevisionCollector(sc); + wcsc.walk(startRevision, mediator); + } else { + if (startRevision == TIP) { + sc.change(endRevision, mediator); + } else { + sc.walk(startRevision, endRevision, mediator); + } + } + } finally { + mediator.done(); + } + } + + public interface Handler { + void handleStatus(HgStatus s); + } + + private class Mediator implements HgStatusInspector { + boolean needModified; + boolean needAdded; + boolean needRemoved; + boolean needUnknown; + boolean needMissing; + boolean needClean; + boolean needIgnored; + boolean needCopies; + Matcher matcher; + Handler handler; + private ChangelogHelper logHelper; + + Mediator() { + } + + public void start(Handler h, ChangelogHelper changelogHelper) { + handler = h; + logHelper = changelogHelper; + } + + public void done() { + handler = null; + logHelper = null; + } + + public boolean busy() { + return handler != null; + } + + public void modified(Path fname) { + if (needModified) { + if (matcher == null || matcher.accept(fname)) { + handler.handleStatus(new HgStatus(Modified, fname, logHelper)); + } + } + } + public void added(Path fname) { + if (needAdded) { + if (matcher == null || matcher.accept(fname)) { + handler.handleStatus(new HgStatus(Added, fname, logHelper)); + } + } + } + public void removed(Path fname) { + if (needRemoved) { + if (matcher == null || matcher.accept(fname)) { + handler.handleStatus(new HgStatus(Removed, fname, logHelper)); + } + } + } + public void copied(Path fnameOrigin, Path fnameAdded) { + if (needCopies) { + if (matcher == null || matcher.accept(fnameAdded)) { + // 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)); + } + } + } + public void missing(Path fname) { + if (needMissing) { + if (matcher == null || matcher.accept(fname)) { + handler.handleStatus(new HgStatus(Missing, fname, logHelper)); + } + } + } + public void unknown(Path fname) { + if (needUnknown) { + if (matcher == null || matcher.accept(fname)) { + handler.handleStatus(new HgStatus(Unknown, fname, logHelper)); + } + } + } + public void clean(Path fname) { + if (needClean) { + if (matcher == null || matcher.accept(fname)) { + handler.handleStatus(new HgStatus(Clean, fname, logHelper)); + } + } + } + public void ignored(Path fname) { + if (needIgnored) { + if (matcher == null || matcher.accept(fname)) { + handler.handleStatus(new HgStatus(Ignored, fname, logHelper)); + } + } + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/Nodeid.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/Nodeid.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2010-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 static org.tmatesoft.hg.internal.DigestHelper.toHexString; + +import java.util.Arrays; + + + +/** + * A 20-bytes (40 characters) long hash value to identify a revision. + * @see http://mercurial.selenic.com/wiki/Nodeid + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + * + */ +public final class Nodeid implements Comparable { + + /** + * nullid, empty root revision. + */ + public static final Nodeid NULL = new Nodeid(new byte[20], false); + + private final byte[] binaryData; + + /** + * @param binaryRepresentation - array of exactly 20 bytes + * @param shallClone - true if array is subject to future modification and shall be copied, not referenced + * @throws IllegalArgumentException if supplied binary representation doesn't correspond to 20 bytes of sha1 digest + */ + public Nodeid(byte[] binaryRepresentation, boolean shallClone) { + // 5 int fields => 32 bytes + // byte[20] => 48 bytes (16 bytes is Nodeid with one field, 32 bytes for byte[20] + if (binaryRepresentation == null || binaryRepresentation.length != 20) { + throw new IllegalArgumentException(); + } + /* + * byte[].clone() is not reflected when ran with -agentlib:hprof=heap=sites + * thus not to get puzzled why there are N Nodeids and much less byte[] instances, + * may use following code to see N byte[] as well. + * + if (shallClone) { + binaryData = new byte[20]; + System.arraycopy(binaryRepresentation, 0, binaryData, 0, 20); + } else { + binaryData = binaryRepresentation; + } + */ + binaryData = shallClone ? binaryRepresentation.clone() : binaryRepresentation; + } + + @Override + public int hashCode() { + // digest (part thereof) seems to be nice candidate for the hashCode + byte[] b = binaryData; + return b[0] << 24 | (b[1] & 0xFF) << 16 | (b[2] & 0xFF) << 8 | (b[3] & 0xFF); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Nodeid) { + return this == o || equalsTo(((Nodeid) o).binaryData); + } + return false; + } + + public boolean equalsTo(byte[] buf) { + return Arrays.equals(this.binaryData, buf); + } + + public int compareTo(Nodeid o) { + if (this == o) { + return 0; + } + for (int i = 0; i < 20; i++) { + if (binaryData[i] != o.binaryData[i]) { + return binaryData[i] < o.binaryData[i] ? -1 : 1; + } + } + return 0; + } + + @Override + public String toString() { + // XXX may want to output just single 0 for the NULL id? + return toHexString(binaryData, 0, binaryData.length); + } + + public String shortNotation() { + return toHexString(binaryData, 0, 6); + } + + public boolean isNull() { + if (this == NULL) { + return true; + } + for (int i = 0; i < 20; i++) { + if (this.binaryData[i] != 0) { + return false; + } + } + return true; + } + + // copy + public byte[] toByteArray() { + return binaryData.clone(); + } + + /** + * Factory for {@link Nodeid Nodeids}. + * Primary difference with cons is handling of NULL id (this method returns constant) and control over array + * duplication - this method always makes a copy of an array passed + * @param binaryRepresentation - byte array of a length at least offset + 20 + * @param offset - index in the array to start from + * @throws IllegalArgumentException when arguments don't select 20 bytes + */ + public static Nodeid fromBinary(byte[] binaryRepresentation, int offset) { + if (binaryRepresentation == null || binaryRepresentation.length - offset < 20) { + throw new IllegalArgumentException(); + } + int i = 0; + while (i < 20 && binaryRepresentation[offset+i] == 0) i++; + if (i == 20) { + return NULL; + } + if (offset == 0 && binaryRepresentation.length == 20) { + return new Nodeid(binaryRepresentation, true); + } + byte[] b = new byte[20]; // create new instance if no other reasonable guesses possible + System.arraycopy(binaryRepresentation, offset, b, 0, 20); + return new Nodeid(b, false); + } + + /** + * Parse encoded representation. + * + * @param asciiRepresentation - encoded form of the Nodeid. + * @return object representation + * @throws IllegalArgumentException when argument doesn't match encoded form of 20-bytes sha1 digest. + */ + public static Nodeid fromAscii(String asciiRepresentation) { + if (asciiRepresentation.length() != 40) { + throw new IllegalArgumentException(); + } + // XXX is better impl for String possible? + return fromAscii(asciiRepresentation.getBytes(), 0, 40); + } + + /** + * Parse encoded representation. Similar to {@link #fromAscii(String)}. + */ + public static Nodeid fromAscii(byte[] asciiRepresentation, int offset, int length) { + if (length != 40) { + throw new IllegalArgumentException(); + } + byte[] data = new byte[20]; + boolean zeroBytes = true; + for (int i = 0, j = offset; i < data.length; i++) { + int hiNibble = Character.digit(asciiRepresentation[j++], 16); + int lowNibble = Character.digit(asciiRepresentation[j++], 16); + byte b = (byte) (((hiNibble << 4) | lowNibble) & 0xFF); + data[i] = b; + zeroBytes = zeroBytes && b == 0; + } + if (zeroBytes) { + return NULL; + } + return new Nodeid(data, false); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/core/package.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/core/package.html Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,5 @@ + + +Hi-level API + + \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/ByteArrayChannel.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/ByteArrayChannel.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,91 @@ +/* + * 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 java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; + +import org.tmatesoft.hg.util.ByteChannel; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class ByteArrayChannel implements ByteChannel { + private final List buffers; + private ByteBuffer target; + private byte[] result; + + public ByteArrayChannel() { + this(-1); + } + + public ByteArrayChannel(int size) { + if (size == -1) { + buffers = new LinkedList(); + } else { + if (size < 0) { + throw new IllegalArgumentException(String.valueOf(size)); + } + buffers = null; + target = ByteBuffer.allocate(size); + } + } + + // TODO document what happens on write after toArray() in each case + public int write(ByteBuffer buffer) { + int rv = buffer.remaining(); + if (buffers == null) { + target.put(buffer); + } else { + ByteBuffer copy = ByteBuffer.allocate(rv); + copy.put(buffer); + buffers.add(copy); + } + return rv; + } + + public byte[] toArray() { + if (result != null) { + return result; + } + if (buffers == null) { + assert target.hasArray(); + // int total = target.position(); + // System.arraycopy(target.array(), new byte[total]); + // I don't want to duplicate byte[] for now + // although correct way of doing things is to make a copy and discard target + return target.array(); + } else { + int total = 0; + for (ByteBuffer bb : buffers) { + bb.flip(); + total += bb.limit(); + } + result = new byte[total]; + int off = 0; + for (ByteBuffer bb : buffers) { + bb.get(result, off, bb.limit()); + off += bb.limit(); + } + buffers.clear(); + return result; + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/ByteArrayDataAccess.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/ByteArrayDataAccess.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,90 @@ +/* + * 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 java.io.IOException; + + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class ByteArrayDataAccess extends DataAccess { + + private final byte[] data; + private final int offset; + private final int length; + private int pos; + + public ByteArrayDataAccess(byte[] data) { + this(data, 0, data.length); + } + + public ByteArrayDataAccess(byte[] data, int offset, int length) { + this.data = data; + this.offset = offset; + this.length = length; + pos = 0; + } + + @Override + public byte readByte() throws IOException { + if (pos >= length) { + throw new IOException(); + } + return data[offset + pos++]; + } + @Override + public void readBytes(byte[] buf, int off, int len) throws IOException { + if (len > (this.length - pos)) { + throw new IOException(); + } + System.arraycopy(data, pos, buf, off, len); + pos += len; + } + + @Override + public ByteArrayDataAccess reset() { + pos = 0; + return this; + } + @Override + public int length() { + return length; + } + @Override + public void seek(int offset) { + pos = (int) offset; + } + @Override + public void skip(int bytes) throws IOException { + seek(pos + bytes); + } + @Override + public boolean isEmpty() { + return pos >= length; + } + + // + + // when byte[] needed from DA, we may save few cycles and some memory giving this (otherwise unsafe) access to underlying data + @Override + public byte[] byteArray() { + return data; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/ChangelogHelper.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/ChangelogHelper.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,83 @@ +/* + * 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 java.util.TreeMap; + +import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; +import org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgInternals; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.Path; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class ChangelogHelper { + private final int leftBoundary; + private final HgRepository repo; + private final TreeMap cache = new TreeMap(); + private String nextCommitAuthor; + + /** + * @param hgRepo + * @param leftBoundaryRevision walker never visits revisions with local numbers less than specified, + * IOW only revisions [leftBoundaryRevision..TIP] are considered. + */ + public ChangelogHelper(HgRepository hgRepo, int leftBoundaryRevision) { + repo = hgRepo; + leftBoundary = leftBoundaryRevision; + } + + /** + * @return the repo + */ + public HgRepository getRepo() { + return repo; + } + + /** + * Walks changelog in reverse order + * @param file + * @return changeset where specified file is mentioned among affected files, or + * null if none found up to leftBoundary + */ + public RawChangeset findLatestChangeWith(Path file) { + HgDataFile df = repo.getFileNode(file); + int changelogRev = df.getChangesetLocalRevision(HgRepository.TIP); + if (changelogRev >= leftBoundary) { + // the method is likely to be invoked for different files, + // while changesets might be the same. Cache 'em not to read too much. + RawChangeset cs = cache.get(changelogRev); + if (cs == null) { + cs = repo.getChangelog().range(changelogRev, changelogRev).get(0); + cache.put(changelogRev, cs); + } + return cs; + } + return null; + } + + public String getNextCommitUsername() { + if (nextCommitAuthor == null) { + nextCommitAuthor = new HgInternals(repo).getNextCommitUsername(); + } + return nextCommitAuthor; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/ConfigFile.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/ConfigFile.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,151 @@ +/* + * 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 java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class ConfigFile { + + private List sections; + private List> content; + + ConfigFile() { + } + + public void addLocation(File path) { + read(path); + } + + public boolean hasSection(String sectionName) { + return sections == null ? false : sections.indexOf(sectionName) == -1; + } + + // XXX perhaps, should be moved to subclass HgRepoConfig, as it is not common operation for any config file + public boolean hasEnabledExtension(String extensionName) { + int x = sections != null ? sections.indexOf("extensions") : -1; + if (x == -1) { + return false; + } + String value = content.get(x).get(extensionName); + return value != null && !"!".equals(value); + } + + public List getSectionNames() { + return sections == null ? Collections.emptyList() : Collections.unmodifiableList(sections); + } + + public Map getSection(String sectionName) { + if (sections == null) { + return Collections.emptyMap(); + } + int x = sections.indexOf(sectionName); + if (x == -1) { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(content.get(x)); + } + + public boolean getBoolean(String sectionName, String key, boolean defaultValue) { + String value = getSection(sectionName).get(key); + if (value == null) { + return defaultValue; + } + for (String s : new String[] { "true", "yes", "on", "1" }) { + if (s.equalsIgnoreCase(value)) { + return true; + } + } + return false; + } + + public String getString(String sectionName, String key, String defaultValue) { + String value = getSection(sectionName).get(key); + return value == null ? defaultValue : value; + } + + // TODO handle %include and %unset directives + // TODO "" and lists + private void read(File f) { + if (f == null || !f.canRead()) { + return; + } + if (sections == null) { + sections = new ArrayList(); + content = new ArrayList>(); + } + try { + BufferedReader br = new BufferedReader(new FileReader(f)); + String line; + String sectionName = ""; + Map section = new LinkedHashMap(); + while ((line = br.readLine()) != null) { + line = line.trim(); + int x; + if ((x = line.indexOf('#')) != -1) { + // do not keep comments in memory, get new, shorter string + line = new String(line.substring(0, x).trim()); + } + if (line.length() <= 2) { // a=b or [a] are at least of length 3 + continue; + } + if (line.charAt(0) == '[' && line.charAt(line.length() - 1) == ']') { + sectionName = line.substring(1, line.length() - 1); + if (sections.indexOf(sectionName) == -1) { + sections.add(sectionName); + content.add(section = new LinkedHashMap()); + } else { + section = null; // drop cached value + } + } else if ((x = line.indexOf('=')) != -1) { + // share char[] of the original string + String key = line.substring(0, x).trim(); + String value = line.substring(x+1).trim(); + if (section == null) { + int i = sections.indexOf(sectionName); + assert i >= 0; + section = content.get(i); + } + if (sectionName.length() == 0) { + // add fake section only if there are any values + sections.add(sectionName); + content.add(section); + } + section.put(key, value); + } + } + br.close(); + } catch (IOException ex) { + ex.printStackTrace(); // XXX shall outer world care? + } + ((ArrayList) sections).trimToSize(); + ((ArrayList) content).trimToSize(); + assert sections.size() == content.size(); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/DataAccess.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/DataAccess.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2010-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 java.io.IOException; +import java.nio.ByteBuffer; + +/** + * relevant parts of DataInput, non-stream nature (seek operation), explicit check for end of data. + * convenient skip (+/- bytes) + * Primary goal - effective file read, so that clients don't need to care whether to call few + * distinct getInt() or readBytes(totalForFewInts) and parse themselves instead in an attempt to optimize. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class DataAccess { + public boolean isEmpty() { + return true; + } + public int length() { + return 0; + } + /** + * get this instance into initial state + * @throws IOException + * @return this for convenience + */ + public DataAccess reset() throws IOException { + // nop, empty instance is always in the initial state + return this; + } + // absolute positioning + public void seek(int offset) throws IOException { + throw new UnsupportedOperationException(); + } + // relative positioning + public void skip(int bytes) throws IOException { + throw new UnsupportedOperationException(); + } + // shall be called once this object no longer needed + public void done() { + // no-op in this empty implementation + } + public int readInt() throws IOException { + byte[] b = new byte[4]; + readBytes(b, 0, 4); + return b[0] << 24 | (b[1] & 0xFF) << 16 | (b[2] & 0xFF) << 8 | (b[3] & 0xFF); + } + public long readLong() throws IOException { + byte[] b = new byte[8]; + readBytes(b, 0, 8); + int i1 = b[0] << 24 | (b[1] & 0xFF) << 16 | (b[2] & 0xFF) << 8 | (b[3] & 0xFF); + int i2 = b[4] << 24 | (b[5] & 0xFF) << 16 | (b[6] & 0xFF) << 8 | (b[7] & 0xFF); + return ((long) i1) << 32 | ((long) i2 & 0xFFFFFFFFl); + } + public void readBytes(byte[] buf, int offset, int length) throws IOException { + throw new UnsupportedOperationException(); + } + // 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) + public void readBytes(ByteBuffer buf) throws IOException { +// int toRead = Math.min(buf.remaining(), (int) length()); +// if (buf.hasArray()) { +// readBytes(buf.array(), buf.arrayOffset(), toRead); +// } else { +// byte[] bb = new byte[toRead]; +// readBytes(bb, 0, bb.length); +// buf.put(bb); +// } + // FIXME optimize to read as much as possible at once + while (!isEmpty() && buf.hasRemaining()) { + buf.put(readByte()); + } + } + public byte readByte() throws IOException { + throw new UnsupportedOperationException(); + } + + // 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 + public byte[] byteArray() throws IOException { + reset(); + byte[] rv = new byte[length()]; + readBytes(rv, 0, rv.length); + return rv; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/DataAccessProvider.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/DataAccessProvider.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,307 @@ +/* + * Copyright (c) 2010-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 java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; + +import org.tmatesoft.hg.core.HgBadStateException; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class DataAccessProvider { + + private final int mapioMagicBoundary; + private final int bufferSize; + + public DataAccessProvider() { + this(100 * 1024, 8 * 1024); + } + + public DataAccessProvider(int mapioBoundary, int regularBufferSize) { + mapioMagicBoundary = mapioBoundary; + bufferSize = regularBufferSize; + } + + public DataAccess create(File f) { + if (!f.exists()) { + return new DataAccess(); + } + 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"); + } + if (flen > mapioMagicBoundary) { + // TESTS: bufLen of 1024 was used to test MemMapFileAccess + return new MemoryMapFileAccess(fc, flen, mapioMagicBoundary); + } 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; + // 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); + } + } catch (IOException ex) { + // unlikely to happen, we've made sure file exists. + ex.printStackTrace(); // FIXME log error + } + 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 final int memBufferSize; + private MappedByteBuffer buffer; + + public MemoryMapFileAccess(FileChannel fc, int channelSize, int /*long?*/ bufferSize) { + fileChannel = fc; + size = channelSize; + memBufferSize = bufferSize; + } + + @Override + public boolean isEmpty() { + return position + (buffer == null ? 0 : buffer.position()) >= size; + } + + @Override + public int length() { + return size; + } + + @Override + public DataAccess reset() throws IOException { + seek(0); + return this; + } + + @Override + public void seek(int 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)); + } else { + position = offset; + buffer = null; + } + } + + @Override + public void skip(int bytes) throws IOException { + assert bytes >= 0; + if (buffer == null) { + position += bytes; + return; + } + if (buffer.remaining() > bytes) { + buffer.position(buffer.position() + bytes); + } else { + position += buffer.position() + bytes; + buffer = null; + } + } + + private void fill() throws IOException { + if (buffer != null) { + position += buffer.position(); + } + long left = size - position; + buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, position, left < memBufferSize ? left : memBufferSize); + } + + @Override + public void readBytes(byte[] buf, int offset, int length) throws IOException { + if (buffer == null || !buffer.hasRemaining()) { + fill(); + } + // XXX in fact, we may try to create a MappedByteBuffer of exactly length size here, and read right away + while (length > 0) { + int tail = buffer.remaining(); + if (tail == 0) { + throw new IOException(); + } + if (tail >= length) { + buffer.get(buf, offset, length); + } else { + buffer.get(buf, offset, tail); + fill(); + } + offset += tail; + length -= tail; + } + } + + @Override + public byte readByte() throws IOException { + if (buffer == null || !buffer.hasRemaining()) { + fill(); + } + if (buffer.hasRemaining()) { + return buffer.get(); + } + throw new IOException(); + } + + @Override + public void done() { + buffer = null; + if (fileChannel != null) { + try { + fileChannel.close(); + } catch (IOException ex) { + ex.printStackTrace(); // log debug + } + fileChannel = null; + } + } + } + + // (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. + + public FileAccess(FileChannel fc, int channelSize, int bufferSizeHint, boolean useDirect) { + fileChannel = fc; + size = channelSize; + final int capacity = size < bufferSizeHint ? size : bufferSizeHint; + buffer = useDirect ? ByteBuffer.allocateDirect(capacity) : ByteBuffer.allocate(capacity); + buffer.flip(); // or .limit(0) to indicate it's empty + } + + @Override + public boolean isEmpty() { + return bufferStartInFile + buffer.position() >= size; + } + + @Override + public int length() { + return size; + } + + @Override + public DataAccess reset() throws IOException { + seek(0); + return this; + } + + @Override + public void seek(int offset) throws IOException { + if (offset > size) { + throw new IllegalArgumentException(); + } + if (offset < bufferStartInFile + buffer.limit() && offset >= bufferStartInFile) { + buffer.position((int) (offset - bufferStartInFile)); + } else { + // out of current buffer, invalidate it (force re-read) + // XXX or ever re-read it right away? + bufferStartInFile = offset; + buffer.clear(); + buffer.limit(0); // or .flip() to indicate we switch to reading + fileChannel.position(offset); + } + } + + @Override + public void skip(int bytes) throws IOException { + final int newPos = buffer.position() + bytes; + if (newPos >= 0 && newPos < buffer.limit()) { + // no need to move file pointer, just rewind/seek buffer + buffer.position(newPos); + } else { + // + seek(bufferStartInFile + newPos); + } + } + + private boolean fill() throws IOException { + if (!buffer.hasRemaining()) { + bufferStartInFile += buffer.limit(); + buffer.clear(); + if (bufferStartInFile < size) { // just in case there'd be any exception on EOF, not -1 + fileChannel.read(buffer); + // may return -1 when EOF, but empty will reflect this, hence no explicit support here + } + buffer.flip(); + } + return buffer.hasRemaining(); + } + + @Override + public void readBytes(byte[] buf, int offset, int length) throws IOException { + if (!buffer.hasRemaining()) { + fill(); + } + while (length > 0) { + int tail = buffer.remaining(); + if (tail == 0) { + throw new IOException(); // shall not happen provided stream contains expected data and no attempts to read past isEmpty() == true are made. + } + if (tail >= length) { + buffer.get(buf, offset, length); + } else { + buffer.get(buf, offset, tail); + fill(); + } + offset += tail; + length -= tail; + } + } + + @Override + public byte readByte() throws IOException { + if (buffer.hasRemaining()) { + return buffer.get(); + } + if (fill()) { + return buffer.get(); + } + throw new IOException(); + } + + @Override + public void done() { + if (buffer != null) { + buffer = null; + } + if (fileChannel != null) { + try { + fileChannel.close(); + } catch (IOException ex) { + ex.printStackTrace(); // log debug + } + fileChannel = null; + } + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/DigestHelper.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/DigestHelper.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2010-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 java.io.IOException; +import java.io.InputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import org.tmatesoft.hg.core.Nodeid; + + +/** + *
+ * DigestHelper dh;
+ * dh.sha1(...).asHexString();
+ *  or 
+ * dh = dh.sha1(...);
+ * nodeid.equalsTo(dh.asBinary());
+ * 
+ * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class DigestHelper { + private MessageDigest sha1; + private byte[] digest; + + public DigestHelper() { + } + + private MessageDigest getSHA1() { + if (sha1 == null) { + try { + sha1 = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException ex) { + // could hardly happen, JDK from Sun always has sha1. + ex.printStackTrace(); // FIXME log error + } + } + return sha1; + } + + + public DigestHelper sha1(Nodeid nodeid1, Nodeid nodeid2, byte[] data) { + return sha1(nodeid1.toByteArray(), nodeid2.toByteArray(), data); + } + + // sha1_digest(min(p1,p2) ++ max(p1,p2) ++ final_text) + public DigestHelper sha1(byte[] nodeidParent1, byte[] nodeidParent2, byte[] data) { + MessageDigest alg = getSHA1(); + if ((nodeidParent1[0] & 0x00FF) < (nodeidParent2[0] & 0x00FF)) { + alg.update(nodeidParent1); + alg.update(nodeidParent2); + } else { + alg.update(nodeidParent2); + alg.update(nodeidParent1); + } + digest = alg.digest(data); + assert digest.length == 20; + return this; + } + + public String asHexString() { + if (digest == null) { + throw new IllegalStateException("Shall init with sha1() call first"); + } + return toHexString(digest, 0, digest.length); + } + + // by reference, be careful not to modify (or #clone() if needed) + public byte[] asBinary() { + if (digest == null) { + throw new IllegalStateException("Shall init with sha1() call first"); + } + return digest; + } + + // XXX perhaps, digest functions should throw an exception, as it's caller responsibility to deal with eof, etc + public DigestHelper sha1(InputStream is /*ByteBuffer*/) throws IOException { + MessageDigest alg = getSHA1(); + byte[] buf = new byte[1024]; + int c; + while ((c = is.read(buf)) != -1) { + alg.update(buf, 0, c); + } + digest = alg.digest(); + return this; + } + + public DigestHelper sha1(CharSequence... seq) { + MessageDigest alg = getSHA1(); + for (CharSequence s : seq) { + byte[] b = s.toString().getBytes(); + alg.update(b); + } + digest = alg.digest(); + return this; + } + + public static String toHexString(byte[] data, final int offset, final int count) { + char[] result = new char[count << 1]; + final String hexDigits = "0123456789abcdef"; + final int end = offset+count; + for (int i = offset, j = 0; i < end; i++) { + result[j++] = hexDigits.charAt((data[i] >>> 4) & 0x0F); + result[j++] = hexDigits.charAt(data[i] & 0x0F); + } + return new String(result); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/Filter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/Filter.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,57 @@ +/* + * 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 java.nio.ByteBuffer; + +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.Path; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public interface Filter { + + // returns a buffer ready to be read. may return original buffer. + // original buffer may not be fully consumed, #compact() might be operation to perform + ByteBuffer filter(ByteBuffer src); + + interface Factory { + void initialize(HgRepository hgRepo, ConfigFile cfg); + // may return null if for a given path and/or options this filter doesn't make any sense + Filter create(Path path, Options opts); + } + + enum Direction { + FromRepo, ToRepo + } + + public class Options { + + private final Direction direction; + public Options(Direction dir) { + direction = dir; + } + + Direction getDirection() { + return direction; + } + + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/FilterByteChannel.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/FilterByteChannel.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,66 @@ +/* + * 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 java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collection; + +import org.tmatesoft.hg.util.Adaptable; +import org.tmatesoft.hg.util.ByteChannel; +import org.tmatesoft.hg.util.CancelledException; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class FilterByteChannel implements ByteChannel, Adaptable { + private final Filter[] filters; + private final ByteChannel delegate; + + public FilterByteChannel(ByteChannel delegateChannel, Collection filtersToApply) { + if (delegateChannel == null || filtersToApply == null) { + throw new IllegalArgumentException(); + } + delegate = delegateChannel; + filters = filtersToApply.toArray(new Filter[filtersToApply.size()]); + } + + public int write(ByteBuffer buffer) throws IOException, CancelledException { + final int srcPos = buffer.position(); + ByteBuffer processed = buffer; + for (Filter f : filters) { + // each next filter consumes not more than previous + // hence total consumed equals position shift in the original buffer + processed = f.filter(processed); + } + delegate.write(processed); + return buffer.position() - srcPos; // consumed as much from original buffer + } + + // adapters or implemented interfaces of the original class shall not be obfuscated by filter + public T getAdapter(Class adapterClass) { + if (delegate instanceof Adaptable) { + return ((Adaptable) delegate).getAdapter(adapterClass); + } + if (adapterClass != null && adapterClass.isInstance(delegate)) { + return adapterClass.cast(delegate); + } + return null; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/FilterDataAccess.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/FilterDataAccess.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,116 @@ +/* + * 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 java.io.IOException; + + +/** + * XXX Perhaps, DataAccessSlice? Unlike FilterInputStream, we limit amount of data read from DataAccess being filtered. + * + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class FilterDataAccess extends DataAccess { + private final DataAccess dataAccess; + private final int offset; + private final int length; + private int count; + + public FilterDataAccess(DataAccess dataAccess, int offset, int length) { + this.dataAccess = dataAccess; + this.offset = offset; + this.length = length; + count = length; + } + + protected int available() { + return count; + } + + @Override + public FilterDataAccess reset() throws IOException { + count = length; + return this; + } + + @Override + public boolean isEmpty() { + return count <= 0; + } + + @Override + public int length() { + return length; + } + + @Override + public void seek(int localOffset) throws IOException { + if (localOffset < 0 || localOffset > length) { + throw new IllegalArgumentException(); + } + dataAccess.seek(offset + localOffset); + count = (int) (length - localOffset); + } + + @Override + public void skip(int bytes) throws IOException { + int newCount = count - bytes; + if (newCount < 0 || newCount > length) { + throw new IllegalArgumentException(); + } + seek(length - newCount); + /* + can't use next code because don't want to rewind backing DataAccess on reset() + i.e. this.reset() modifies state of this instance only, while filtered DA may go further. + Only actual this.skip/seek/read would rewind it to desired position + dataAccess.skip(bytes); + count = newCount; + */ + + } + + @Override + public byte readByte() throws IOException { + if (count <= 0) { + throw new IllegalArgumentException("Underflow"); // XXX be descriptive + } + if (count == length) { + dataAccess.seek(offset); + } + count--; + return dataAccess.readByte(); + } + + @Override + public void readBytes(byte[] b, int off, int len) throws IOException { + if (len == 0) { + return; + } + if (count <= 0 || len > count) { + throw new IllegalArgumentException(String.format("Underflow. Bytes left: %d, asked to read %d", count, len)); + } + if (count == length) { + dataAccess.seek(offset); + } + dataAccess.readBytes(b, off, len); + count -= len; + } + + // done shall be no-op, as we have no idea what's going on with DataAccess we filter +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/InflaterDataAccess.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/InflaterDataAccess.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,185 @@ +/* + * 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 java.io.EOFException; +import java.io.IOException; +import java.util.zip.DataFormatException; +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? + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class InflaterDataAccess extends FilterDataAccess { + + private final Inflater inflater; + private final byte[] buffer; + private final byte[] singleByte = new byte[1]; + private int decompressedPos = 0; + private int decompressedLength; + + public InflaterDataAccess(DataAccess dataAccess, int offset, int compressedLength) { + this(dataAccess, offset, compressedLength, -1, new Inflater(), 512); + } + + public InflaterDataAccess(DataAccess dataAccess, int offset, int compressedLength, int actualLength) { + this(dataAccess, offset, compressedLength, actualLength, new Inflater(), 512); + } + + public InflaterDataAccess(DataAccess dataAccess, int offset, int compressedLength, int actualLength, Inflater inflater, int bufSize) { + super(dataAccess, offset, compressedLength); + this.inflater = inflater; + this.decompressedLength = actualLength; + buffer = new byte[bufSize]; + } + + @Override + public InflaterDataAccess reset() throws IOException { + super.reset(); + inflater.reset(); + decompressedPos = 0; + return this; + } + + @Override + protected int available() { + return length() - decompressedPos; + } + + @Override + public boolean isEmpty() { + // 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() { + 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; + } + 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); + } + } + 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 + } + } + + @Override + public void seek(int localOffset) throws IOException { + if (localOffset < 0 /* || localOffset >= length() */) { + throw new IllegalArgumentException(); + } + if (localOffset >= decompressedPos) { + skip((int) (localOffset - decompressedPos)); + } else { + reset(); + skip((int) localOffset); + } + } + + @Override + public void skip(int bytes) throws IOException { + if (bytes < 0) { + bytes += decompressedPos; + if (bytes < 0) { + throw new IOException("Underflow. Rewind past start of the slice."); + } + reset(); + // fall-through + } + while (!isEmpty() && bytes > 0) { + readByte(); + bytes--; + } + if (bytes != 0) { + throw new IOException("Underflow. Rewind past end of the slice"); + } + } + + @Override + public byte readByte() throws IOException { + readBytes(singleByte, 0, 1); + return singleByte[0]; + } + + @Override + public void readBytes(byte[] b, int off, int len) throws IOException { + try { + int n; + while (len > 0) { + while ((n = inflater.inflate(b, off, len)) == 0) { + // FIXME few last bytes (checksum?) may be ignored by inflater, thus inflate may return 0 in + // perfectly legal conditions (when all data already expanded, but there are still some bytes + // in the input stream + if (inflater.finished() || inflater.needsDictionary()) { + throw new EOFException(); + } + if (inflater.needsInput()) { + // fill: + int toRead = super.available(); + if (toRead > buffer.length) { + toRead = buffer.length; + } + super.readBytes(buffer, 0, toRead); + inflater.setInput(buffer, 0, toRead); + } + } + off += n; + len -= n; + decompressedPos += n; + if (len == 0) { + return; // filled + } + } + } catch (DataFormatException e) { + String s = e.getMessage(); + throw new ZipException(s != null ? s : "Invalid ZLIB data format"); + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/Internals.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/Internals.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,111 @@ +/* + * 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 static org.tmatesoft.hg.internal.RequiresFile.*; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.PathRewrite; + +/** + * Fields/members that shall not be visible + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class Internals { + + private int requiresFlags = 0; + private List filterFactories; + + + public Internals() { + } + + public/*for tests, otherwise pkg*/ void setStorageConfig(int version, int flags) { + requiresFlags = flags; + } + + // XXX perhaps, should keep both fields right here, not in the HgRepository + public PathRewrite buildDataFilesHelper() { + return new StoragePathHelper((requiresFlags & STORE) != 0, (requiresFlags & FNCACHE) != 0, (requiresFlags & DOTENCODE) != 0); + } + + public PathRewrite buildRepositoryFilesHelper() { + if ((requiresFlags & STORE) != 0) { + return new PathRewrite() { + public String rewrite(String path) { + return "store/" + path; + } + }; + } else { + return new PathRewrite() { + public String rewrite(String path) { + //no-op + return path; + } + }; + } + } + + public ConfigFile newConfigFile() { + return new ConfigFile(); + } + + public List getFilters(HgRepository hgRepo, ConfigFile cfg) { + if (filterFactories == null) { + filterFactories = new ArrayList(); + if (cfg.hasEnabledExtension("eol")) { + NewlineFilter.Factory ff = new NewlineFilter.Factory(); + ff.initialize(hgRepo, cfg); + filterFactories.add(ff); + } + if (cfg.hasEnabledExtension("keyword")) { + KeywordFilter.Factory ff = new KeywordFilter.Factory(); + ff.initialize(hgRepo, cfg); + filterFactories.add(ff); + } + } + return filterFactories; + } + + public void initEmptyRepository(File hgDir) throws IOException { + hgDir.mkdir(); + FileOutputStream requiresFile = new FileOutputStream(new File(hgDir, "requires")); + StringBuilder sb = new StringBuilder(40); + sb.append("revlogv1\n"); + if ((requiresFlags & STORE) != 0) { + sb.append("store\n"); + } + if ((requiresFlags & FNCACHE) != 0) { + sb.append("fncache\n"); + } + if ((requiresFlags & DOTENCODE) != 0) { + sb.append("dotencode\n"); + } + requiresFile.write(sb.toString().getBytes()); + requiresFile.close(); + new File(hgDir, "store").mkdir(); // with that, hg verify says ok. + } + +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/KeywordFilter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/KeywordFilter.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,323 @@ +/* + * 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 java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Map; +import java.util.TreeMap; + +import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.Path; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class KeywordFilter implements Filter { + // present implementation is stateless, however, filter use pattern shall not assume that. In fact, Factory may us that + private final HgRepository repo; + private final boolean isExpanding; + private final TreeMap keywords; + private final int minBufferLen; + private final Path path; + private RawChangeset latestFileCset; + + /** + * + * @param hgRepo + * @param path + * @param expand true to expand keywords, false to shrink + */ + private KeywordFilter(HgRepository hgRepo, Path p, boolean expand) { + repo = hgRepo; + path = p; + isExpanding = expand; + keywords = new TreeMap(); + keywords.put("Id", "Id"); + keywords.put("Revision", "Revision"); + keywords.put("Author", "Author"); + keywords.put("Date", "Date"); + keywords.put("LastChangedRevision", "LastChangedRevision"); + keywords.put("LastChangedBy", "LastChangedBy"); + keywords.put("LastChangedDate", "LastChangedDate"); + keywords.put("Source", "Source"); + keywords.put("Header", "Header"); + + int l = 0; + for (String s : keywords.keySet()) { + if (s.length() > l) { + l = s.length(); + } + } + // FIXME 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*/); + } + + /** + * @param src buffer ready to be read + * @return buffer ready to be read and original buffer's position modified to reflect consumed bytes. IOW, if source buffer + * on return has remaining bytes, they are assumed not-read (not processed) and next chunk passed to filter is supposed to + * start with them + */ + public ByteBuffer filter(ByteBuffer src) { + if (src.capacity() < minBufferLen) { + throw new IllegalStateException(String.format("Need buffer of at least %d bytes to ensure filter won't hang", minBufferLen)); + } + ByteBuffer rv = null; + int keywordStart = -1; + int x = src.position(); + int copyFrom = x; // needs to be updated each time we copy a slice, but not each time we modify source index (x) + while (x < src.limit()) { + if (keywordStart == -1) { + int i = indexOf(src, '$', x, false); + if (i == -1) { + if (rv == null) { + return src; + } else { + copySlice(src, copyFrom, src.limit(), rv); + rv.flip(); + src.position(src.limit()); + return rv; + } + } + keywordStart = i; + // fall-through + } + if (keywordStart >= 0) { + int i = indexOf(src, '$', keywordStart+1, true); + if (i == -1) { + // 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, + // 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 + // not to run into such situation + throw new IllegalStateException("Try src buffer of a greater size"); + } + rv = ByteBuffer.allocate(keywordStart - copyFrom); + } + // copy all from source till latest possible kw start + copySlice(src, copyFrom, keywordStart, rv); + rv.flip(); + // and tell caller we've consumed only to the potential kw start + src.position(keywordStart); + return rv; + } else if (src.get(i) == '$') { + // end of keyword, or start of a new one. + String keyword; + if ((keyword = matchKeyword(src, keywordStart, i)) != null) { + if (rv == null) { + // src.remaining(), not .capacity because src is not read, and remaining represents + // actual bytes count, while capacity - potential. + // Factor of 4 is pure guess and a HACK, need to be fixed with re-expanding buffer on demand + rv = ByteBuffer.allocate(isExpanding ? src.remaining() * 4 : src.remaining()); + } + copySlice(src, copyFrom, keywordStart+1, rv); + rv.put(keyword.getBytes()); + if (isExpanding) { + rv.put((byte) ':'); + rv.put((byte) ' '); + expandKeywordValue(keyword, rv); + rv.put((byte) ' '); + } + rv.put((byte) '$'); + keywordStart = -1; + x = i+1; + copyFrom = x; + continue; + } else { + if (rv != null) { + // we've already did some substitution, thus need to copy bytes we've scanned. + copySlice(src, x, i, rv); + copyFrom = i; + } // no else in attempt to avoid rv creation if no real kw would be found + keywordStart = i; + x = i; // '$' at i wasn't consumed, hence x points to i, not i+1. This is to avoid problems with case: "sdfsd $ asdfs $Id$ sdf" + continue; + } + } else { + assert src.get(i) == '\n' || src.get(i) == '\r'; + // line break + if (rv != null) { + copySlice(src, x, i+1, rv); + copyFrom = i+1; + } + x = i+1; + keywordStart = -1; // Wasn't keyword, really + continue; // try once again + } + } + } + if (keywordStart != -1) { + if (rv == null) { + // no expansion happened yet, and we have potential kw start + rv = ByteBuffer.allocate(keywordStart - src.position()); + copySlice(src, src.position(), keywordStart, rv); + } + src.position(keywordStart); + } + if (rv != null) { + rv.flip(); + return rv; + } + return src; + } + + /** + * @param keyword + * @param rv + */ + private void expandKeywordValue(String keyword, ByteBuffer rv) { + if ("Id".equals(keyword)) { + rv.put(identityString().getBytes()); + } else if ("Revision".equals(keyword)) { + rv.put(revision().getBytes()); + } else if ("Author".equals(keyword)) { + rv.put(username().getBytes()); + } else if ("Date".equals(keyword)) { + rv.put(date().getBytes()); + } else { + throw new IllegalStateException(String.format("Keyword %s is not yet supported", keyword)); + } + } + + private String matchKeyword(ByteBuffer src, int kwStart, int kwEnd) { + assert kwEnd - kwStart - 1 > 0; + assert src.get(kwStart) == src.get(kwEnd) && src.get(kwEnd) == '$'; + char[] chars = new char[kwEnd - kwStart - 1]; + int i; + for (i = 0; i < chars.length; i++) { + char c = (char) src.get(kwStart + 1 + i); + if (c == ':') { + break; + } + chars[i] = c; + } + String kw = new String(chars, 0, i); +// XXX may use subMap to look up keywords based on few available characters (not waiting till closing $) +// System.out.println(keywords.subMap("I", "J")); +// System.out.println(keywords.subMap("A", "B")); +// System.out.println(keywords.subMap("Au", "B")); + return keywords.get(kw); + } + + // copies part of the src buffer, [from..to). doesn't modify src position + static void copySlice(ByteBuffer src, int from, int to, ByteBuffer dst) { + if (to > src.limit()) { + throw new IllegalArgumentException("Bad right boundary"); + } + if (dst.remaining() < to - from) { + throw new IllegalArgumentException("Not enough room in the destination buffer"); + } + for (int i = from; i < to; i++) { + dst.put(src.get(i)); + } + } + + private static int indexOf(ByteBuffer b, char ch, int from, boolean newlineBreaks) { + for (int i = from; i < b.limit(); i++) { + byte c = b.get(i); + if (ch == c) { + return i; + } + if (newlineBreaks && (c == '\n' || c == '\r')) { + return i; + } + } + return -1; + } + + private String identityString() { + return String.format("%s,v %s %s %s", path, revision(), date(), username()); + } + + private String revision() { + // FIXME add cset's nodeid into Changeset class + int csetRev = repo.getFileNode(path).getChangesetLocalRevision(HgRepository.TIP); + return repo.getChangelog().getRevision(csetRev).shortNotation(); + } + + private String username() { + return getChangeset().user(); + } + + private String date() { + return String.format("%tY/% patterns = new ArrayList(); + for (Map.Entry e : cfg.getSection("keyword").entrySet()) { + if (!"ignore".equalsIgnoreCase(e.getValue())) { + patterns.add(e.getKey()); + } + } + matcher = new PathGlobMatcher(patterns.toArray(new String[patterns.size()])); + // TODO read and respect keyword patterns from [keywordmaps] + } + + public Filter create(Path path, Options opts) { + if (matcher.accept(path)) { + return new KeywordFilter(repo, path, opts.getDirection() == Filter.Direction.FromRepo); + } + return null; + } + } + +// +// public static void main(String[] args) throws Exception { +// FileInputStream fis = new FileInputStream(new File("/temp/kwoutput.txt")); +// FileOutputStream fos = new FileOutputStream(new File("/temp/kwoutput2.txt")); +// ByteBuffer b = ByteBuffer.allocate(256); +// KeywordFilter kwFilter = new KeywordFilter(false); +// while (fis.getChannel().read(b) != -1) { +// b.flip(); // get ready to be read +// ByteBuffer f = kwFilter.filter(b); +// fos.getChannel().write(f); // XXX in fact, f may not be fully consumed +// if (b.hasRemaining()) { +// b.compact(); +// } else { +// b.clear(); +// } +// } +// fis.close(); +// fos.flush(); +// fos.close(); +// } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/NewlineFilter.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/NewlineFilter.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,269 @@ +/* + * 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 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 java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Map; + +import org.tmatesoft.hg.repo.HgInternals; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.Path; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class NewlineFilter implements Filter { + + // if allowInconsistent is true, filter simply pass incorrect newline characters (single \r or \r\n on *nix and single \n on Windows) as is, + // i.e. doesn't try to convert them into appropriate newline characters. XXX revisit if Keyword extension behaves differently + private final boolean allowInconsistent; + private final boolean winToNix; + + private NewlineFilter(boolean failIfInconsistent, int transform) { + winToNix = transform == 0; + allowInconsistent = !failIfInconsistent; + } + + public ByteBuffer filter(ByteBuffer src) { + if (winToNix) { + return win2nix(src); + } else { + return nix2win(src); + } + } + + private ByteBuffer win2nix(ByteBuffer src) { + int x = src.position(); // source index + int lookupStart = x; + ByteBuffer dst = null; + while (x < src.limit()) { + // x, lookupStart, ir and in are absolute positions within src buffer, which is never read with modifying operations + int ir = indexOf('\r', src, lookupStart); + int in = indexOf('\n', src, lookupStart); + if (ir == -1) { + if (in == -1 || allowInconsistent) { + if (dst != null) { + copySlice(src, x, src.limit(), dst); + x = src.limit(); // consumed all + } + break; + } else { + fail(src, in); + } + } + // in == -1 while ir != -1 may be valid case if ir is the last char of the buffer, we check below for that + if (in != -1 && in != ir+1 && !allowInconsistent) { + fail(src, in); + } + if (dst == null) { + dst = ByteBuffer.allocate(src.remaining()); + } + copySlice(src, x, ir, dst); + if (ir+1 == src.limit()) { + // last char of the buffer - + // consume src till that char and let next iteration work on it + x = ir; + break; + } + if (in != ir + 1) { + x = ir+1; // generally in, but if allowInconsistent==true and \r is not followed by \n, then + // cases like "one \r two \r\n three" shall be processed correctly (second pair would be ignored if x==in) + lookupStart = ir+1; + } else { + x = in; + lookupStart = x+1; // skip \n for next lookup + } + } + src.position(x); // mark we've consumed up to x + return dst == null ? src : (ByteBuffer) dst.flip(); + } + + private ByteBuffer nix2win(ByteBuffer src) { + int x = src.position(); + ByteBuffer dst = null; + while (x < src.limit()) { + int in = indexOf('\n', src, x); + int ir = indexOf('\r', src, x, in == -1 ? src.limit() : in); + if (in == -1) { + if (ir == -1 || allowInconsistent) { + break; + } else { + fail(src, ir); + } + } else if (ir != -1 && !allowInconsistent) { + fail(src, ir); + } + + // x <= in < src.limit + // allowInconsistent && x <= ir < in || ir == -1 + if (dst == null) { + // buffer full of \n grows as much as twice in size + dst = ByteBuffer.allocate(src.remaining() * 2); + } + copySlice(src, x, in, dst); + if (ir == -1 || ir+1 != in) { + dst.put((byte) '\r'); + } // otherwise (ir!=-1 && ir+1==in) we found \r\n pair, don't convert to \r\r\n + // we may copy \n at src[in] on the next iteration, but would need extra lookupIndex variable then. + dst.put((byte) '\n'); + x = in+1; + } + src.position(x); + return dst == null ? src : (ByteBuffer) dst.flip(); + } + + + private void fail(ByteBuffer b, int pos) { + throw new RuntimeException(String.format("Inconsistent newline characters in the stream (char 0x%x, local index:%d)", b.get(pos), pos)); + } + + private static int indexOf(char ch, ByteBuffer b, int from) { + return indexOf(ch, b, from, b.limit()); + } + + // looks up in buf[from..to) + private static int indexOf(char ch, ByteBuffer b, int from, int to) { + for (int i = from; i < to; i++) { + byte c = b.get(i); + if (ch == c) { + return i; + } + } + return -1; + } + + public static class Factory implements Filter.Factory { + private boolean failIfInconsistent = true; + private Path.Matcher lfMatcher; + private Path.Matcher crlfMatcher; + private Path.Matcher binMatcher; + private Path.Matcher nativeMatcher; + private String nativeRepoFormat; + private String nativeOSFormat; + + public void initialize(HgRepository hgRepo, ConfigFile cfg) { + failIfInconsistent = cfg.getBoolean("eol", "only-consistent", true); + File cfgFile = new File(new HgInternals(hgRepo).getRepositoryDir().getParentFile(), ".hgeol"); + if (!cfgFile.canRead()) { + return; + } + // XXX if .hgeol is not checked out, we may get it from repository +// HgDataFile cfgFileNode = hgRepo.getFileNode(".hgeol"); +// if (!cfgFileNode.exists()) { +// return; +// } + // XXX perhaps, add HgDataFile.hasWorkingCopy and workingCopyContent()? + ConfigFile hgeol = new ConfigFile(); + hgeol.addLocation(cfgFile); + nativeRepoFormat = hgeol.getSection("repository").get("native"); + if (nativeRepoFormat == null) { + nativeRepoFormat = "LF"; + } + final String os = System.getProperty("os.name"); // XXX need centralized set of properties + nativeOSFormat = os.indexOf("Windows") != -1 ? "CRLF" : "LF"; + // I assume pattern ordering in .hgeol is not important + ArrayList lfPatterns = new ArrayList(); + ArrayList crlfPatterns = new ArrayList(); + ArrayList nativePatterns = new ArrayList(); + ArrayList binPatterns = new ArrayList(); + for (Map.Entry e : hgeol.getSection("patterns").entrySet()) { + if ("CRLF".equals(e.getValue())) { + crlfPatterns.add(e.getKey()); + } else if ("LF".equals(e.getValue())) { + lfPatterns.add(e.getKey()); + } else if ("native".equals(e.getValue())) { + nativePatterns.add(e.getKey()); + } else if ("BIN".equals(e.getValue())) { + binPatterns.add(e.getKey()); + } else { + System.out.printf("Can't recognize .hgeol entry: %s for %s", e.getValue(), e.getKey()); // FIXME log warning + } + } + if (!crlfPatterns.isEmpty()) { + crlfMatcher = new PathGlobMatcher(crlfPatterns.toArray(new String[crlfPatterns.size()])); + } + if (!lfPatterns.isEmpty()) { + lfMatcher = new PathGlobMatcher(lfPatterns.toArray(new String[lfPatterns.size()])); + } + if (!binPatterns.isEmpty()) { + binMatcher = new PathGlobMatcher(binPatterns.toArray(new String[binPatterns.size()])); + } + if (!nativePatterns.isEmpty()) { + nativeMatcher = new PathGlobMatcher(nativePatterns.toArray(new String[nativePatterns.size()])); + } + } + + public Filter create(Path path, Options opts) { + if (binMatcher == null && crlfMatcher == null && lfMatcher == null && nativeMatcher == null) { + // not initialized - perhaps, no .hgeol found + return null; + } + if (binMatcher != null && binMatcher.accept(path)) { + return null; + } + if (crlfMatcher != null && crlfMatcher.accept(path)) { + return new NewlineFilter(failIfInconsistent, 1); + } else if (lfMatcher != null && lfMatcher.accept(path)) { + return new NewlineFilter(failIfInconsistent, 0); + } else if (nativeMatcher != null && nativeMatcher.accept(path)) { + if (nativeOSFormat.equals(nativeRepoFormat)) { + return null; + } + if (opts.getDirection() == FromRepo) { + int transform = "CRLF".equals(nativeOSFormat) ? 1 : 0; + return new NewlineFilter(failIfInconsistent, transform); + } else if (opts.getDirection() == ToRepo) { + int transform = "CRLF".equals(nativeOSFormat) ? 0 : 1; + return new NewlineFilter(failIfInconsistent, transform); + } + return null; + } + return null; + } + } + + public static void main(String[] args) throws Exception { + FileInputStream fis = new FileInputStream(new File("/temp/design.lf.txt")); + FileOutputStream fos = new FileOutputStream(new File("/temp/design.newline.out")); + ByteBuffer b = ByteBuffer.allocate(12); + NewlineFilter nlFilter = new NewlineFilter(true, 1); + while (fis.getChannel().read(b) != -1) { + b.flip(); // get ready to be read + ByteBuffer f = nlFilter.filter(b); + fos.getChannel().write(f); // XXX in fact, f may not be fully consumed + if (b.hasRemaining()) { + b.compact(); + } else { + b.clear(); + } + } + fis.close(); + fos.flush(); + fos.close(); + } + +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/PathGlobMatcher.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/PathGlobMatcher.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,94 @@ +/* + * 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 java.util.regex.PatternSyntaxException; + +import org.tmatesoft.hg.util.Path; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class PathGlobMatcher implements Path.Matcher { + + private final PathRegexpMatcher delegate; + + /** + * + * @param globPatterns + * @throws NullPointerException if argument is null + * @throws IllegalArgumentException if any of the patterns is not valid + */ + public PathGlobMatcher(String... globPatterns) { + String[] regexp = new String[globPatterns.length]; //deliberately let fail with NPE + int i = 0; + for (String s : globPatterns) { + regexp[i] = glob2regexp(s); + } + try { + delegate = new PathRegexpMatcher(regexp); + } catch (PatternSyntaxException ex) { + ex.printStackTrace(); + throw new IllegalArgumentException(ex); + } + } + + + // 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) { + int end = glob.length() - 1; + boolean needLineEndMatch = glob.charAt(end) != '*'; + while (end > 0 && glob.charAt(end) == '*') end--; // remove trailing * that are useless for Pattern.find() + StringBuilder sb = new StringBuilder(end*2); + if (glob.charAt(0) != '*') { + sb.append('^'); + } + for (int i = 0; i <= end; i++) { + char ch = glob.charAt(i); + if (ch == '*') { + if (glob.charAt(i+1) == '*') { // i < end because we've stripped any trailing * earlier + // any char, including path segment separator + sb.append(".*?"); + i++; + } else { + // just path segments + sb.append("[^/]*?"); + } + continue; + } else if (ch == '?') { + sb.append("[^/]"); + continue; + } else if (ch == '.' || ch == '\\') { + sb.append('\\'); + } + sb.append(ch); + } + if (needLineEndMatch) { + sb.append('$'); + } + return sb.toString(); + } + + public boolean accept(Path path) { + return delegate.accept(path); + } + +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/PathRegexpMatcher.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/PathRegexpMatcher.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,64 @@ +/* + * 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 java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +import org.tmatesoft.hg.util.Path; +import org.tmatesoft.hg.util.Path.Matcher; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class PathRegexpMatcher implements Matcher { + private Pattern[] patterns; + + // disjunction, matches if any pattern found + // uses pattern.find(), not pattern.matches() + public PathRegexpMatcher(Pattern... p) { + if (p == null) { + throw new IllegalArgumentException(); + } + patterns = p; + } + + public PathRegexpMatcher(String... p) throws PatternSyntaxException { + this(compile(p)); + } + + private static Pattern[] compile(String[] p) throws PatternSyntaxException { + // deliberately do no check for null, let it fail + Pattern[] rv = new Pattern[p.length]; + int i = 0; + for (String s : p) { + rv[i++] = Pattern.compile(s); + } + return rv; + } + + public boolean accept(Path path) { + for (Pattern p : patterns) { + if (p.matcher(path).find()) { + return true; + } + } + return false; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/Pool.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/Pool.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,53 @@ +/* + * 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 java.util.HashMap; + +/** + * Instance pooling. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class Pool { + private final HashMap unify = new HashMap(); + + public T unify(T t) { + T rv = unify.get(t); + if (rv == null) { + // first time we see a new value + unify.put(t, t); + rv = t; + } + return rv; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append(Pool.class.getSimpleName()); + sb.append('<'); + if (!unify.isEmpty()) { + sb.append(unify.keySet().iterator().next().getClass().getName()); + } + sb.append('>'); + sb.append(':'); + sb.append(unify.size()); + return sb.toString(); + } +} \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/RelativePathRewrite.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/RelativePathRewrite.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,49 @@ +/* + * 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 java.io.File; + +import org.tmatesoft.hg.util.PathRewrite; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class RelativePathRewrite implements PathRewrite { + + private final String rootPath; + + public RelativePathRewrite(File root) { + this(root.getPath()); + } + + public RelativePathRewrite(String rootPath) { + this.rootPath = rootPath; + } + + public String rewrite(String path) { + if (path != null && path.startsWith(rootPath)) { + if (path.length() == rootPath.length()) { + return ""; + } + return path.substring(rootPath.length() + 1); + } + return path; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/RepositoryComparator.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/RepositoryComparator.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,570 @@ +/* + * 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 static org.tmatesoft.hg.core.Nodeid.NULL; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +import org.tmatesoft.hg.core.HgBadStateException; +import org.tmatesoft.hg.core.HgException; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.HgChangelog; +import org.tmatesoft.hg.repo.HgRemoteRepository; +import org.tmatesoft.hg.repo.HgRemoteRepository.Range; +import org.tmatesoft.hg.repo.HgRemoteRepository.RemoteBranch; +import org.tmatesoft.hg.util.CancelSupport; +import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.util.ProgressSupport; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class RepositoryComparator { + + private final boolean debug = Boolean.parseBoolean(System.getProperty("hg4j.remote.debug")); + private final HgChangelog.ParentWalker localRepo; + private final HgRemoteRepository remoteRepo; + private List common; + + public RepositoryComparator(HgChangelog.ParentWalker pwLocal, HgRemoteRepository hgRemote) { + localRepo = pwLocal; + remoteRepo = hgRemote; + } + + public RepositoryComparator compare(Object context) throws HgException, CancelledException { + ProgressSupport progressSupport = ProgressSupport.Factory.get(context); + CancelSupport cancelSupport = CancelSupport.Factory.get(context); + cancelSupport.checkCancelled(); + progressSupport.start(10); + common = Collections.unmodifiableList(findCommonWithRemote()); + // sanity check + for (Nodeid n : common) { + if (!localRepo.knownNode(n)) { + throw new HgBadStateException("Unknown node reported as common:" + n); + } + } + progressSupport.done(); + return this; + } + + public List getCommon() { + if (common == null) { + throw new HgBadStateException("Call #compare(Object) first"); + } + return common; + } + + /** + * @return revisions that are children of common entries, i.e. revisions that are present on the local server and not on remote. + */ + public List getLocalOnlyRevisions() { + return localRepo.childrenOf(getCommon()); + } + + /** + * Similar to @link {@link #getLocalOnlyRevisions()}, use this one if you need access to changelog entry content, not + * only its revision number. + * @param inspector delegate to analyze changesets, shall not be null + */ + public void visitLocalOnlyRevisions(HgChangelog.Inspector inspector) { + if (inspector == null) { + throw new IllegalArgumentException(); + } + // one can use localRepo.childrenOf(getCommon()) and then iterate over nodeids, but there seems to be + // another approach to get all changes after common: + // find index of earliest revision, and report all that were later + final HgChangelog changelog = localRepo.getRepo().getChangelog(); + int earliestRevision = Integer.MAX_VALUE; + List commonKnown = getCommon(); + for (Nodeid n : commonKnown) { + if (!localRepo.hasChildren(n)) { + // there might be (old) nodes, known both locally and remotely, with no children + // hence, we don't need to consider their local revision number + continue; + } + int lr = changelog.getLocalRevision(n); + if (lr < earliestRevision) { + earliestRevision = lr; + } + } + if (earliestRevision == Integer.MAX_VALUE) { + // either there are no common nodes (known locally and at remote) + // or no local children found (local is up to date). In former case, perhaps I shall bit return silently, + // but check for possible wrong repo comparison (hs says 'repository is unrelated' if I try to + // check in/out for a repo that has no common nodes. + 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())); + } + changelog.range(earliestRevision+1, changelog.getLastRevision(), inspector); + } + + private List findCommonWithRemote() throws HgException { + List remoteHeads = remoteRepo.heads(); + LinkedList resultCommon = new LinkedList(); // these remotes are known in local + LinkedList toQuery = new LinkedList(); // these need further queries to find common + for (Nodeid rh : remoteHeads) { + if (localRepo.knownNode(rh)) { + resultCommon.add(rh); + } else { + toQuery.add(rh); + } + } + if (toQuery.isEmpty()) { + return resultCommon; + } + LinkedList checkUp2Head = new LinkedList(); // branch.root and branch.head are of interest only. + // these are branches with unknown head but known root, which might not be the last common known, + // i.e. there might be children changeset that are also available at remote, [..?..common-head..remote-head] - need to + // scroll up to common head. + while (!toQuery.isEmpty()) { + List remoteBranches = remoteRepo.branches(toQuery); //head, root, first parent, second parent + toQuery.clear(); + while(!remoteBranches.isEmpty()) { + RemoteBranch rb = remoteBranches.remove(0); + // I assume branches remote call gives branches with head equal to what I pass there, i.e. + // that I don't need to check whether rb.head is unknown. + if (localRepo.knownNode(rb.root)) { + // we known branch start, common head is somewhere in its descendants line + checkUp2Head.add(rb); + } else { + // dig deeper in the history, if necessary + if (!NULL.equals(rb.p1) && !localRepo.knownNode(rb.p1)) { + toQuery.add(rb.p1); + } + if (!NULL.equals(rb.p2) && !localRepo.knownNode(rb.p2)) { + toQuery.add(rb.p2); + } + } + } + } + // can't check nodes between checkUp2Head element and local heads, remote might have distinct descendants sequence + for (RemoteBranch rb : checkUp2Head) { + // rb.root is known locally + List remoteRevisions = remoteRepo.between(rb.head, rb.root); + if (remoteRevisions.isEmpty()) { + // head is immediate child + resultCommon.add(rb.root); + } else { + // between gives result from head to root, I'd like to go in reverse direction + Collections.reverse(remoteRevisions); + Nodeid root = rb.root; + while(!remoteRevisions.isEmpty()) { + Nodeid n = remoteRevisions.remove(0); + if (localRepo.knownNode(n)) { + if (remoteRevisions.isEmpty()) { + // this is the last known node before an unknown + resultCommon.add(n); + break; + } + if (remoteRevisions.size() == 1) { + // there's only one left between known n and unknown head + // this check is to save extra between query, not really essential + Nodeid last = remoteRevisions.remove(0); + resultCommon.add(localRepo.knownNode(last) ? last : n); + break; + } + // might get handy for next between query, to narrow search down + root = n; + } else { + remoteRevisions = remoteRepo.between(n, root); + Collections.reverse(remoteRevisions); + if (remoteRevisions.isEmpty()) { + resultCommon.add(root); + } + } + } + } + } + // TODO ensure unique elements in the list + return resultCommon; + } + + // somewhat similar to Outgoing.findCommonWithRemote() + public List calculateMissingBranches() throws HgException { + List remoteHeads = remoteRepo.heads(); + LinkedList common = new LinkedList(); // these remotes are known in local + LinkedList toQuery = new LinkedList(); // these need further queries to find common + for (Nodeid rh : remoteHeads) { + if (localRepo.knownNode(rh)) { + common.add(rh); + } else { + toQuery.add(rh); + } + } + if (toQuery.isEmpty()) { + return Collections.emptyList(); // no incoming changes + } + LinkedList branches2load = new LinkedList(); // return value + // detailed comments are in Outgoing.findCommonWithRemote + LinkedList checkUp2Head = new LinkedList(); + // records relation between branch head and its parent branch, if any + HashMap head2chain = new HashMap(); + while (!toQuery.isEmpty()) { + List remoteBranches = remoteRepo.branches(toQuery); //head, root, first parent, second parent + toQuery.clear(); + while(!remoteBranches.isEmpty()) { + RemoteBranch rb = remoteBranches.remove(0); + BranchChain chainElement = head2chain.get(rb.head); + if (chainElement == null) { + chainElement = new BranchChain(rb.head); + // record this unknown branch to download later + branches2load.add(chainElement); + // the only chance we'll need chainElement in the head2chain is when we know this branch's root + head2chain.put(rb.head, chainElement); + } + if (localRepo.knownNode(rb.root)) { + // we known branch start, common head is somewhere in its descendants line + checkUp2Head.add(rb); + } else { + chainElement.branchRoot = rb.root; + // dig deeper in the history, if necessary + boolean hasP1 = !NULL.equals(rb.p1), hasP2 = !NULL.equals(rb.p2); + if (hasP1 && !localRepo.knownNode(rb.p1)) { + toQuery.add(rb.p1); + // we might have seen parent node already, and recorded it as a branch chain + // we shall reuse existing BC to get it completely initializer (head2chain map + // on second put with the same key would leave first BC uninitialized. + + // It seems there's no reason to be affraid (XXX although shall double-check) + // that BC's chain would get corrupt (its p1 and p2 fields assigned twice with different values) + // as parents are always the same (and likely, BC that is common would be the last unknown) + BranchChain bc = head2chain.get(rb.p1); + if (bc == null) { + head2chain.put(rb.p1, bc = new BranchChain(rb.p1)); + } + chainElement.p1 = bc; + } + if (hasP2 && !localRepo.knownNode(rb.p2)) { + toQuery.add(rb.p2); + BranchChain bc = head2chain.get(rb.p2); + if (bc == null) { + head2chain.put(rb.p2, bc = new BranchChain(rb.p2)); + } + chainElement.p2 = bc; + } + if (!hasP1 && !hasP2) { + // special case, when we do incoming against blank repository, chainElement.branchRoot + // is first unknown element (revision 0). We need to add another fake BranchChain + // to fill the promise that terminal BranchChain has branchRoot that is known both locally and remotely + BranchChain fake = new BranchChain(NULL); + fake.branchRoot = NULL; + chainElement.p1 = chainElement.p2 = fake; + } + } + } + } + for (RemoteBranch rb : checkUp2Head) { + Nodeid h = rb.head; + Nodeid r = rb.root; + int watchdog = 1000; + assert head2chain.containsKey(h); + BranchChain bc = head2chain.get(h); + assert bc != null : h.toString(); + // if we know branch root locally, there could be no parent branch chain elements. + assert bc.p1 == null; + assert bc.p2 == null; + do { + List between = remoteRepo.between(h, r); + if (between.isEmpty()) { + bc.branchRoot = r; + break; + } else { + Collections.reverse(between); + for (Nodeid n : between) { + if (localRepo.knownNode(n)) { + r = n; + } else { + h = n; + break; + } + } + Nodeid lastInBetween = between.get(between.size() - 1); + if (r.equals(lastInBetween)) { + bc.branchRoot = r; + break; + } else if (h.equals(lastInBetween)) { // the only chance for current head pointer to point to the sequence tail + // is when r is second from the between list end (iow, head,1,[2],4,8...,root) + bc.branchRoot = r; + break; + } + } + } while(--watchdog > 0); + if (watchdog == 0) { + throw new HgBadStateException(String.format("Can't narrow down branch [%s, %s]", rb.head.shortNotation(), rb.root.shortNotation())); + } + } + if (debug) { + System.out.println("calculateMissingBranches:"); + for (BranchChain bc : branches2load) { + bc.dump(); + } + } + return branches2load; + } + + // root and head (and all between) are unknown for each chain element but last (terminal), which has known root (revision + // known to be locally and at remote server + // alternative would be to keep only unknown elements (so that promise of calculateMissingBranches would be 100% true), but that + // seems to complicate the method, while being useful only for the case when we ask incoming for an empty repository (i.e. + // where branch chain return all nodes, -1..tip. + public static final class BranchChain { + // when we construct a chain, we know head which is missing locally, hence init it right away. + // as for root (branch unknown start), we might happen to have one locally, and need further digging to find out right branch start + public final Nodeid branchHead; + public Nodeid branchRoot; + // either of these can be null, or both. + // although RemoteBranch has either both parents null, or both non-null, when we construct a chain + // we might encounter that we locally know one of branch's parent, hence in the chain corresponding field will be blank. + public BranchChain p1; + public BranchChain p2; + + public BranchChain(Nodeid head) { + assert head != null; + branchHead = head; + } + public boolean isTerminal() { + return p1 == null && p2 == null; // either can be null, see comment above. Terminal is only when no way to descent + } + + // true when this BranchChain is a branch that spans up to very start of the repository + // Thus, the only common revision is NULL, recorded in a fake BranchChain object shared between p1 and p2 + /*package-local*/ boolean isRepoStart() { + return p1 == p2 && p1 != null && p1.branchHead == p1.branchRoot && NULL.equals(p1.branchHead); + } + + @Override + public String toString() { + return String.format("BranchChain [%s, %s]", branchRoot, branchHead); + } + + void dump() { + System.out.println(toString()); + internalDump(" "); + } + + private void internalDump(String prefix) { + if (p1 != null) { + System.out.println(prefix + p1.toString()); + } else if (p2 != null) { + System.out.println(prefix + "NONE?!"); + } + if (p2 != null) { + System.out.println(prefix + p2.toString()); + } else if (p1 != null) { + System.out.println(prefix + "NONE?!"); + } + prefix += " "; + if (p1 != null) { + p1.internalDump(prefix); + } + if (p2 != null) { + p2.internalDump(prefix); + } + } + } + + /** + * @return list of nodeids from branchRoot to branchHead, inclusive. IOW, first element of the list is always root of the branch + */ + public List completeBranch(final Nodeid branchRoot, final Nodeid branchHead) throws HgException { + class DataEntry { + public final Nodeid queryHead; + public final int headIndex; + public List entries; + + public DataEntry(Nodeid head, int index, List data) { + queryHead = head; + headIndex = index; + entries = data; + } + }; + + List initial = remoteRepo.between(branchHead, branchRoot); + Nodeid[] result = new Nodeid[1 + (1 << initial.size())]; + result[0] = branchHead; + int rootIndex = -1; // index in the result, where to place branche's root. + if (initial.isEmpty()) { + rootIndex = 1; + } else if (initial.size() == 1) { + rootIndex = 2; + } + LinkedList datas = new LinkedList(); + // DataEntry in datas has entries list filled with 'between' data, whereas + // DataEntry in toQuery keeps only nodeid and its index, with entries to be initialized before + // moving to datas. + LinkedList toQuery = new LinkedList(); + // + datas.add(new DataEntry(branchHead, 0, initial)); + int totalQueries = 1; + HashSet queried = new HashSet(); + while(!datas.isEmpty()) { + // keep record of those planned to be queried next time we call between() + // although may keep these in queried, if really don't want separate collection + HashSet scheduled = new HashSet(); + do { + DataEntry de = datas.removeFirst(); + // populate result with discovered elements between de.qiueryRoot and branch's head + for (int i = 1, j = 0; j < de.entries.size(); i = i << 1, j++) { + int idx = de.headIndex + i; + result[idx] = de.entries.get(j); + } + // form next query entries from new unknown elements + if (de.entries.size() > 1) { + /* when entries has only one element, it means de.queryRoot was at head-2 position, and thus + * no new information can be obtained. E.g. when it's 2, it might be case of [0..4] query with + * [1,2] result, and we need one more query to get element 3. + */ + for (int i =1, j = 0; j < de.entries.size(); i = i<<1, j++) { + int idx = de.headIndex + i; + Nodeid x = de.entries.get(j); + if (!queried.contains(x) && !scheduled.contains(x) && (rootIndex == -1 || rootIndex - de.headIndex > 1)) { + /*queries for elements right before head is senseless, but unless we know head's index, do it anyway*/ + toQuery.add(new DataEntry(x, idx, null)); + scheduled.add(x); + } + } + } + } while (!datas.isEmpty()); + if (!toQuery.isEmpty()) { + totalQueries++; + } + // for each query, create an between request range, keep record Range->DataEntry to know range's start index + LinkedList betweenBatch = new LinkedList(); + HashMap rangeToEntry = new HashMap(); + for (DataEntry de : toQuery) { + queried.add(de.queryHead); + HgRemoteRepository.Range r = new HgRemoteRepository.Range(branchRoot, de.queryHead); + betweenBatch.add(r); + rangeToEntry.put(r, de); + } + if (!betweenBatch.isEmpty()) { + Map> between = remoteRepo.between(betweenBatch); + for (Entry> e : between.entrySet()) { + DataEntry de = rangeToEntry.get(e.getKey()); + assert de != null; + de.entries = e.getValue(); + if (rootIndex == -1 && de.entries.size() == 1) { + // returned sequence of length 1 means we used element from [head-2] as root + int numberOfElementsExcludingRootAndHead = de.headIndex + 1; + rootIndex = numberOfElementsExcludingRootAndHead + 1; + if (debug) { + System.out.printf("On query %d found out exact number of missing elements: %d\n", totalQueries, numberOfElementsExcludingRootAndHead); + } + } + datas.add(de); // queue up to record result and construct further requests + } + betweenBatch.clear(); + rangeToEntry.clear(); + } + toQuery.clear(); + } + if (rootIndex == -1) { + throw new HgBadStateException("Shall not happen, provided between output is correct"); // FIXME + } + result[rootIndex] = branchRoot; + boolean resultOk = true; + LinkedList fromRootToHead = new LinkedList(); + 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); + resultOk = false; + } + fromRootToHead.addFirst(n); // reverse order + } + if (debug) { + System.out.println("Total queries:" + totalQueries); + } + if (!resultOk) { + throw new HgBadStateException("See console for details"); // FIXME + } + return fromRootToHead; + } + + /** + * returns in order from branch root to head + * for a non-empty BranchChain, shall return modifiable list + */ + public List visitBranches(BranchChain bc) throws HgException { + if (bc == null) { + return Collections.emptyList(); + } + List mine = completeBranch(bc.branchRoot, bc.branchHead); + if (bc.isTerminal() || bc.isRepoStart()) { + return mine; + } + List parentBranch1 = visitBranches(bc.p1); + List parentBranch2 = visitBranches(bc.p2); + // merge + LinkedList merged = new LinkedList(); + ListIterator i1 = parentBranch1.listIterator(), i2 = parentBranch2.listIterator(); + while (i1.hasNext() && i2.hasNext()) { + Nodeid n1 = i1.next(); + Nodeid n2 = i2.next(); + if (n1.equals(n2)) { + merged.addLast(n1); + } else { + // first different => add both, and continue adding both tails sequentially + merged.add(n2); + merged.add(n1); + break; + } + } + // copy rest of second parent branch + while (i2.hasNext()) { + merged.add(i2.next()); + } + // copy rest of first parent branch + while (i1.hasNext()) { + merged.add(i1.next()); + } + // + ArrayList rv = new ArrayList(mine.size() + merged.size()); + rv.addAll(merged); + rv.addAll(mine); + return rv; + } + + public void collectKnownRoots(BranchChain bc, Set result) { + if (bc == null) { + return; + } + if (bc.isTerminal()) { + result.add(bc.branchRoot); + return; + } + if (bc.isRepoStart()) { + return; + } + collectKnownRoots(bc.p1, result); + collectKnownRoots(bc.p2, result); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/RequiresFile.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/RequiresFile.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,64 @@ +/* + * 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 java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class RequiresFile { + public static final int STORE = 1; + public static final int FNCACHE = 2; + public static final int DOTENCODE = 4; + + public RequiresFile() { + } + + public void parse(Internals repoImpl, File requiresFile) { + if (!requiresFile.exists()) { + return; + } + try { + boolean revlogv1 = false; + boolean store = false; + boolean fncache = false; + boolean dotencode = false; + BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(requiresFile))); + String line; + while ((line = br.readLine()) != null) { + revlogv1 |= "revlogv1".equals(line); + store |= "store".equals(line); + fncache |= "fncache".equals(line); + dotencode |= "dotencode".equals(line); + } + int flags = 0; + flags += store ? STORE : 0; + flags += fncache ? FNCACHE : 0; + flags += dotencode ? DOTENCODE : 0; + repoImpl.setStorageConfig(revlogv1 ? 1 : 0, flags); + } catch (IOException ex) { + ex.printStackTrace(); // FIXME log + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/RevlogDump.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/RevlogDump.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2010-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 java.io.BufferedInputStream; +import java.io.DataInput; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.math.BigInteger; +import java.util.zip.Inflater; + +/** + * Utility to test/debug/troubleshoot + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class RevlogDump { + + /** + * Takes 3 command line arguments - + * repository path, + * path to index file (i.e. store/data/hello.c.i) in the repository (relative) + * and "dumpData" whether to print actual content or just revlog headers + */ + public static void main(String[] args) throws Exception { + String repo = "/temp/hg/hello/.hg/"; + String filename = "store/00changelog.i"; +// String filename = "store/data/hello.c.i"; +// String filename = "store/data/docs/readme.i"; + boolean dumpData = true; + if (args.length > 1) { + repo = args[0]; + filename = args[1]; + dumpData = args.length > 2 ? "dumpData".equals(args[2]) : false; + } + // + DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(new File(repo + filename)))); + DataInput di = dis; + dis.mark(10); + int versionField = di.readInt(); + dis.reset(); + final int INLINEDATA = 1 << 16; + + boolean inlineData = (versionField & INLINEDATA) != 0; + System.out.printf("%#8x, inline: %b\n", versionField, inlineData); + System.out.println("Index Offset Flags Packed Actual Base Rev Link Rev Parent1 Parent2 nodeid"); + int entryCount = 0; + while (dis.available() > 0) { + long l = di.readLong(); + long offset = l >>> 16; + int flags = (int) (l & 0X0FFFF); + int compressedLen = di.readInt(); + int actualLen = di.readInt(); + int baseRevision = di.readInt(); + int linkRevision = di.readInt(); + int parent1Revision = di.readInt(); + int parent2Revision = di.readInt(); + byte[] buf = new byte[32]; + di.readFully(buf, 12, 20); + dis.skipBytes(12); + // CAN'T USE skip() here without extra precautions. E.g. I ran into situation when + // buffer was 8192 and BufferedInputStream was at position 8182 before attempt to skip(12). + // BIS silently skips available bytes and leaves me two extra bytes that ruin the rest of the code. + System.out.printf("%4d:%14d %6X %10d %10d %10d %10d %8d %8d %040x\n", entryCount, offset, flags, compressedLen, actualLen, baseRevision, linkRevision, parent1Revision, parent2Revision, new BigInteger(buf)); + if (inlineData) { + String resultString; + byte[] data = new byte[compressedLen]; + di.readFully(data); + if (data[0] == 0x78 /* 'x' */) { + Inflater zlib = new Inflater(); + zlib.setInput(data, 0, compressedLen); + byte[] result = new byte[actualLen*2]; + int resultLen = zlib.inflate(result); + zlib.end(); + resultString = new String(result, 0, resultLen, "UTF-8"); + } else if (data[0] == 0x75 /* 'u' */) { + resultString = new String(data, 1, data.length - 1, "UTF-8"); + } else { + resultString = new String(data); + } + if (dumpData) { + System.out.println(resultString); + } + } + entryCount++; + } + dis.close(); + // + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/RevlogStream.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/RevlogStream.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,460 @@ +/* + * Copyright (c) 2010-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 static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; +import static org.tmatesoft.hg.repo.HgRepository.TIP; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; + +import org.tmatesoft.hg.core.HgBadStateException; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.HgRepository; + + +/** + * ? Single RevlogStream per file per repository with accessor to record access session (e.g. with back/forward operations), + * or numerous RevlogStream with separate representation of the underlying data (cached, lazy ChunkStream)? + * + * @see http://mercurial.selenic.com/wiki/Revlog + * @see http://mercurial.selenic.com/wiki/RevlogNG + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class RevlogStream { + + /* + * makes sense for index with inline data only - actual offset of the record in the .i file (record entry + revision * record size)) + * + * long[] in fact (there are 8-bytes field in the revlog) + * However, (a) DataAccess currently doesn't operate with long seek/length + * and, of greater significance, (b) files with inlined data are designated for smaller files, + * guess, about 130 Kb, and offset there won't ever break int capacity + */ + private int[] indexRecordOffset; + private int[] baseRevisions; + private boolean inline = false; + private final File indexFile; + private final DataAccessProvider dataAccess; + + // if we need anything else from HgRepo, might replace DAP parameter with HgRepo and query it for DAP. + public RevlogStream(DataAccessProvider dap, File indexFile) { + this.dataAccess = dap; + this.indexFile = indexFile; + } + + /*package*/ DataAccess getIndexStream() { + 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); + } + + public int revisionCount() { + initOutline(); + return baseRevisions.length; + } + + public int dataLength(int revision) { + // XXX in fact, use of iterate() instead of this implementation may be quite reasonable. + // + final int indexSize = revisionCount(); + DataAccess daIndex = getIndexStream(); // XXX may supply a hint that I'll need really few bytes of data (although at some offset) + if (revision == TIP) { + revision = indexSize - 1; + } + try { + int recordOffset = getIndexOffsetInt(revision); + daIndex.seek(recordOffset + 12); // 6+2+4 + int actualLen = daIndex.readInt(); + return actualLen; + } catch (IOException ex) { + ex.printStackTrace(); // log error. FIXME better handling + throw new IllegalStateException(ex); + } finally { + daIndex.done(); + } + } + + public byte[] nodeid(int revision) { + final int indexSize = revisionCount(); + if (revision == TIP) { + revision = indexSize - 1; + } + if (revision < 0 || revision >= indexSize) { + throw new IllegalArgumentException(Integer.toString(revision)); + } + DataAccess daIndex = getIndexStream(); + try { + int recordOffset = getIndexOffsetInt(revision); + daIndex.seek(recordOffset + 32); + byte[] rv = new byte[20]; + daIndex.readBytes(rv, 0, 20); + return rv; + } catch (IOException ex) { + ex.printStackTrace(); + throw new IllegalStateException(); + } finally { + daIndex.done(); + } + } + + public int linkRevision(int revision) { + final int last = revisionCount() - 1; + if (revision == TIP) { + revision = last; + } + if (revision < 0 || revision > last) { + throw new IllegalArgumentException(Integer.toString(revision)); + } + DataAccess daIndex = getIndexStream(); + try { + int recordOffset = getIndexOffsetInt(revision); + daIndex.seek(recordOffset + 20); + int linkRev = daIndex.readInt(); + return linkRev; + } catch (IOException ex) { + ex.printStackTrace(); + throw new IllegalStateException(); + } finally { + daIndex.done(); + } + } + + // Perhaps, RevlogStream should be limited to use of plain int revisions for access, + // while Nodeids should be kept on the level up, in Revlog. Guess, Revlog better keep + // map of nodeids, and once this comes true, we may get rid of this method. + // Unlike its counterpart, {@link Revlog#getLocalRevisionNumber()}, doesn't fail with exception if node not found, + /** + * @return integer in [0..revisionCount()) or {@link HgRepository#BAD_REVISION} if not found + */ + public int findLocalRevisionNumber(Nodeid nodeid) { + // XXX this one may be implemented with iterate() once there's mechanism to stop iterations + final int indexSize = revisionCount(); + DataAccess daIndex = getIndexStream(); + try { + byte[] nodeidBuf = new byte[20]; + for (int i = 0; i < indexSize; i++) { + daIndex.skip(8); + int compressedLen = daIndex.readInt(); + daIndex.skip(20); + daIndex.readBytes(nodeidBuf, 0, 20); + if (nodeid.equalsTo(nodeidBuf)) { + return i; + } + daIndex.skip(inline ? 12 + compressedLen : 12); + } + } catch (IOException ex) { + ex.printStackTrace(); // log error. FIXME better handling + throw new IllegalStateException(ex); + } finally { + daIndex.done(); + } + return BAD_REVISION; + } + + + private final int REVLOGV1_RECORD_SIZE = 64; + + // 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) { + initOutline(); + final int indexSize = revisionCount(); + if (indexSize == 0) { + return; + } + if (end == TIP) { + end = indexSize - 1; + } + if (start == TIP) { + start = indexSize - 1; + } + if (start < 0 || start >= indexSize) { + throw new IllegalArgumentException(String.format("Bad left range boundary %d in [0..%d]", start, indexSize-1)); + } + if (end < start || end >= indexSize) { + throw new IllegalArgumentException(String.format("Bad right range boundary %d in [0..%d]", end, indexSize-1)); + } + // XXX may cache [start .. end] from index with a single read (pre-read) + + DataAccess daIndex = null, daData = null; + daIndex = getIndexStream(); + if (needData && !inline) { + daData = getDataStream(); + } + try { + byte[] nodeidBuf = new byte[20]; + DataAccess lastUserData = null; + int i; + boolean extraReadsToBaseRev = false; + if (needData && getBaseRevision(start) < start) { + i = getBaseRevision(start); + extraReadsToBaseRev = true; + } else { + i = start; + } + + daIndex.seek(getIndexOffsetInt(i)); + for (; i <= end; i++ ) { + if (inline && needData) { + // inspector reading data (though FilterDataAccess) may have affected index position + daIndex.seek(getIndexOffsetInt(i)); + } + long l = daIndex.readLong(); // 0 + long offset = i == 0 ? 0 : (l >>> 16); + @SuppressWarnings("unused") + int flags = (int) (l & 0X0FFFF); + int compressedLen = daIndex.readInt(); // +8 + int actualLen = daIndex.readInt(); // +12 + int baseRevision = daIndex.readInt(); // +16 + int linkRevision = daIndex.readInt(); // +20 + int parent1Revision = daIndex.readInt(); + int parent2Revision = daIndex.readInt(); + // Hg has 32 bytes here, uses 20 for nodeid, and keeps 12 last bytes empty + daIndex.readBytes(nodeidBuf, 0, 20); // +32 + daIndex.skip(12); + DataAccess userDataAccess = null; + if (needData) { + final byte firstByte; + int 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; + streamDataAccess = daData; + daData.seek(streamOffset); + } + final boolean patchToPrevious = baseRevision != i; // the only way I found to tell if it's a patch + firstByte = streamDataAccess.readByte(); + if (firstByte == 0x78 /* 'x' */) { + userDataAccess = new InflaterDataAccess(streamDataAccess, streamOffset, compressedLen, patchToPrevious ? -1 : actualLen); + } else if (firstByte == 0x75 /* 'u' */) { + userDataAccess = new FilterDataAccess(streamDataAccess, streamOffset+1, compressedLen-1); + } else { + // XXX Python impl in fact throws exception when there's not 'x', 'u' or '0' + // but I don't see reason not to return data as is + userDataAccess = new FilterDataAccess(streamDataAccess, streamOffset, compressedLen); + } + // XXX + if (patchToPrevious) { + // this is a patch + LinkedList patches = new LinkedList(); + while (!userDataAccess.isEmpty()) { + PatchRecord pr = PatchRecord.read(userDataAccess); +// System.out.printf("PatchRecord:%d %d %d\n", pr.start, pr.end, pr.len); + patches.add(pr); + } + userDataAccess.done(); + // + byte[] userData = apply(lastUserData, actualLen, patches); + userDataAccess = new ByteArrayDataAccess(userData); + } + } else { + if (inline) { + daIndex.skip(compressedLen); + } + } + if (!extraReadsToBaseRev || i >= start) { + inspector.next(i, actualLen, baseRevision, linkRevision, parent1Revision, parent2Revision, nodeidBuf, userDataAccess); + } + if (userDataAccess != null) { + userDataAccess.reset(); + if (lastUserData != null) { + lastUserData.done(); + } + lastUserData = userDataAccess; + } + } + } catch (IOException ex) { + throw new HgBadStateException(ex); // FIXME need better handling + } finally { + daIndex.done(); + if (daData != null) { + daData.done(); + } + } + } + + private int getBaseRevision(int revision) { + return baseRevisions[revision]; + } + + /** + * @return offset of the revision's record in the index (.i) stream + */ + private int getIndexOffsetInt(int revision) { + return inline ? indexRecordOffset[revision] : revision * REVLOGV1_RECORD_SIZE; + } + + private void initOutline() { + if (baseRevisions != null && baseRevisions.length > 0) { + return; + } + ArrayList resBases = new ArrayList(); + ArrayList resOffsets = new ArrayList(); + DataAccess da = getIndexStream(); + try { + if (da.isEmpty()) { + // do not fail with exception if stream is empty, it's likely intentional + baseRevisions = new int[0]; + return; + } + int versionField = da.readInt(); + da.readInt(); // just to skip next 4 bytes of offset + flags + final int INLINEDATA = 1 << 16; + inline = (versionField & INLINEDATA) != 0; + long offset = 0; // first offset is always 0, thus Hg uses it for other purposes + while(true) { + int compressedLen = da.readInt(); + // 8+4 = 12 bytes total read here + @SuppressWarnings("unused") + int actualLen = da.readInt(); + int baseRevision = da.readInt(); + // 12 + 8 = 20 bytes read here +// int linkRevision = di.readInt(); +// int parent1Revision = di.readInt(); +// int parent2Revision = di.readInt(); +// byte[] nodeid = new byte[32]; + resBases.add(baseRevision); + if (inline) { + int o = (int) 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)"); + } + resOffsets.add(o + REVLOGV1_RECORD_SIZE * resOffsets.size()); + da.skip(3*4 + 32 + compressedLen); // Check: 44 (skip) + 20 (read) = 64 (total RevlogNG record size) + } else { + da.skip(3*4 + 32); + } + if (da.isEmpty()) { + // fine, done then + baseRevisions = toArray(resBases); + if (inline) { + indexRecordOffset = toArray(resOffsets); + } + break; + } else { + // start reading next record + long l = da.readLong(); + offset = l >>> 16; + } + } + } catch (IOException ex) { + ex.printStackTrace(); // log error + // too bad, no outline then, but don't fail with NPE + baseRevisions = new int[0]; + } finally { + da.done(); + } + } + + private static int[] toArray(List l) { + int[] rv = new int[l.size()]; + for (int i = 0; i < rv.length; i++) { + rv[i] = l.get(i); + } + return rv; + } + + + // mpatch.c : apply() + // FIXME need to implement patch merge (fold, combine, gather and discard from aforementioned mpatch.[c|py]), also see Revlog and Mercurial PDF + public/*for HgBundle; until moved to better place*/static byte[] apply(DataAccess baseRevisionContent, int outcomeLen, List patch) throws IOException { + int last = 0, destIndex = 0; + if (outcomeLen == -1) { + outcomeLen = baseRevisionContent.length(); + for (PatchRecord pr : patch) { + outcomeLen += pr.start - last + pr.len; + last = pr.end; + } + outcomeLen -= last; + last = 0; + } + byte[] rv = new byte[outcomeLen]; + for (PatchRecord pr : patch) { + baseRevisionContent.seek(last); + baseRevisionContent.readBytes(rv, destIndex, pr.start-last); + destIndex += pr.start - last; + System.arraycopy(pr.data, 0, rv, destIndex, pr.data.length); + destIndex += pr.data.length; + last = pr.end; + } + baseRevisionContent.seek(last); + baseRevisionContent.readBytes(rv, destIndex, (int) (baseRevisionContent.length() - last)); + return rv; + } + + // @see http://mercurial.selenic.com/wiki/BundleFormat, in Changelog group description + public static class PatchRecord { + /* + Given there are pr1 and pr2: + pr1.start to pr1.end will be replaced with pr's data (of pr1.len) + pr1.end to pr2.start gets copied from base + */ + public int start, end, len; + public byte[] data; + + // TODO consider PatchRecord that only records data position (absolute in data source), and acquires data as needed + private PatchRecord(int p1, int p2, int length, byte[] src) { + start = p1; + end = p2; + len = length; + data = src; + } + + /*package-local*/ static PatchRecord read(byte[] data, int offset) { + final int x = offset; // shorthand + int p1 = ((data[x] & 0xFF)<< 24) | ((data[x+1] & 0xFF) << 16) | ((data[x+2] & 0xFF) << 8) | (data[x+3] & 0xFF); + int p2 = ((data[x+4] & 0xFF) << 24) | ((data[x+5] & 0xFF) << 16) | ((data[x+6] & 0xFF) << 8) | (data[x+7] & 0xFF); + int len = ((data[x+8] & 0xFF) << 24) | ((data[x+9] & 0xFF) << 16) | ((data[x+10] & 0xFF) << 8) | (data[x+11] & 0xFF); + byte[] dataCopy = new byte[len]; + System.arraycopy(data, x+12, dataCopy, 0, len); + return new PatchRecord(p1, p2, len, dataCopy); + } + + public /*for HgBundle*/ static PatchRecord read(DataAccess da) throws IOException { + int p1 = da.readInt(); + int p2 = da.readInt(); + int len = da.readInt(); + byte[] src = new byte[len]; + da.readBytes(src, 0, len); + return new PatchRecord(p1, p2, len, src); + } + } + + // FIXME byte[] data might be too expensive, for few usecases it may be better to have intermediate Access object (when we don't need full data + // instantly - e.g. calculate hash, or comparing two revisions + public interface Inspector { + // 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 revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[/*20*/] nodeid, DataAccess data); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/internal/StoragePathHelper.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/internal/StoragePathHelper.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,197 @@ +/* + * 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 java.util.Arrays; +import java.util.TreeSet; + +import org.tmatesoft.hg.util.PathRewrite; + +/** + * @see http://mercurial.selenic.com/wiki/CaseFoldingPlan + * @see http://mercurial.selenic.com/wiki/fncacheRepoFormat + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +class StoragePathHelper implements PathRewrite { + + private final boolean store; + private final boolean fncache; + private final boolean dotencode; + + public StoragePathHelper(boolean isStore, boolean isFncache, boolean isDotencode) { + store = isStore; + fncache = isFncache; + dotencode = isDotencode; + } + + // 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 + public String rewrite(String path) { + final String STR_STORE = "store/"; + final String STR_DATA = "data/"; + final String STR_DH = "dh/"; + final String reservedChars = "\\:*?\"<>|"; + char[] hexByte = new char[2]; + + path = path.replace(".hg/", ".hg.hg/").replace(".i/", ".i.hg/").replace(".d/", ".d.hg/"); + StringBuilder sb = new StringBuilder(path.length() << 1); + if (store || fncache) { + // encodefilename + for (int i = 0; i < path.length(); i++) { + final char ch = path.charAt(i); + if (ch >= 'a' && ch <= 'z') { + sb.append(ch); // POIRAE + } else if (ch >= 'A' && ch <= 'Z') { + sb.append('_'); + sb.append(Character.toLowerCase(ch)); // Perhaps, (char) (((int) ch) + 32)? Even better, |= 0x20? + } else if (reservedChars.indexOf(ch) != -1) { + sb.append('~'); + sb.append(toHexByte(ch, hexByte)); + } else if ((ch >= '~' /*126*/ && ch <= 255) || ch < ' ' /*32*/) { + sb.append('~'); + sb.append(toHexByte(ch, hexByte)); + } else if (ch == '_') { + sb.append('_'); + sb.append('_'); + } else { + sb.append(ch); + } + } + // auxencode + if (fncache) { + encodeWindowsDeviceNames(sb); + } + } + final int MAX_PATH_LEN = 120; + if (fncache && (sb.length() + STR_DATA.length() + ".i".length() > MAX_PATH_LEN)) { + String digest = new DigestHelper().sha1(STR_DATA, path, ".i").asHexString(); + final int DIR_PREFIX_LEN = 8; + // not sure why (-4) is here. 120 - 40 = up to 80 for path with ext. dh/ + ext(.i) = 3+2 + final int MAX_DIR_PREFIX = 8 * (DIR_PREFIX_LEN + 1) - 4; + sb = new StringBuilder(MAX_PATH_LEN); + for (int i = 0; i < path.length(); i++) { + final char ch = path.charAt(i); + if (ch >= 'a' && ch <= 'z') { + sb.append(ch); + } else if (ch >= 'A' && ch <= 'Z') { + sb.append((char) (ch | 0x20)); // lowercase + } else if (reservedChars.indexOf(ch) != -1) { + sb.append('~'); + sb.append(toHexByte(ch, hexByte)); + } else if ((ch >= '~' /*126*/ && ch <= 255) || ch < ' ' /*32*/) { + sb.append('~'); + sb.append(toHexByte(ch, hexByte)); + } else { + sb.append(ch); + } + } + encodeWindowsDeviceNames(sb); + int fnameStart = sb.lastIndexOf("/"); // since we rewrite file names, it never ends with slash (for dirs, I'd pass length-2); + StringBuilder completeHashName = new StringBuilder(MAX_PATH_LEN); + completeHashName.append(STR_STORE); + completeHashName.append(STR_DH); + if (fnameStart == -1) { + // no dirs, just long filename + sb.setLength(MAX_PATH_LEN - 40 /*digest.length()*/ - STR_DH.length() - ".i".length()); + completeHashName.append(sb); + } else { + StringBuilder sb2 = new StringBuilder(MAX_PATH_LEN); + int x = 0; + do { + int i = sb.indexOf("/", x); + final int sb2Len = sb2.length(); + if (i-x <= DIR_PREFIX_LEN) { // a b c d e f g h / + sb2.append(sb, x, i + 1); // with slash + } else { + sb2.append(sb, x, x + DIR_PREFIX_LEN); + // may unexpectedly end with bad character + final int last = sb2.length()-1; + char lastChar = sb2.charAt(last); + assert lastChar == sb.charAt(x + DIR_PREFIX_LEN - 1); + if (lastChar == '.' || lastChar == ' ') { + sb2.setCharAt(last, '_'); + } + sb2.append('/'); + } + if (sb2.length()-1 > MAX_DIR_PREFIX) { + sb2.setLength(sb2Len); // strip off last segment, it's too much + break; + } + x = i+1; + } while (x < fnameStart); + assert sb2.charAt(sb2.length() - 1) == '/'; + int left = MAX_PATH_LEN - sb2.length() - 40 /*digest.length()*/ - STR_DH.length() - ".i".length(); + assert left >= 0; + fnameStart++; // move from / to actual name + sb2.append(sb, fnameStart, fnameStart + left > sb.length() ? sb.length() : fnameStart+left); + completeHashName.append(sb2); + } + completeHashName.append(digest); + sb = completeHashName; + } else if (store) { + sb.insert(0, STR_STORE + STR_DATA); + } + sb.append(".i"); + return sb.toString(); + } + + private void encodeWindowsDeviceNames(StringBuilder sb) { + char[] hexByte = new char[2]; + int x = 0; // last segment start + final TreeSet windowsReservedFilenames = new TreeSet(); + windowsReservedFilenames.addAll(Arrays.asList("con prn aux nul com1 com2 com3 com4 com5 com6 com7 com8 com9 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9".split(" "))); + do { + int i = sb.indexOf("/", x); + if (i == -1) { + i = sb.length(); + } + // windows reserved filenames are at least of length 3 + if (i - x >= 3) { + boolean found = false; + if (i-x == 3 || i-x == 4) { + found = windowsReservedFilenames.contains(sb.subSequence(x, i)); + } else if (sb.charAt(x+3) == '.') { // implicit i-x > 3 + found = windowsReservedFilenames.contains(sb.subSequence(x, x+3)); + } else if (i-x > 4 && sb.charAt(x+4) == '.') { + found = windowsReservedFilenames.contains(sb.subSequence(x, x+4)); + } + if (found) { + sb.insert(x+3, toHexByte(sb.charAt(x+2), hexByte)); + sb.setCharAt(x+2, '~'); + i += 2; + } + } + if (dotencode && (sb.charAt(x) == '.' || sb.charAt(x) == ' ')) { + sb.insert(x+1, toHexByte(sb.charAt(x), hexByte)); + sb.setCharAt(x, '~'); // setChar *after* charAt/insert to get ~2e, not ~7e for '.' + i += 2; + } + x = i+1; + } while (x < sb.length()); + } + + private static char[] toHexByte(int ch, char[] buf) { + assert buf.length > 1; + final String hexDigits = "0123456789abcdef"; + buf[0] = hexDigits.charAt((ch & 0x00F0) >>> 4); + buf[1] = hexDigits.charAt(ch & 0x0F); + return buf; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/HgBundle.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/HgBundle.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,429 @@ +/* + * 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.repo; + +import static org.tmatesoft.hg.core.Nodeid.NULL; + +import java.io.File; +import java.io.IOException; +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.Nodeid; +import org.tmatesoft.hg.internal.ByteArrayChannel; +import org.tmatesoft.hg.internal.ByteArrayDataAccess; +import org.tmatesoft.hg.internal.DataAccess; +import org.tmatesoft.hg.internal.DataAccessProvider; +import org.tmatesoft.hg.internal.DigestHelper; +import org.tmatesoft.hg.internal.InflaterDataAccess; +import org.tmatesoft.hg.internal.RevlogStream; +import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; +import org.tmatesoft.hg.util.CancelledException; + +/** + * @see http://mercurial.selenic.com/wiki/BundleFormat + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgBundle { + + private final File bundleFile; + private final DataAccessProvider accessProvider; + + HgBundle(DataAccessProvider dap, File bundle) { + accessProvider = dap; + bundleFile = bundle; + } + + private DataAccess getDataStream() throws IOException { + DataAccess da = accessProvider.create(bundleFile); + byte[] signature = new byte[6]; + if (da.length() > 6) { + da.readBytes(signature, 0, 6); + if (signature[0] == 'H' && signature[1] == 'G' && signature[2] == '1' && signature[3] == '0') { + if (signature[4] == 'G' && signature[5] == 'Z') { + return new InflaterDataAccess(da, 6, da.length() - 6); + } + if (signature[4] == 'B' && signature[5] == 'Z') { + throw HgRepository.notImplemented(); + } + if (signature[4] != 'U' || signature[5] != 'N') { + throw new HgBadStateException("Bad bundle signature:" + new String(signature)); + } + // "...UN", fall-through + } else { + da.reset(); + } + } + return da; + } + + private int uses = 0; + public HgBundle link() { + uses++; + return this; + } + public void unlink() { + uses--; + if (uses == 0 && bundleFile != null) { + bundleFile.deleteOnExit(); + } + } + public boolean inUse() { + return uses > 0; + } + + /** + * Get changes recorded in the bundle that are missing from the supplied repository. + * @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 HgException, IOException { + Inspector bundleInsp = new Inspector() { + DigestHelper dh = new DigestHelper(); + boolean emptyChangelog = true; + private DataAccess prevRevContent; + private int revisionIndex; + + public void changelogStart() { + emptyChangelog = true; + revisionIndex = 0; + } + + public void changelogEnd() { + if (emptyChangelog) { + throw new IllegalStateException("No changelog group in the bundle"); // XXX perhaps, just be silent and/or log? + } + } + +/* + * Despite that BundleFormat wiki says: "Each Changelog entry patches the result of all previous patches + * (the previous, or parent patch of a given patch p is the patch that has a node equal to p's p1 field)", + * it seems not to hold true. Instead, each entry patches previous one, regardless of whether the one + * before is its parent (i.e. ge.firstParent()) or not. + * +Actual state in the changelog.i +Index Offset Flags Packed Actual Base Rev Link Rev Parent1 Parent2 nodeid + 50: 9212 0 209 329 48 50 49 -1 f1db8610da62a3e0beb8d360556ee1fd6eb9885e + 51: 9421 0 278 688 48 51 50 -1 9429c7bd1920fab164a9d2b621d38d57bcb49ae0 + 52: 9699 0 154 179 52 52 50 -1 30bd389788464287cee22ccff54c330a4b715de5 + 53: 9853 0 133 204 52 53 51 52 a6f39e595b2b54f56304470269a936ead77f5725 + 54: 9986 0 156 182 54 54 52 -1 fd4f2c98995beb051070630c272a9be87bef617d + +Excerpt from bundle (nodeid, p1, p2, cs): + f1db8610da62a3e0beb8d360556ee1fd6eb9885e 26e3eeaa39623de552b45ee1f55c14f36460f220 0000000000000000000000000000000000000000 f1db8610da62a3e0beb8d360556ee1fd6eb9885e; patches:4 + 9429c7bd1920fab164a9d2b621d38d57bcb49ae0 f1db8610da62a3e0beb8d360556ee1fd6eb9885e 0000000000000000000000000000000000000000 9429c7bd1920fab164a9d2b621d38d57bcb49ae0; patches:3 +> 30bd389788464287cee22ccff54c330a4b715de5 f1db8610da62a3e0beb8d360556ee1fd6eb9885e 0000000000000000000000000000000000000000 30bd389788464287cee22ccff54c330a4b715de5; patches:3 + a6f39e595b2b54f56304470269a936ead77f5725 9429c7bd1920fab164a9d2b621d38d57bcb49ae0 30bd389788464287cee22ccff54c330a4b715de5 a6f39e595b2b54f56304470269a936ead77f5725; patches:3 + fd4f2c98995beb051070630c272a9be87bef617d 30bd389788464287cee22ccff54c330a4b715de5 0000000000000000000000000000000000000000 fd4f2c98995beb051070630c272a9be87bef617d; patches:3 + +To recreate 30bd..e5, one have to take content of 9429..e0, not its p1 f1db..5e + */ + public boolean element(GroupElement ge) { + emptyChangelog = false; + HgChangelog changelog = hgRepo.getChangelog(); + try { + if (prevRevContent == null) { + if (NULL.equals(ge.firstParent()) && NULL.equals(ge.secondParent())) { + prevRevContent = new ByteArrayDataAccess(new byte[0]); + } else { + final Nodeid base = ge.firstParent(); + if (!changelog.isKnown(base) /*only first parent, that's Bundle contract*/) { + 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 + // extra byte[] (inside ByteArrayChannel) duplication just for the sake of subsequent ByteArrayDataChannel wrap. + prevRevContent = new ByteArrayDataAccess(bac.toArray()); + } + } + // + 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()); + } + ByteArrayDataAccess csetDataAccess = new ByteArrayDataAccess(csetContent); + RawChangeset cs = RawChangeset.parse(csetDataAccess); + inspector.next(revisionIndex++, ge.node(), cs); + prevRevContent.done(); + prevRevContent = csetDataAccess.reset(); + } catch (CancelledException ex) { + return false; + } catch (Exception ex) { + throw new HgBadStateException(ex); // FIXME + } + return true; + } + + public void manifestStart() {} + public void manifestEnd() {} + public void fileStart(String name) {} + public void fileEnd(String name) {} + + }; + inspectChangelog(bundleInsp); + } + + public void dump() throws IOException { + Dump dump = new Dump(); + inspectAll(dump); + System.out.println("Total files:" + dump.names.size()); + for (String s : dump.names) { + System.out.println(s); + } + } + + // callback to minimize amount of Strings and Nodeids instantiated + public interface Inspector { + void changelogStart(); + + void changelogEnd(); + + void manifestStart(); + + void manifestEnd(); + + void fileStart(String name); + + void fileEnd(String name); + + /** + * XXX desperately need exceptions here + * @param element data element, instance might be reused, don't keep a reference to it or its raw data + * @return true to continue + */ + boolean element(GroupElement element); + } + + public static class Dump implements Inspector { + public final LinkedList names = new LinkedList(); + + public void changelogStart() { + System.out.println("Changelog group"); + } + + public void changelogEnd() { + } + + public void manifestStart() { + System.out.println("Manifest group"); + } + + public void manifestEnd() { + } + + public void fileStart(String name) { + names.add(name); + System.out.println(name); + } + + public void fileEnd(String name) { + } + + public boolean element(GroupElement ge) { + try { + System.out.printf(" %s %s %s %s; patches:%d\n", ge.node(), ge.firstParent(), ge.secondParent(), ge.cset(), ge.patches().size()); + } catch (Exception ex) { + ex.printStackTrace(); // FIXME + } + return true; + } + } + + public void inspectChangelog(Inspector inspector) throws IOException { + if (inspector == null) { + throw new IllegalArgumentException(); + } + DataAccess da = getDataStream(); + try { + internalInspectChangelog(da, inspector); + } finally { + da.done(); + } + } + + public void inspectManifest(Inspector inspector) throws IOException { + if (inspector == null) { + throw new IllegalArgumentException(); + } + DataAccess da = getDataStream(); + try { + if (da.isEmpty()) { + return; + } + skipGroup(da); // changelog + internalInspectManifest(da, inspector); + } finally { + da.done(); + } + } + + public void inspectFiles(Inspector inspector) throws IOException { + if (inspector == null) { + throw new IllegalArgumentException(); + } + DataAccess da = getDataStream(); + try { + if (da.isEmpty()) { + return; + } + skipGroup(da); // changelog + if (da.isEmpty()) { + return; + } + skipGroup(da); // manifest + internalInspectFiles(da, inspector); + } finally { + da.done(); + } + } + + public void inspectAll(Inspector inspector) throws IOException { + if (inspector == null) { + throw new IllegalArgumentException(); + } + DataAccess da = getDataStream(); + try { + internalInspectChangelog(da, inspector); + internalInspectManifest(da, inspector); + internalInspectFiles(da, inspector); + } finally { + da.done(); + } + } + + private void internalInspectChangelog(DataAccess da, Inspector inspector) throws IOException { + if (da.isEmpty()) { + return; + } + inspector.changelogStart(); + readGroup(da, inspector); + inspector.changelogEnd(); + } + + private void internalInspectManifest(DataAccess da, Inspector inspector) throws IOException { + if (da.isEmpty()) { + return; + } + inspector.manifestStart(); + readGroup(da, inspector); + inspector.manifestEnd(); + } + + private void internalInspectFiles(DataAccess da, Inspector inspector) throws IOException { + while (!da.isEmpty()) { + int fnameLen = da.readInt(); + if (fnameLen <= 4) { + break; // null chunk, the last one. + } + byte[] fnameBuf = new byte[fnameLen - 4]; + da.readBytes(fnameBuf, 0, fnameBuf.length); + String name = new String(fnameBuf); + inspector.fileStart(name); + readGroup(da, inspector); + inspector.fileEnd(name); + } + } + + private static void readGroup(DataAccess da, Inspector inspector) throws IOException { + int len = da.readInt(); + boolean good2go = true; + while (len > 4 && !da.isEmpty() && good2go) { + byte[] nb = new byte[80]; + da.readBytes(nb, 0, 80); + int dataLength = len - 84 /* length field + 4 nodeids */; + byte[] data = new byte[dataLength]; + da.readBytes(data, 0, dataLength); + DataAccess slice = new ByteArrayDataAccess(data); // XXX in fact, may pass a slicing DataAccess. + // Just need to make sure that we seek to proper location afterwards (where next GroupElement starts), + // regardless whether that slice has read it or not. + GroupElement ge = new GroupElement(nb, slice); + good2go = inspector.element(ge); + slice.done(); // BADA doesn't implement done(), but it could (e.g. free array) + /// and we'd better tell it we are not going to use it any more. However, it's important to ensure Inspector + // implementations out there do not retain GroupElement.rawData() + len = da.isEmpty() ? 0 : da.readInt(); + } + // need to skip up to group end if inspector told he don't want to continue with the group, + // because outer code may try to read next group immediately as we return back. + while (len > 4 && !da.isEmpty()) { + da.skip(len - 4 /* length field */); + len = da.isEmpty() ? 0 : da.readInt(); + } + } + + private static void skipGroup(DataAccess da) throws IOException { + int len = da.readInt(); + while (len > 4 && !da.isEmpty()) { + da.skip(len - 4); // sizeof(int) + len = da.isEmpty() ? 0 : da.readInt(); + } + } + + public static class GroupElement { + private final byte[] header; // byte[80] takes 120 bytes, 4 Nodeids - 192 + private final DataAccess dataAccess; + private List patches; + + GroupElement(byte[] fourNodeids, DataAccess rawDataAccess) { + assert fourNodeids != null && fourNodeids.length == 80; + header = fourNodeids; + dataAccess = rawDataAccess; + } + + public Nodeid node() { + return Nodeid.fromBinary(header, 0); + } + + public Nodeid firstParent() { + return Nodeid.fromBinary(header, 20); + } + + public Nodeid secondParent() { + return Nodeid.fromBinary(header, 40); + } + + public Nodeid cset() { // cs seems to be changeset + return Nodeid.fromBinary(header, 60); + } + + public DataAccess rawData() { + return dataAccess; + } + + public List patches() throws IOException { + if (patches == null) { + dataAccess.reset(); + LinkedList p = new LinkedList(); + while (!dataAccess.isEmpty()) { + RevlogStream.PatchRecord pr = RevlogStream.PatchRecord.read(dataAccess); + p.add(pr); + } + patches = p; + } + return patches; + } + + public byte[] apply(DataAccess baseContent) throws IOException { + return RevlogStream.apply(baseContent, -1, patches()); + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/HgChangelog.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/HgChangelog.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2010-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.repo; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.Formatter; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TimeZone; + +import org.tmatesoft.hg.core.HgBadStateException; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.DataAccess; +import org.tmatesoft.hg.internal.Pool; +import org.tmatesoft.hg.internal.RevlogStream; + +/** + * Representation of the Mercurial changelog file (list of ChangeSets) + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgChangelog extends Revlog { + + /* package-local */HgChangelog(HgRepository hgRepo, RevlogStream content) { + super(hgRepo, content); + } + + public void all(final HgChangelog.Inspector inspector) { + range(0, getLastRevision(), inspector); + } + + public void range(int start, int end, final HgChangelog.Inspector inspector) { + if (inspector == null) { + throw new IllegalArgumentException(); + } + content.iterate(start, end, true, new RawCsetParser(inspector)); + } + + public List range(int start, int end) { + final RawCsetCollector c = new RawCsetCollector(end - start + 1); + range(start, end, c); + return c.result; + } + + public void range(final HgChangelog.Inspector inspector, final int... revisions) { + if (revisions == null || revisions.length == 0) { + return; + } + RevlogStream.Inspector i = new RevlogStream.Inspector() { + private final RawCsetParser delegate = new RawCsetParser(inspector); + + public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) { + if (Arrays.binarySearch(revisions, revisionNumber) >= 0) { + delegate.next(revisionNumber, actualLen, baseRevision, linkRevision, parent1Revision, parent2Revision, nodeid, da); + } + } + }; + Arrays.sort(revisions); + content.iterate(revisions[0], revisions[revisions.length - 1], true, i); + } + + 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); + } + + /** + * Entry in the Changelog + */ + public static class RawChangeset implements Cloneable /* for those that would like to keep a copy */{ + // TODO immutable + private/* final */Nodeid manifest; + private String user; + private String comment; + private List files; // unmodifiable collection (otherwise #files() and implicit #clone() shall be revised) + private Date time; + private int timezone; + // http://mercurial.selenic.com/wiki/PruningDeadBranches - Closing changesets can be identified by close=1 in the changeset's extra field. + private Map extras; + + /** + * @see mercurial/changelog.py:read() + * + *
+		 *         format used:
+		 *         nodeid\n        : manifest node in ascii
+		 *         user\n          : user, no \n or \r allowed
+		 *         time tz extra\n : date (time is int or float, timezone is int)
+		 *                         : extra is metadatas, encoded and separated by '\0'
+		 *                         : older versions ignore it
+		 *         files\n\n       : files modified by the cset, no \n or \r allowed
+		 *         (.*)            : comment (free text, ideally utf-8)
+		 * 
+		 *         changelog v0 doesn't use extra
+		 * 
+ */ + private RawChangeset() { + } + + public Nodeid manifest() { + return manifest; + } + + public String user() { + return user; + } + + public String comment() { + return comment; + } + + public List files() { + return files; + } + + public Date date() { + return time; + } + + /** + * @return time zone value, as is, positive for Western Hemisphere. + */ + public int timezone() { + return timezone; + } + + public String dateString() { + // XXX keep once formatted? Perhaps, there's faster way to set up calendar/time zone? + StringBuilder sb = new StringBuilder(30); + Formatter f = new Formatter(sb, Locale.US); + TimeZone tz = TimeZone.getTimeZone(TimeZone.getAvailableIDs(timezone * 1000)[0]); + // apparently timezone field records number of seconds time differs from UTC, + // i.e. value to substract from time to get UTC time. Calendar seems to add + // timezone offset to UTC, instead, hence sign change. +// tz.setRawOffset(timezone * -1000); + Calendar c = Calendar.getInstance(tz, Locale.US); + c.setTime(time); + f.format("%ta % extras() { + return extras; + } + + public String branch() { + return extras.get("branch"); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Changeset {"); + sb.append("User: ").append(user).append(", "); + sb.append("Comment: ").append(comment).append(", "); + sb.append("Manifest: ").append(manifest).append(", "); + sb.append("Date: ").append(time).append(", "); + sb.append("Files: ").append(files.size()); + for (String s : files) { + sb.append(", ").append(s); + } + if (extras != null) { + sb.append(", Extra: ").append(extras); + } + sb.append("}"); + return sb.toString(); + } + + @Override + public RawChangeset clone() { + try { + return (RawChangeset) super.clone(); + } catch (CloneNotSupportedException ex) { + throw new InternalError(ex.toString()); + } + } + + public static RawChangeset parse(DataAccess da) { + try { + byte[] data = da.byteArray(); + RawChangeset rv = new RawChangeset(); + rv.init(data, 0, data.length, null); + return rv; + } catch (IOException ex) { + throw new HgBadStateException(ex); // FIXME "Error reading changeset data" + } + } + + // @param usersPool - it's likely user names get repeated again and again throughout repository. can be null + /* package-local */void init(byte[] data, int offset, int length, Pool usersPool) { + final int bufferEndIndex = offset + length; + final byte lineBreak = (byte) '\n'; + int breakIndex1 = indexOf(data, lineBreak, offset, bufferEndIndex); + if (breakIndex1 == -1) { + throw new IllegalArgumentException("Bad Changeset data"); + } + Nodeid _nodeid = Nodeid.fromAscii(data, 0, breakIndex1); + int breakIndex2 = indexOf(data, lineBreak, breakIndex1 + 1, bufferEndIndex); + if (breakIndex2 == -1) { + throw new IllegalArgumentException("Bad Changeset data"); + } + String _user = new String(data, breakIndex1 + 1, breakIndex2 - breakIndex1 - 1); + if (usersPool != null) { + _user = usersPool.unify(_user); + } + int breakIndex3 = indexOf(data, lineBreak, breakIndex2 + 1, bufferEndIndex); + if (breakIndex3 == -1) { + throw new IllegalArgumentException("Bad Changeset data"); + } + String _timeString = new String(data, breakIndex2 + 1, breakIndex3 - breakIndex2 - 1); + int space1 = _timeString.indexOf(' '); + if (space1 == -1) { + throw new IllegalArgumentException("Bad Changeset data"); + } + int space2 = _timeString.indexOf(' ', space1 + 1); + if (space2 == -1) { + space2 = _timeString.length(); + } + long unixTime = Long.parseLong(_timeString.substring(0, space1)); // XXX Float, perhaps + int _timezone = Integer.parseInt(_timeString.substring(space1 + 1, space2)); + // XXX not sure need to add timezone here - I can't figure out whether Hg keeps GMT time, and records timezone just for info, or unixTime is taken local + // on commit and timezone is recorded to adjust it to UTC. + Date _time = new Date(unixTime * 1000); + String _extras = space2 < _timeString.length() ? _timeString.substring(space2 + 1) : null; + Map _extrasMap; + if (_extras == null) { + _extrasMap = Collections.singletonMap("branch", "default"); + } else { + _extrasMap = new HashMap(); + for (String pair : _extras.split("\00")) { + int eq = pair.indexOf(':'); + // FIXME need to decode key/value, @see changelog.py:decodeextra + _extrasMap.put(pair.substring(0, eq), pair.substring(eq + 1)); + } + if (!_extrasMap.containsKey("branch")) { + _extrasMap.put("branch", "default"); + } + _extrasMap = Collections.unmodifiableMap(_extrasMap); + } + + // + int lastStart = breakIndex3 + 1; + int breakIndex4 = indexOf(data, lineBreak, lastStart, bufferEndIndex); + ArrayList _files = null; + if (breakIndex4 > lastStart) { + // if breakIndex4 == lastStart, we already found \n\n and hence there are no files (e.g. merge revision) + _files = new ArrayList(5); + while (breakIndex4 != -1 && breakIndex4 + 1 < bufferEndIndex) { + _files.add(new String(data, lastStart, breakIndex4 - lastStart)); + lastStart = breakIndex4 + 1; + if (data[breakIndex4 + 1] == lineBreak) { + // found \n\n + break; + } else { + breakIndex4 = indexOf(data, lineBreak, lastStart, bufferEndIndex); + } + } + if (breakIndex4 == -1 || breakIndex4 >= bufferEndIndex) { + throw new IllegalArgumentException("Bad Changeset data"); + } + } else { + breakIndex4--; + } + String _comment; + try { + _comment = new String(data, breakIndex4 + 2, bufferEndIndex - breakIndex4 - 2, "UTF-8"); + // FIXME respect ui.fallbackencoding and try to decode if set + } catch (UnsupportedEncodingException ex) { + _comment = ""; + throw new IllegalStateException("Could hardly happen"); + } + // change this instance at once, don't leave it partially changes in case of error + this.manifest = _nodeid; + this.user = _user; + this.time = _time; + this.timezone = _timezone; + this.files = _files == null ? Collections. emptyList() : Collections.unmodifiableList(_files); + this.comment = _comment; + this.extras = _extrasMap; + } + + private static int indexOf(byte[] src, byte what, int startOffset, int endIndex) { + for (int i = startOffset; i < endIndex; i++) { + if (src[i] == what) { + return i; + } + } + return -1; + } + } + + private static class RawCsetCollector implements Inspector { + final ArrayList result; + + public RawCsetCollector(int count) { + result = new ArrayList(count > 0 ? count : 5); + } + + public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { + result.add(cset.clone()); + } + } + + private static class RawCsetParser implements RevlogStream.Inspector { + + private final Inspector inspector; + private final Pool usersPool; + private final RawChangeset cset = new RawChangeset(); + + public RawCsetParser(HgChangelog.Inspector delegate) { + assert delegate != null; + inspector = delegate; + usersPool = new Pool(); + } + + 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); + } catch (Exception ex) { + throw new HgBadStateException(ex); // FIXME exception handling + } + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/HgDataFile.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/HgDataFile.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,390 @@ +/* + * Copyright (c) 2010-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.repo; + +import static org.tmatesoft.hg.repo.HgInternals.wrongLocalRevision; +import static org.tmatesoft.hg.repo.HgRepository.*; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.TreeMap; + +import org.tmatesoft.hg.core.HgDataStreamException; +import org.tmatesoft.hg.core.HgException; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.DataAccess; +import org.tmatesoft.hg.internal.FilterByteChannel; +import org.tmatesoft.hg.internal.RevlogStream; +import org.tmatesoft.hg.util.ByteChannel; +import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.util.Path; + + + +/** + * ? name:HgFileNode? + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgDataFile extends Revlog { + + // absolute from repo root? + // slashes, unix-style? + // repo location agnostic, just to give info to user, not to access real storage + private final Path path; + private Metadata metadata; // get initialized on first access to file content. + + /*package-local*/HgDataFile(HgRepository hgRepo, Path filePath, RevlogStream content) { + super(hgRepo, content); + path = filePath; + } + + /*package-local*/HgDataFile(HgRepository hgRepo, Path filePath) { + super(hgRepo); + path = filePath; + } + + // 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. + 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") + public Path getPath() { + return path; // hgRepo.backresolve(this) -> name? In this case, what about hashed long names? + } + + public int length(Nodeid nodeid) { + return content.dataLength(getLocalRevision(nodeid)); + } + + public void workingCopy(ByteChannel sink) throws IOException, CancelledException { + throw HgRepository.notImplemented(); + } + +// 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, IOException, CancelledException { + content(revision, new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath()))); + } + + // for data files need to check heading of the file content for possible metadata + // @see http://mercurial.selenic.com/wiki/FileFormats#data.2BAC8- + public void content(int revision, ByteChannel sink) throws HgDataStreamException, IOException, CancelledException { + if (revision == TIP) { + revision = getLastRevision(); + } + if (revision == WORKING_COPY) { + workingCopy(sink); + return; + } + if (wrongLocalRevision(revision) || revision == BAD_REVISION) { + throw new IllegalArgumentException(String.valueOf(revision)); + } + if (sink == null) { + throw new IllegalArgumentException(); + } + if (metadata == null) { + metadata = new Metadata(); + } + ContentPipe insp; + if (metadata.none(revision)) { + insp = new ContentPipe(sink, 0); + } else if (metadata.known(revision)) { + insp = new ContentPipe(sink, metadata.dataOffset(revision)); + } else { + // do not know if there's metadata + insp = new MetadataContentPipe(sink, metadata); + } + insp.checkCancelled(); + super.content.iterate(revision, revision, true, insp); + try { + insp.checkFailed(); + } catch (HgDataStreamException ex) { + throw ex; + } catch (HgException ex) { + // shall not happen, unless we changed ContentPipe or its subclass + throw new HgDataStreamException(ex.getClass().getName(), ex); + } + } + + public void history(HgChangelog.Inspector inspector) { + history(0, getLastRevision(), inspector); + } + + public void history(int start, int end, HgChangelog.Inspector inspector) { + if (!exists()) { + throw new IllegalStateException("Can't get history of invalid repository file node"); + } + final int last = getLastRevision(); + if (start < 0 || start > last) { + throw new IllegalArgumentException(); + } + if (end == TIP) { + end = last; + } else if (end < start || end > last) { + throw new IllegalArgumentException(); + } + final int[] commitRevisions = new int[end - start + 1]; + RevlogStream.Inspector insp = new RevlogStream.Inspector() { + int count = 0; + + public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) { + commitRevisions[count++] = linkRevision; + } + }; + content.iterate(start, end, false, insp); + getRepo().getChangelog().range(inspector, commitRevisions); + } + + // for a given local revision of the file, find out local revision in the changelog + public int getChangesetLocalRevision(int revision) { + return content.linkRevision(revision); + } + + public Nodeid getChangesetRevision(Nodeid nid) { + int changelogRevision = getChangesetLocalRevision(getLocalRevision(nid)); + return getRepo().getChangelog().getRevision(changelogRevision); + } + + public boolean isCopy() throws HgDataStreamException { + if (metadata == null || !metadata.checked(0)) { + // content() always initializes metadata. + // FIXME 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 + // (compared to command-line counterpart of 190ms) + try { + content(0, new ByteChannel() { // No-op channel + public int write(ByteBuffer buffer) throws IOException, CancelledException { + // pretend we consumed whole buffer +// int rv = buffer.remaining(); +// buffer.position(buffer.limit()); +// return rv; + throw new CancelledException(); + } + }); + } catch (CancelledException ex) { + // it's ok, we did that + } catch (Exception ex) { + throw new HgDataStreamException("Can't initialize metadata", ex); + } + } + if (!metadata.known(0)) { + return false; + } + return metadata.find(0, "copy") != null; + } + + public Path getCopySourceName() throws HgDataStreamException { + 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 { + if (isCopy()) { + return Nodeid.fromAscii(metadata.find(0, "copyrev")); // XXX reuse/cache Nodeid + } + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getClass().getSimpleName()); + sb.append('('); + sb.append(getPath()); + sb.append(')'); + return sb.toString(); + } + + private static final class MetadataEntry { + private final String entry; + private final int valueStart; + /*package-local*/MetadataEntry(String key, String value) { + entry = key + value; + valueStart = key.length(); + } + /*package-local*/boolean matchKey(String key) { + return key.length() == valueStart && entry.startsWith(key); + } +// uncomment once/if needed +// public String key() { +// return entry.substring(0, valueStart); +// } + public String value() { + return entry.substring(valueStart); + } + } + + private static class Metadata { + // XXX sparse array needed + private final TreeMap offsets = new TreeMap(); + private final TreeMap entries = new TreeMap(); + + private final Integer NONE = new Integer(-1); // do not duplicate -1 integers at least within single file (don't want statics) + + // true when there's metadata for given revision + boolean known(int revision) { + Integer i = offsets.get(revision); + return i != null && NONE != i; + } + + // true when revision has been checked for metadata presence. + public boolean checked(int revision) { + return offsets.containsKey(revision); + } + + // true when revision has been checked and found not having any metadata + boolean none(int revision) { + Integer i = offsets.get(revision); + return i == NONE; + } + + // mark revision as having no metadata. + void recordNone(int revision) { + Integer i = offsets.get(revision); + if (i == NONE) { + return; // already there + } + if (i != null) { + throw new IllegalStateException(String.format("Trying to override Metadata state for revision %d (known offset: %d)", revision, i)); + } + offsets.put(revision, NONE); + } + + // since this is internal class, callers are supposed to ensure arg correctness (i.e. ask known() before) + int dataOffset(int revision) { + return offsets.get(revision); + } + void add(int revision, int dataOffset, Collection e) { + assert !offsets.containsKey(revision); + offsets.put(revision, dataOffset); + entries.put(revision, e.toArray(new MetadataEntry[e.size()])); + } + String find(int revision, String key) { + for (MetadataEntry me : entries.get(revision)) { + if (me.matchKey(key)) { + return me.value(); + } + } + return null; + } + } + + private static class MetadataContentPipe extends ContentPipe { + + private final Metadata metadata; + + public MetadataContentPipe(ByteChannel sink, Metadata _metadata) { + super(sink, 0); + metadata = _metadata; + } + + @Override + protected void prepare(int revisionNumber, DataAccess da) throws HgException, IOException { + final int daLength = da.length(); + if (daLength < 4 || da.readByte() != 1 || da.readByte() != 10) { + metadata.recordNone(revisionNumber); + da.reset(); + return; + } + int lastEntryStart = 2; + int lastColon = -1; + ArrayList _metadata = new ArrayList(); + // XXX in fact, need smth like ByteArrayBuilder, similar to StringBuilder, + // which can't be used here because we can't convert bytes to chars as we read them + // (there might be multi-byte encoding), and we need to collect all bytes before converting to string + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + String key = null, value = null; + boolean byteOne = false; + for (int i = 2; i < daLength; i++) { + byte b = da.readByte(); + if (b == '\n') { + if (byteOne) { // i.e. \n follows 1 + lastEntryStart = i+1; + // XXX is it possible to have here incomplete key/value (i.e. if last pair didn't end with \n) + break; + } + if (key == null || lastColon == -1 || i <= lastColon) { + throw new IllegalStateException(); // FIXME log instead and record null key in the metadata. Ex just to fail fast during dev + } + value = new String(bos.toByteArray()).trim(); + bos.reset(); + _metadata.add(new MetadataEntry(key, value)); + key = value = null; + lastColon = -1; + lastEntryStart = i+1; + continue; + } + // byteOne has to be consumed up to this line, if not jet, consume it + if (byteOne) { + // insert 1 we've read on previous step into the byte builder + bos.write(1); + // fall-through to consume current byte + byteOne = false; + } + if (b == (int) ':') { + assert value == null; + key = new String(bos.toByteArray()); + bos.reset(); + lastColon = i; + } else if (b == 1) { + byteOne = true; + } else { + bos.write(b); + } + } + _metadata.trimToSize(); + metadata.add(revisionNumber, lastEntryStart, _metadata); + if (da.isEmpty() || !byteOne) { + throw new HgDataStreamException(String.format("Metadata for revision %d is not closed properly", revisionNumber), null); + } + // da is in prepared state (i.e. we consumed all bytes up to metadata end). + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/HgDirstate.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/HgDirstate.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2010-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.repo; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.TreeSet; + +import org.tmatesoft.hg.internal.DataAccess; +import org.tmatesoft.hg.internal.DataAccessProvider; +import org.tmatesoft.hg.util.Path; + + +/** + * @see http://mercurial.selenic.com/wiki/DirState + * @see http://mercurial.selenic.com/wiki/FileFormats#dirstate + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +class HgDirstate { + + private final DataAccessProvider accessProvider; + private final File dirstateFile; + // deliberate String, not Path as it seems useless to keep Path here + private Map normal; + private Map added; + private Map removed; + private Map merged; + + /*package-local*/ HgDirstate() { + // empty instance + accessProvider = null; + dirstateFile = null; + } + + public HgDirstate(DataAccessProvider dap, File dirstate) { + accessProvider = dap; + dirstateFile = dirstate; + } + + private void read() { + normal = added = removed = merged = Collections.emptyMap(); + if (dirstateFile == null || !dirstateFile.exists()) { + return; + } + DataAccess da = accessProvider.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 { + // XXX skip(40) if we don't need these? + byte[] parents = new byte[40]; + da.readBytes(parents, 0, 40); + parents = null; + do { + final byte state = da.readByte(); + final int fmode = da.readInt(); + final int size = da.readInt(); + final int time = da.readInt(); + final int nameLen = da.readInt(); + String fn1 = null, fn2 = null; + byte[] name = new byte[nameLen]; + da.readBytes(name, 0, nameLen); + for (int i = 0; i < nameLen; i++) { + if (name[i] == 0) { + fn1 = new String(name, 0, i, "UTF-8"); // XXX unclear from documentation what encoding is used there + fn2 = new String(name, i+1, nameLen - i - 1, "UTF-8"); // need to check with different system codepages + break; + } + } + if (fn1 == null) { + fn1 = new String(name); + } + Record r = new Record(fmode, size, time, fn1, fn2); + if (state == 'n') { + normal.put(r.name1, r); + } else if (state == 'a') { + added.put(r.name1, r); + } else if (state == 'r') { + removed.put(r.name1, r); + } else if (state == 'm') { + merged.put(r.name1, r); + } else { + // FIXME log error? + } + } while (!da.isEmpty()); + } catch (IOException ex) { + ex.printStackTrace(); // FIXME log error, clean dirstate? + } finally { + da.done(); + } + } + + // new, modifiable collection + /*package-local*/ TreeSet all() { + read(); + TreeSet rv = new TreeSet(); + @SuppressWarnings("unchecked") + Map[] all = new Map[] { normal, added, removed, merged }; + for (int i = 0; i < all.length; i++) { + for (Record r : all[i].values()) { + rv.add(r.name1); + } + } + return rv; + } + + /*package-local*/ Record checkNormal(Path fname) { + return normal.get(fname.toString()); + } + + /*package-local*/ Record checkAdded(Path fname) { + return added.get(fname.toString()); + } + /*package-local*/ Record checkRemoved(Path fname) { + return removed.get(fname.toString()); + } + /*package-local*/ Record checkRemoved(String fname) { + return removed.get(fname); + } + /*package-local*/ Record checkMerged(Path fname) { + return merged.get(fname.toString()); + } + + + + + /*package-local*/ void dump() { + read(); + @SuppressWarnings("unchecked") + Map[] all = new Map[] { normal, added, removed, merged }; + char[] x = new char[] {'n', 'a', 'r', 'm' }; + for (int i = 0; i < all.length; i++) { + for (Record r : all[i].values()) { + System.out.printf("%c %3o%6d %30tc\t\t%s", x[i], r.mode, r.size, (long) r.time * 1000, r.name1); + if (r.name2 != null) { + System.out.printf(" --> %s", r.name2); + } + System.out.println(); + } + System.out.println(); + } + } + + /*package-local*/ static class Record { + final int mode; + final int size; + final int time; + final String name1; + final String name2; + + public Record(int fmode, int fsize, int ftime, String name1, String name2) { + mode = fmode; + size = fsize; + time = ftime; + this.name1 = name1; + this.name2 = name2; + + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/HgIgnore.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/HgIgnore.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2010-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.repo; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.regex.Pattern; + +import org.tmatesoft.hg.util.Path; + +/** + * Handling of ignored paths according to .hgignore configuration + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgIgnore { + + private List entries; + + HgIgnore() { + entries = Collections.emptyList(); + } + + /* package-local */void read(File hgignoreFile) throws IOException { + if (!hgignoreFile.exists()) { + return; + } + ArrayList result = new ArrayList(entries); // start with existing + String syntax = "regex"; // or "glob" + BufferedReader fr = new BufferedReader(new FileReader(hgignoreFile)); + String line; + while ((line = fr.readLine()) != null) { + line = line.trim(); + if (line.startsWith("syntax:")) { + syntax = line.substring("syntax:".length()).trim(); + if (!"regex".equals(syntax) && !"glob".equals(syntax)) { + throw new IllegalStateException(line); + } + } else if (line.length() > 0) { + // shall I account for local paths in the file (i.e. + // back-slashed on windows)? + int x; + if ((x = line.indexOf('#')) >= 0) { + line = line.substring(0, x).trim(); + if (line.length() == 0) { + continue; + } + } + if ("glob".equals(syntax)) { + // hgignore(5) + // (http://www.selenic.com/mercurial/hgignore.5.html) says slashes '\' are escape characters, + // hence no special treatment of Windows path + // however, own attempts make me think '\' on Windows are not treated as escapes + line = glob2regex(line); + } + result.add(Pattern.compile(line)); // case-sensitive + } + } + result.trimToSize(); + entries = result; + } + + // note, #isIgnored(), even if queried for directories and returned positive reply, may still get + // a file from that ignored folder to get examined. Thus, patterns like "bin" shall match not only a folder, + // but any file under that folder as well + // Alternatively, file walker may memorize folder is ignored and uses this information for all nested files. However, + // this approach would require walker (a) return directories (b) provide nesting information. This may become + // troublesome when one walks not over io.File, but Eclipse's IResource or any other custom VFS. + // + // + // might be interesting, although looks like of no direct use in my case + // @see http://stackoverflow.com/questions/1247772/is-there-an-equivalent-of-java-util-regex-for-glob-type-patterns + private String glob2regex(String line) { + assert line.length() > 0; + StringBuilder sb = new StringBuilder(line.length() + 10); + sb.append('^'); // help avoid matcher.find() to match 'bin' pattern in the middle of the filename + int start = 0, end = line.length() - 1; + // '*' at the beginning and end of a line are useless for Pattern + // XXX although how about **.txt - such globs can be seen in a config, are they valid for HgIgnore? + while (start <= end && line.charAt(start) == '*') start++; + while (end > start && line.charAt(end) == '*') end--; + + for (int i = start; i <= end; i++) { + char ch = line.charAt(i); + if (ch == '.' || ch == '\\') { + sb.append('\\'); + } else if (ch == '?') { + // simple '.' substitution might work out, however, more formally + // a char class seems more appropriate to avoid accidentally + // matching a subdirectory with ? char (i.e. /a/b?d against /a/bad, /a/bed and /a/b/d) + // @see http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_13_03 + // quote: "The slash character in a pathname shall be explicitly matched by using one or more slashes in the pattern; + // it shall neither be matched by the asterisk or question-mark special characters nor by a bracket expression" + sb.append("[^/]"); + continue; + } else if (ch == '*') { + sb.append("[^/]*?"); + continue; + } + sb.append(ch); + } + return sb.toString(); + } + + // TODO use PathGlobMatcher + public boolean isIgnored(Path path) { + for (Pattern p : entries) { + if (p.matcher(path).find()) { + return true; + } + } + return false; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/HgInternals.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/HgInternals.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,94 @@ +/* + * 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.repo; + +import static org.tmatesoft.hg.repo.HgRepository.*; + +import java.io.File; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import org.tmatesoft.hg.internal.ConfigFile; +import org.tmatesoft.hg.util.Path; + + +/** + * DO NOT USE THIS CLASS, INTENDED FOR TESTING PURPOSES. + * + * Debug helper, to access otherwise restricted (package-local) methods + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + + */ +public class HgInternals { + + private final HgRepository repo; + + public HgInternals(HgRepository hgRepo) { + repo = hgRepo; + } + + public void dumpDirstate() { + repo.loadDirstate().dump(); + } + + public boolean[] checkIgnored(String... toCheck) { + HgIgnore ignore = repo.getIgnore(); + boolean[] rv = new boolean[toCheck.length]; + for (int i = 0; i < toCheck.length; i++) { + rv[i] = ignore.isIgnored(Path.create(toCheck[i])); + } + return rv; + } + + public File getRepositoryDir() { + return repo.getRepositoryRoot(); + } + + public ConfigFile getRepoConfig() { + return repo.getConfigFile(); + } + + // in fact, need a setter for this anyway, shall move to internal.Internals perhaps? + public String getNextCommitUsername() { + String hgUser = System.getenv("HGUSER"); + if (hgUser != null && hgUser.trim().length() > 0) { + return hgUser.trim(); + } + String configValue = getRepoConfig().getString("ui", "username", null); + if (configValue != null) { + return configValue; + } + String email = System.getenv("EMAIL"); + if (email != null && email.trim().length() > 0) { + return email; + } + String username = System.getProperty("user.name"); + try { + String hostname = InetAddress.getLocalHost().getHostName(); + return username + '@' + hostname; + } catch (UnknownHostException ex) { + return username; + } + } + + // Convenient check of local revision number for validity (not all negative values are wrong as long as we use negative constants) + public static boolean wrongLocalRevision(int rev) { + return rev < 0 && rev != TIP && rev != WORKING_COPY && rev != BAD_REVISION; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/HgLookup.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/HgLookup.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2010-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.repo; + +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.HgException; +import org.tmatesoft.hg.internal.ConfigFile; +import org.tmatesoft.hg.internal.DataAccessProvider; +import org.tmatesoft.hg.internal.Internals; + +/** + * Utility methods to find Mercurial repository at a given location + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgLookup { + + private ConfigFile globalCfg; + + public HgRepository detectFromWorkingDir() throws HgException { + return detect(System.getProperty("user.dir")); + } + + public HgRepository detect(String location) throws HgException { + return detect(new File(location)); + } + + // look up in specified location and above + public HgRepository detect(File location) throws HgException { + 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) { + // return invalid repository + return new HgRepository(location.getPath()); + } + try { + String repoPath = repository.getParentFile().getCanonicalPath(); + return new HgRepository(repoPath, repository); + } catch (IOException ex) { + throw new HgException(location.toString(), ex); + } + } + + public HgBundle loadBundle(File location) throws HgException { + if (location == null || !location.canRead()) { + throw new IllegalArgumentException(); + } + return new HgBundle(new DataAccessProvider(), location).link(); + } + + /** + * Try to instantiate remote server. + * @param key either URL or a key from configuration file that points to remote server + * @param hgRepo NOT USED YET local repository that may have extra config, or default remote location + * @return an instance featuring access to remote repository, check {@link HgRemoteRepository#isInvalid()} before actually using it + * @throws HgBadArgumentException if anything is wrong with the remote server's URL + */ + public HgRemoteRepository detectRemote(String key, HgRepository hgRepo) throws HgBadArgumentException { + URL url; + Exception toReport; + try { + url = new URL(key); + toReport = null; + } catch (MalformedURLException ex) { + url = null; + toReport = ex; + } + if (url == null) { + String server = getGlobalConfig().getSection("paths").get(key); + if (server == null) { + throw new HgBadArgumentException(String.format("Can't find server %s specification in the config", key), toReport); + } + try { + url = new URL(server); + } catch (MalformedURLException ex) { + throw new HgBadArgumentException(String.format("Found %s server spec in the config, but failed to initialize with it", key), ex); + } + } + return new HgRemoteRepository(url); + } + + public HgRemoteRepository detect(URL url) throws HgException { + if (url == null) { + throw new IllegalArgumentException(); + } + if (Boolean.FALSE.booleanValue()) { + throw HgRepository.notImplemented(); + } + return new HgRemoteRepository(url); + } + + private ConfigFile getGlobalConfig() { + if (globalCfg == null) { + globalCfg = new Internals().newConfigFile(); + globalCfg.addLocation(new File(System.getProperty("user.home"), ".hgrc")); + } + return globalCfg; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/HgManifest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/HgManifest.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2010-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.repo; + +import java.io.IOException; + +import org.tmatesoft.hg.core.HgBadStateException; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.DataAccess; +import org.tmatesoft.hg.internal.Pool; +import org.tmatesoft.hg.internal.RevlogStream; + + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgManifest extends Revlog { + + /*package-local*/ HgManifest(HgRepository hgRepo, RevlogStream content) { + super(hgRepo, content); + } + + public void walk(int start, int end, final Inspector inspector) { + if (inspector == null) { + throw new IllegalArgumentException(); + } + content.iterate(start, end, true, new ManifestParser(inspector)); + } + + public interface Inspector { + boolean begin(int revision, Nodeid nid); + boolean next(Nodeid nid, String fname, String flags); + boolean end(int revision); + } + + private static class ManifestParser implements RevlogStream.Inspector { + private boolean gtg = true; // good to go + private final Inspector inspector; + private final Pool nodeidPool; + private final Pool fnamePool; + private final Pool flagsPool; + + public ManifestParser(Inspector delegate) { + assert delegate != null; + inspector = delegate; + nodeidPool = new Pool(); + fnamePool = new Pool(); + flagsPool = new Pool(); + } + + public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) { + if (!gtg) { + return; + } + try { + gtg = gtg && inspector.begin(revisionNumber, new Nodeid(nodeid, true)); + int i; + String fname = null; + String flags = null; + Nodeid nid = null; + byte[] data = da.byteArray(); + for (i = 0; gtg && i < actualLen; i++) { + int x = i; + for( ; data[i] != '\n' && i < actualLen; i++) { + if (fname == null && data[i] == 0) { + fname = fnamePool.unify(new String(data, x, i - x)); + x = i+1; + } + } + if (i < actualLen) { + assert data[i] == '\n'; + int nodeidLen = i - x < 40 ? i-x : 40; + nid = nodeidPool.unify(Nodeid.fromAscii(data, x, nodeidLen)); + if (nodeidLen + x < i) { + // 'x' and 'l' for executable bits and symlinks? + // hg --debug manifest shows 644 for each regular file in my repo + flags = flagsPool.unify(new String(data, x + nodeidLen, i-x-nodeidLen)); + } + gtg = gtg && inspector.next(nid, fname, flags); + } + nid = null; + fname = flags = null; + } + gtg = gtg && inspector.end(revisionNumber); + } catch (IOException ex) { + throw new HgBadStateException(ex); + } + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/HgRemoteRepository.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/HgRemoteRepository.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,447 @@ +/* + * 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.repo; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.StreamTokenizer; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLConnection; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.prefs.BackingStoreException; +import java.util.prefs.Preferences; +import java.util.zip.InflaterInputStream; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.tmatesoft.hg.core.HgBadArgumentException; +import org.tmatesoft.hg.core.HgBadStateException; +import org.tmatesoft.hg.core.HgException; +import org.tmatesoft.hg.core.Nodeid; + +/** + * WORK IN PROGRESS, DO NOT USE + * + * @see http://mercurial.selenic.com/wiki/WireProtocol + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgRemoteRepository { + + private final URL url; + private final SSLContext sslContext; + private final String authInfo; + private final boolean debug = Boolean.parseBoolean(System.getProperty("hg4j.remote.debug")); + private HgLookup lookupHelper; + + HgRemoteRepository(URL url) throws HgBadArgumentException { + if (url == null) { + throw new IllegalArgumentException(); + } + this.url = url; + if ("https".equals(url.getProtocol())) { + try { + sslContext = SSLContext.getInstance("SSL"); + class TrustEveryone implements X509TrustManager { + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + if (debug) { + System.out.println("checkClientTrusted:" + authType); + } + } + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + if (debug) { + System.out.println("checkServerTrusted:" + authType); + } + } + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + }; + sslContext.init(null, new TrustManager[] { new TrustEveryone() }, null); + } catch (Exception ex) { + throw new HgBadArgumentException("Can't initialize secure connection", ex); + } + } else { + sslContext = null; + } + if (url.getUserInfo() != null) { + String ai = null; + try { + // Hack to get Base64-encoded credentials + Preferences tempNode = Preferences.userRoot().node("xxx"); + tempNode.putByteArray("xxx", url.getUserInfo().getBytes()); + ai = tempNode.get("xxx", null); + tempNode.removeNode(); + } catch (BackingStoreException ex) { + ex.printStackTrace(); + // IGNORE + } + authInfo = ai; + } else { + authInfo = null; + } + } + + public boolean isInvalid() throws HgException { + // say hello to server, check response + if (Boolean.FALSE.booleanValue()) { + throw HgRepository.notImplemented(); + } + return false; // FIXME + } + + /** + * @return human-readable address of the server, without user credentials or any other security information + */ + public String getLocation() { + if (url.getUserInfo() == null) { + return url.toExternalForm(); + } + if (url.getPort() != -1) { + return String.format("%s://%s:%d%s", url.getProtocol(), url.getHost(), url.getPort(), url.getPath()); + } else { + return String.format("%s://%s%s", url.getProtocol(), url.getHost(), url.getPath()); + } + } + + public List heads() throws HgException { + try { + URL u = new URL(url, url.getPath() + "?cmd=heads"); + HttpURLConnection c = setupConnection(u.openConnection()); + c.connect(); + if (debug) { + dumpResponseHeader(u, c); + } + InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); + StreamTokenizer st = new StreamTokenizer(is); + st.ordinaryChars('0', '9'); + st.wordChars('0', '9'); + st.eolIsSignificant(false); + LinkedList parseResult = new LinkedList(); + while (st.nextToken() != StreamTokenizer.TT_EOF) { + parseResult.add(Nodeid.fromAscii(st.sval)); + } + return parseResult; + } catch (MalformedURLException ex) { + throw new HgException(ex); + } catch (IOException ex) { + throw new HgException(ex); + } + } + + public List between(Nodeid tip, Nodeid base) throws HgException { + Range r = new Range(base, tip); + // XXX shall handle errors like no range key in the returned map, not sure how. + return between(Collections.singletonList(r)).get(r); + } + + /** + * @param ranges + * @return map, where keys are input instances, values are corresponding server reply + * @throws HgException + */ + public Map> between(Collection ranges) throws HgException { + if (ranges.isEmpty()) { + return Collections.emptyMap(); + } + // if fact, shall do other way round, this method shall send + LinkedHashMap> rv = new LinkedHashMap>(ranges.size() * 4 / 3); + StringBuilder sb = new StringBuilder(20 + ranges.size() * 82); + sb.append("pairs="); + for (Range r : ranges) { + sb.append(r.end.toString()); + sb.append('-'); + sb.append(r.start.toString()); + sb.append('+'); + } + if (sb.charAt(sb.length() - 1) == '+') { + // strip last space + sb.setLength(sb.length() - 1); + } + try { + boolean usePOST = ranges.size() > 3; + URL u = new URL(url, url.getPath() + "?cmd=between" + (usePOST ? "" : '&' + sb.toString())); + HttpURLConnection c = setupConnection(u.openConnection()); + if (usePOST) { + c.setRequestMethod("POST"); + c.setRequestProperty("Content-Length", String.valueOf(sb.length()/*nodeids are ASCII, bytes == characters */)); + c.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + c.setDoOutput(true); + c.connect(); + OutputStream os = c.getOutputStream(); + os.write(sb.toString().getBytes()); + os.flush(); + os.close(); + } else { + c.connect(); + } + if (debug) { + System.out.printf("%d ranges, method:%s \n", ranges.size(), c.getRequestMethod()); + dumpResponseHeader(u, c); + } + InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); + StreamTokenizer st = new StreamTokenizer(is); + st.ordinaryChars('0', '9'); + st.wordChars('0', '9'); + st.eolIsSignificant(true); + Iterator rangeItr = ranges.iterator(); + LinkedList currRangeList = null; + Range currRange = null; + boolean possiblyEmptyNextLine = true; + while (st.nextToken() != StreamTokenizer.TT_EOF) { + if (st.ttype == StreamTokenizer.TT_EOL) { + if (possiblyEmptyNextLine) { + // newline follows newline; + assert currRange == null; + assert currRangeList == null; + if (!rangeItr.hasNext()) { + throw new HgBadStateException(); + } + rv.put(rangeItr.next(), Collections.emptyList()); + } else { + if (currRange == null || currRangeList == null) { + throw new HgBadStateException(); + } + // indicate next range value is needed + currRange = null; + currRangeList = null; + possiblyEmptyNextLine = true; + } + } else { + possiblyEmptyNextLine = false; + if (currRange == null) { + if (!rangeItr.hasNext()) { + throw new HgBadStateException(); + } + currRange = rangeItr.next(); + currRangeList = new LinkedList(); + rv.put(currRange, currRangeList); + } + Nodeid nid = Nodeid.fromAscii(st.sval); + currRangeList.addLast(nid); + } + } + is.close(); + return rv; + } catch (MalformedURLException ex) { + throw new HgException(ex); + } catch (IOException ex) { + throw new HgException(ex); + } + } + + public List branches(List nodes) throws HgException { + StringBuilder sb = new StringBuilder(20 + nodes.size() * 41); + sb.append("nodes="); + for (Nodeid n : nodes) { + sb.append(n.toString()); + sb.append('+'); + } + if (sb.charAt(sb.length() - 1) == '+') { + // strip last space + sb.setLength(sb.length() - 1); + } + try { + URL u = new URL(url, url.getPath() + "?cmd=branches&" + sb.toString()); + HttpURLConnection c = setupConnection(u.openConnection()); + c.connect(); + if (debug) { + dumpResponseHeader(u, c); + } + InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); + StreamTokenizer st = new StreamTokenizer(is); + st.ordinaryChars('0', '9'); + st.wordChars('0', '9'); + st.eolIsSignificant(false); + ArrayList parseResult = new ArrayList(nodes.size() * 4); + while (st.nextToken() != StreamTokenizer.TT_EOF) { + parseResult.add(Nodeid.fromAscii(st.sval)); + } + if (parseResult.size() != nodes.size() * 4) { + throw new HgException(String.format("Bad number of nodeids in result (shall be factor 4), expected %d, got %d", nodes.size()*4, parseResult.size())); + } + ArrayList rv = new ArrayList(nodes.size()); + for (int i = 0; i < nodes.size(); i++) { + RemoteBranch rb = new RemoteBranch(parseResult.get(i*4), parseResult.get(i*4 + 1), parseResult.get(i*4 + 2), parseResult.get(i*4 + 3)); + rv.add(rb); + } + return rv; + } catch (MalformedURLException ex) { + throw new HgException(ex); + } catch (IOException ex) { + throw new HgException(ex); + } + } + + /* + * XXX need to describe behavior when roots arg is empty; our RepositoryComparator code currently returns empty lists when + * no common elements found, which in turn means we need to query changes starting with NULL nodeid. + * + * WireProtocol wiki: roots = a list of the latest nodes on every service side changeset branch that both the client and server know about. + * + * Perhaps, shall be named 'changegroup' + + * Changegroup: + * http://mercurial.selenic.com/wiki/Merge + * http://mercurial.selenic.com/wiki/WireProtocol + * + * according to latter, bundleformat data is sent through zlib + * (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 HgException { + List _roots = roots.isEmpty() ? Collections.singletonList(Nodeid.NULL) : roots; + StringBuilder sb = new StringBuilder(20 + _roots.size() * 41); + sb.append("roots="); + for (Nodeid n : _roots) { + sb.append(n.toString()); + sb.append('+'); + } + if (sb.charAt(sb.length() - 1) == '+') { + // strip last space + sb.setLength(sb.length() - 1); + } + try { + URL u = new URL(url, url.getPath() + "?cmd=changegroup&" + sb.toString()); + HttpURLConnection c = setupConnection(u.openConnection()); + c.connect(); + if (debug) { + dumpResponseHeader(u, c); + } + File tf = writeBundle(c.getInputStream(), false, "HG10GZ" /*didn't see any other that zip*/); + if (debug) { + System.out.printf("Wrote bundle %s for roots %s\n", tf, sb); + } + return getLookupHelper().loadBundle(tf); + } catch (MalformedURLException ex) { + throw new HgException(ex); + } catch (IOException ex) { + throw new HgException(ex); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + '[' + getLocation() + ']'; + } + + private HgLookup getLookupHelper() { + if (lookupHelper == null) { + lookupHelper = new HgLookup(); + } + return lookupHelper; + } + + private HttpURLConnection setupConnection(URLConnection urlConnection) { + urlConnection.setRequestProperty("User-Agent", "hg4j/0.5.0"); + urlConnection.addRequestProperty("Accept", "application/mercurial-0.1"); + if (authInfo != null) { + urlConnection.addRequestProperty("Authorization", "Basic " + authInfo); + } + if (sslContext != null) { + ((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslContext.getSocketFactory()); + } + return (HttpURLConnection) urlConnection; + } + + private void dumpResponseHeader(URL u, HttpURLConnection c) { + System.out.printf("Query (%d bytes):%s\n", u.getQuery().length(), u.getQuery()); + System.out.println("Response headers:"); + final Map> headerFields = c.getHeaderFields(); + for (String s : headerFields.keySet()) { + System.out.printf("%s: %s\n", s, c.getHeaderField(s)); + } + } + + private static File writeBundle(InputStream is, boolean decompress, String header) throws IOException { + InputStream zipStream = decompress ? new InflaterInputStream(is) : is; + File tf = File.createTempFile("hg-bundle-", null); + FileOutputStream fos = new FileOutputStream(tf); + fos.write(header.getBytes()); + int r; + byte[] buf = new byte[8*1024]; + while ((r = zipStream.read(buf)) != -1) { + fos.write(buf, 0, r); + } + fos.close(); + zipStream.close(); + return tf; + } + + + public static final class Range { + /** + * Root of the range, earlier revision + */ + public final Nodeid start; + /** + * Head of the range, later revision. + */ + public final Nodeid end; + + /** + * @param from - root/base revision + * @param to - head/tip revision + */ + public Range(Nodeid from, Nodeid to) { + start = from; + end = to; + } + } + + public static final class RemoteBranch { + public final Nodeid head, root, p1, p2; + + public RemoteBranch(Nodeid h, Nodeid r, Nodeid parent1, Nodeid parent2) { + head = h; + root = r; + p1 = parent1; + p2 = parent2; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (false == obj instanceof RemoteBranch) { + return false; + } + RemoteBranch o = (RemoteBranch) obj; + return head.equals(o.head) && root.equals(o.root) && (p1 == null && o.p1 == null || p1.equals(o.p1)) && (p2 == null && o.p2 == null || p2.equals(o.p2)); + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/HgRepository.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/HgRepository.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2010-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.repo; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +import org.tmatesoft.hg.internal.ConfigFile; +import org.tmatesoft.hg.internal.DataAccessProvider; +import org.tmatesoft.hg.internal.Filter; +import org.tmatesoft.hg.internal.RelativePathRewrite; +import org.tmatesoft.hg.internal.RequiresFile; +import org.tmatesoft.hg.internal.RevlogStream; +import org.tmatesoft.hg.util.FileIterator; +import org.tmatesoft.hg.util.FileWalker; +import org.tmatesoft.hg.util.Path; +import org.tmatesoft.hg.util.PathRewrite; + + + +/** + * Shall be as state-less as possible, all the caching happens outside the repo, in commands/walkers + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public final class HgRepository { + + // if new constants added, consider fixing HgInternals#wrongLocalRevision + public static final int TIP = -3; + public static final int BAD_REVISION = Integer.MIN_VALUE; + public static final int WORKING_COPY = -2; + + // temp aux marker method + public static IllegalStateException notImplemented() { + return new IllegalStateException("Not implemented"); + } + + private final File repoDir; // .hg folder + private final String repoLocation; + private final DataAccessProvider dataAccess; + private final PathRewrite normalizePath; + private final PathRewrite dataPathHelper; + private final PathRewrite repoPathHelper; + + private HgChangelog changelog; + private HgManifest manifest; + private HgTags tags; + // XXX perhaps, shall enable caching explicitly + private final HashMap> streamsCache = new HashMap>(); + + private final org.tmatesoft.hg.internal.Internals impl = new org.tmatesoft.hg.internal.Internals(); + private HgIgnore ignore; + private ConfigFile configFile; + + HgRepository(String repositoryPath) { + repoDir = null; + repoLocation = repositoryPath; + dataAccess = null; + dataPathHelper = repoPathHelper = null; + normalizePath = null; + } + + HgRepository(String repositoryPath, File repositoryRoot) { + assert ".hg".equals(repositoryRoot.getName()) && repositoryRoot.isDirectory(); + assert repositoryPath != null; + assert repositoryRoot != null; + repoDir = repositoryRoot; + repoLocation = repositoryPath; + dataAccess = new DataAccessProvider(); + final boolean runningOnWindows = System.getProperty("os.name").indexOf("Windows") != -1; + if (runningOnWindows) { + normalizePath = new PathRewrite() { + + public String rewrite(String path) { + // TODO handle . and .. (although unlikely to face them from GUI client) + path = path.replace('\\', '/').replace("//", "/"); + if (path.startsWith("/")) { + path = path.substring(1); + } + return path; + } + }; + } else { + normalizePath = new PathRewrite.Empty(); // or strip leading slash, perhaps? + } + parseRequires(); + dataPathHelper = impl.buildDataFilesHelper(); + repoPathHelper = impl.buildRepositoryFilesHelper(); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[" + getLocation() + (isInvalid() ? "(BAD)" : "") + "]"; + } + + public String getLocation() { + return repoLocation; + } + + public boolean isInvalid() { + return repoDir == null || !repoDir.exists() || !repoDir.isDirectory(); + } + + public HgChangelog getChangelog() { + if (this.changelog == null) { + String storagePath = repoPathHelper.rewrite("00changelog.i"); + RevlogStream content = resolve(Path.create(storagePath), true); + this.changelog = new HgChangelog(this, content); + } + return this.changelog; + } + + public HgManifest getManifest() { + if (this.manifest == null) { + RevlogStream content = resolve(Path.create(repoPathHelper.rewrite("00manifest.i")), true); + this.manifest = new HgManifest(this, content); + } + return this.manifest; + } + + public final HgTags getTags() { + if (tags == null) { + tags = new HgTags(); + try { + tags.readGlobal(new File(repoDir.getParentFile(), ".hgtags")); + tags.readLocal(new File(repoDir, "localtags")); + } catch (IOException ex) { + ex.printStackTrace(); // FIXME log or othewise report + } + } + return tags; + } + + public HgDataFile getFileNode(String path) { + String nPath = normalizePath.rewrite(path); + String storagePath = dataPathHelper.rewrite(nPath); + RevlogStream content = resolve(Path.create(storagePath), false); + Path p = Path.create(nPath); + if (content == null) { + return new HgDataFile(this, p); + } + return new HgDataFile(this, p, content); + } + + public HgDataFile getFileNode(Path path) { + String storagePath = dataPathHelper.rewrite(path.toString()); + RevlogStream content = resolve(Path.create(storagePath), false); + // XXX no content when no file? or HgDataFile.exists() to detect that? + if (content == null) { + return new HgDataFile(this, path); + } + return new HgDataFile(this, path, content); + } + + /* clients need to rewrite path from their FS to a repository-friendly paths, and, perhaps, vice versa*/ + public PathRewrite getToRepoPathHelper() { + return normalizePath; + } + + // local to hide use of io.File. + /*package-local*/ File getRepositoryRoot() { + return repoDir; + } + + // 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) + /*package-local*/ final HgDirstate loadDirstate() { + return new HgDirstate(getDataAccess(), new File(repoDir, "dirstate")); + } + + // package-local, see comment for loadDirstate + /*package-local*/ final HgIgnore getIgnore() { + // TODO read config for additional locations + if (ignore == null) { + ignore = new HgIgnore(); + try { + File ignoreFile = new File(repoDir.getParentFile(), ".hgignore"); + ignore.read(ignoreFile); + } catch (IOException ex) { + ex.printStackTrace(); // log warn + } + } + return ignore; + } + + /*package-local*/ DataAccessProvider getDataAccess() { + return dataAccess; + } + + // FIXME not sure repository shall create walkers + /*package-local*/ FileIterator createWorkingDirWalker() { + File repoRoot = repoDir.getParentFile(); + Path.Source pathSrc = new Path.SimpleSource(new PathRewrite.Composite(new RelativePathRewrite(repoRoot), 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); + } + + /** + * Perhaps, should be separate interface, like ContentLookup + * path - repository storage path (i.e. one usually with .i or .d) + */ + /*package-local*/ RevlogStream resolve(Path path, boolean shallFakeNonExistent) { + final SoftReference ref = streamsCache.get(path); + RevlogStream cached = ref == null ? null : ref.get(); + if (cached != null) { + return cached; + } + File f = new File(repoDir, path.toString()); + if (f.exists()) { + RevlogStream s = new RevlogStream(dataAccess, f); + streamsCache.put(path, new SoftReference(s)); + return s; + } else { + if (shallFakeNonExistent) { + try { + File fake = File.createTempFile(f.getName(), null); + fake.deleteOnExit(); + return new RevlogStream(dataAccess, fake); + } catch (IOException ex) { + ex.printStackTrace(); // FIXME report in debug + } + } + } + return null; // XXX empty stream instead? + } + + // can't expose internal class, otherwise seems reasonable to have it in API + /*package-local*/ ConfigFile getConfigFile() { + if (configFile == null) { + configFile = impl.newConfigFile(); + configFile.addLocation(new File(System.getProperty("user.home"), ".hgrc")); + // last one, overrides anything else + // /.hg/hgrc + configFile.addLocation(new File(getRepositoryRoot(), "hgrc")); + } + return configFile; + } + + /*package-local*/ List getFiltersFromRepoToWorkingDir(Path p) { + return instantiateFilters(p, new Filter.Options(Filter.Direction.FromRepo)); + } + + /*package-local*/ List getFiltersFromWorkingDirToRepo(Path p) { + return instantiateFilters(p, new Filter.Options(Filter.Direction.ToRepo)); + } + + private List instantiateFilters(Path p, Filter.Options opts) { + List factories = impl.getFilters(this, getConfigFile()); + if (factories.isEmpty()) { + return Collections.emptyList(); + } + ArrayList rv = new ArrayList(factories.size()); + for (Filter.Factory ff : factories) { + Filter f = ff.create(p, opts); + if (f != null) { + rv.add(f); + } + } + return rv; + } + + private void parseRequires() { + new RequiresFile().parse(impl, new File(repoDir, "requires")); + } + +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/HgStatusCollector.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/HgStatusCollector.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,466 @@ +/* + * 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.repo; + +import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; +import static org.tmatesoft.hg.repo.HgRepository.TIP; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.tmatesoft.hg.core.HgDataStreamException; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.Pool; +import org.tmatesoft.hg.util.Path; +import org.tmatesoft.hg.util.PathPool; +import org.tmatesoft.hg.util.PathRewrite; + + +/** + * RevisionWalker? + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgStatusCollector { + + private final HgRepository repo; + private final SortedMap cache; // sparse array, in fact + // with cpython repository, ~70 000 changes, complete Log (direct out, no reverse) output + // no cache limit, no nodeids and fname caching - OOME on changeset 1035 + // 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 final Pool cacheNodes; + private final Pool cacheFilenames; // XXX in fact, need to think if use of PathPool directly instead is better solution + private final ManifestRevisionInspector emptyFakeState; + + + public HgStatusCollector(HgRepository hgRepo) { + this.repo = hgRepo; + cache = new TreeMap(); + cacheNodes = new Pool(); + cacheFilenames = new Pool(); + + emptyFakeState = new ManifestRevisionInspector(null, null); + emptyFakeState.begin(-1, null); + emptyFakeState.end(-1); + } + + public HgRepository getRepo() { + return repo; + } + + private ManifestRevisionInspector get(int rev) { + ManifestRevisionInspector i = cache.get(rev); + if (i == null) { + if (rev == -1) { + return emptyFakeState; + } + while (cache.size() > cacheMaxSize) { + // assume usually we go from oldest to newest, hence remove oldest as most likely to be no longer necessary + cache.remove(cache.firstKey()); + } + i = new ManifestRevisionInspector(cacheNodes, cacheFilenames); + cache.put(rev, i); + repo.getManifest().walk(rev, rev, i); + } + return i; + } + + private boolean cached(int revision) { + return cache.containsKey(revision) || revision == -1; + } + + private void initCacheRange(int minRev, int maxRev) { + while (cache.size() > cacheMaxSize) { + // assume usually we go from oldest to newest, hence remove oldest as most likely to be no longer necessary + cache.remove(cache.firstKey()); + } + repo.getManifest().walk(minRev, maxRev, new HgManifest.Inspector() { + private ManifestRevisionInspector delegate; + private boolean cacheHit; // range may include revisions we already know about, do not re-create them + + public boolean begin(int revision, Nodeid nid) { + assert delegate == null; + if (cache.containsKey(revision)) { // don't need to check emptyFakeState hit as revision never -1 here + cacheHit = true; + } else { + cache.put(revision, delegate = new ManifestRevisionInspector(cacheNodes, cacheFilenames)); + // cache may grow bigger than max size here, but it's ok as present simplistic cache clearing mechanism may + // otherwise remove entries we just added + delegate.begin(revision, nid); + cacheHit = false; + } + return true; + } + + public boolean next(Nodeid nid, String fname, String flags) { + if (!cacheHit) { + delegate.next(nid, fname, flags); + } + return true; + } + + public boolean end(int revision) { + if (!cacheHit) { + delegate.end(revision); + } + cacheHit = false; + delegate = null; + return true; + } + }); + } + + /*package-local*/ ManifestRevisionInspector raw(int rev) { + return get(rev); + } + /*package-local*/ PathPool getPathPool() { + if (pathPool == null) { + pathPool = new PathPool(new PathRewrite.Empty()); + } + return pathPool; + } + + /** + * Allows sharing of a common path cache + */ + public void setPathPool(PathPool pathPool) { + this.pathPool = pathPool; + } + + + // hg status --change + public void change(int rev, HgStatusInspector inspector) { + int[] parents = new int[2]; + repo.getChangelog().parents(rev, parents, null, null); + walk(parents[0], rev, inspector); + } + + // I assume revision numbers are the same for changelog and manifest - here + // user would like to pass changelog revision numbers, and I use them directly to walk manifest. + // if this assumption is wrong, fix this (lookup manifest revisions from changeset). + // rev1 and rev2 may be -1 to indicate comparison to empty repository + // argument order matters + public void walk(int rev1, int rev2, HgStatusInspector inspector) { + if (rev1 == rev2) { + throw new IllegalArgumentException(); + } + if (inspector == null) { + throw new IllegalArgumentException(); + } + if (inspector instanceof Record) { + ((Record) inspector).init(rev1, rev2, this); + } + final int lastManifestRevision = repo.getManifest().getLastRevision(); + if (rev1 == TIP) { + rev1 = lastManifestRevision; + } + if (rev2 == TIP) { + rev2 = lastManifestRevision; + } + // in fact, rev1 and rev2 are often next (or close) to each other, + // thus, we can optimize Manifest reads here (manifest.walk(rev1, rev2)) + ManifestRevisionInspector r1, r2 ; + boolean need1 = !cached(rev1), need2 = !cached(rev2); + if (need1 || need2) { + int minRev, maxRev; + if (need1 && need2 && Math.abs(rev1 - rev2) < 5 /*subjective equivalent of 'close enough'*/) { + minRev = rev1 < rev2 ? rev1 : rev2; + maxRev = minRev == rev1 ? rev2 : rev1; + if (minRev > 0) { + minRev--; // expand range a bit + } + initCacheRange(minRev, maxRev); + need1 = need2 = false; + } + // either both unknown and far from each other, or just one of them. + // read with neighbors to save potential subsequent calls for neighboring elements + // XXX perhaps, if revlog.baseRevision is cheap, shall expand minRev up to baseRevision + // which going to be read anyway + if (need1) { + minRev = rev1; + maxRev = rev1 < lastManifestRevision-5 ? rev1+5 : lastManifestRevision; + initCacheRange(minRev, maxRev); + } + if (need2) { + minRev = rev2; + maxRev = rev2 < lastManifestRevision-5 ? rev2+5 : lastManifestRevision; + initCacheRange(minRev, maxRev); + } + } + r1 = get(rev1); + r2 = get(rev2); + + PathPool pp = getPathPool(); + + TreeSet r1Files = new TreeSet(r1.files()); + for (String fname : r2.files()) { + if (r1Files.remove(fname)) { + Nodeid nidR1 = r1.nodeid(fname); + Nodeid nidR2 = r2.nodeid(fname); + String flagsR1 = r1.flags(fname); + String flagsR2 = r2.flags(fname); + if (nidR1.equals(nidR2) && ((flagsR2 == null && flagsR1 == null) || flagsR2.equals(flagsR1))) { + inspector.clean(pp.path(fname)); + } else { + inspector.modified(pp.path(fname)); + } + } else { + try { + Path copyTarget = pp.path(fname); + Path copyOrigin = getOriginIfCopy(repo, copyTarget, r1Files, rev1); + if (copyOrigin != null) { + inspector.copied(pp.path(copyOrigin) /*pipe through pool, just in case*/, copyTarget); + } else { + inspector.added(copyTarget); + } + } catch (HgDataStreamException ex) { + ex.printStackTrace(); + // FIXME perhaps, shall record this exception to dedicated mediator and continue + // for a single file not to be irresolvable obstacle for a status operation + } + } + } + for (String left : r1Files) { + inspector.removed(pp.path(left)); + } + } + + public Record status(int rev1, int rev2) { + Record rv = new Record(); + walk(rev1, rev2, rv); + return rv; + } + + /*package-local*/static Path getOriginIfCopy(HgRepository hgRepo, Path fname, Collection originals, int originalChangelogRevision) throws HgDataStreamException { + HgDataFile df = hgRepo.getFileNode(fname); + while (df.isCopy()) { + Path original = df.getCopySourceName(); + if (originals.contains(original.toString())) { + df = hgRepo.getFileNode(original); + int changelogRevision = df.getChangesetLocalRevision(0); + if (changelogRevision <= originalChangelogRevision) { + // copy/rename source was known prior to rev1 + // (both r1Files.contains is true and original was created earlier than rev1) + // without r1Files.contains changelogRevision <= rev1 won't suffice as the file + // might get removed somewhere in between (changelogRevision < R < rev1) + return original; + } + break; // copy/rename done later + } + df = hgRepo.getFileNode(original); // try more steps away + } + return null; + } + + // XXX for r1..r2 status, only modified, added, removed (and perhaps, clean) make sense + // XXX Need to specify whether copy targets are in added or not (@see Inspector#copied above) + public static class Record implements HgStatusInspector { + private List modified, added, removed, clean, missing, unknown, ignored; + private Map copied; + + private int startRev, endRev; + private HgStatusCollector statusHelper; + + // XXX StatusCollector may additionally initialize Record instance to speed lookup of changed file revisions + // here I need access to ManifestRevisionInspector via #raw(). Perhaps, non-static class (to get + // implicit reference to StatusCollector) may be better? + // Since users may want to reuse Record instance we've once created (and initialized), we need to + // ensure functionality is correct for each/any call (#walk checks instanceof Record and fixes it up) + // Perhaps, distinct helper (sc.getRevisionHelper().nodeid(fname)) would be better, just not clear + // how to supply [start..end] values there easily + /*package-local*/void init(int startRevision, int endRevision, HgStatusCollector self) { + startRev = startRevision; + endRev = endRevision; + statusHelper = self; + } + + public Nodeid nodeidBeforeChange(Path fname) { + if (statusHelper == null || startRev == BAD_REVISION) { + return null; + } + if ((modified == null || !modified.contains(fname)) && (removed == null || !removed.contains(fname))) { + return null; + } + return statusHelper.raw(startRev).nodeid(fname.toString()); + } + public Nodeid nodeidAfterChange(Path fname) { + if (statusHelper == null || endRev == BAD_REVISION) { + return null; + } + if ((modified == null || !modified.contains(fname)) && (added == null || !added.contains(fname))) { + return null; + } + return statusHelper.raw(endRev).nodeid(fname.toString()); + } + + public List getModified() { + return proper(modified); + } + + public List getAdded() { + return proper(added); + } + + public List getRemoved() { + return proper(removed); + } + + public Map getCopied() { + if (copied == null) { + return Collections.emptyMap(); + } + return Collections.unmodifiableMap(copied); + } + + public List getClean() { + return proper(clean); + } + + public List getMissing() { + return proper(missing); + } + + public List getUnknown() { + return proper(unknown); + } + + public List getIgnored() { + return proper(ignored); + } + + private List proper(List l) { + if (l == null) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(l); + } + + // + // + + public void modified(Path fname) { + modified = doAdd(modified, fname); + } + + public void added(Path fname) { + added = doAdd(added, fname); + } + + public void copied(Path fnameOrigin, Path fnameAdded) { + if (copied == null) { + copied = new LinkedHashMap(); + } + added(fnameAdded); + copied.put(fnameAdded, fnameOrigin); + } + + public void removed(Path fname) { + removed = doAdd(removed, fname); + } + + public void clean(Path fname) { + clean = doAdd(clean, fname); + } + + public void missing(Path fname) { + missing = doAdd(missing, fname); + } + + public void unknown(Path fname) { + unknown = doAdd(unknown, fname); + } + + public void ignored(Path fname) { + ignored = doAdd(ignored, fname); + } + + private static List doAdd(List l, Path p) { + if (l == null) { + l = new LinkedList(); + } + l.add(p); + return l; + } + } + + /*package-local*/ static final class ManifestRevisionInspector implements HgManifest.Inspector { + private final TreeMap idsMap; + private final TreeMap flagsMap; + private final Pool idsPool; + private final Pool namesPool; + + // optional pools for effective management of nodeids and filenames (they are likely + // to be duplicated among different manifest revisions + public ManifestRevisionInspector(Pool nodeidPool, Pool filenamePool) { + idsPool = nodeidPool; + namesPool = filenamePool; + idsMap = new TreeMap(); + flagsMap = new TreeMap(); + } + + public Collection files() { + return idsMap.keySet(); + } + + public Nodeid nodeid(String fname) { + return idsMap.get(fname); + } + + public String flags(String fname) { + return flagsMap.get(fname); + } + + // + + public boolean next(Nodeid nid, String fname, String flags) { + if (namesPool != null) { + fname = namesPool.unify(fname); + } + if (idsPool != null) { + nid = idsPool.unify(nid); + } + idsMap.put(fname, nid); + if (flags != null) { + // TreeMap$Entry takes 32 bytes. No reason to keep null for such price + // Perhaps, Map> might be better solution + flagsMap.put(fname, flags); + } + return true; + } + + public boolean end(int revision) { + // in fact, this class cares about single revision + return false; + } + + public boolean begin(int revision, Nodeid nid) { + return true; + } + } + +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/HgStatusInspector.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/HgStatusInspector.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2010-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.repo; + +import org.tmatesoft.hg.util.Path; + +/** + * Callback to get file status information + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public interface HgStatusInspector { + void modified(Path fname); + void added(Path fname); + // XXX need to specify whether StatusCollector invokes added() along with copied or not! + void copied(Path fnameOrigin, Path fnameAdded); // if copied files of no interest, should delegate to self.added(fnameAdded); + void removed(Path fname); + void clean(Path fname); + void missing(Path fname); // aka deleted (tracked by Hg, but not available in FS any more + void unknown(Path fname); // not tracked + void ignored(Path fname); +} \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/HgTags.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/HgTags.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,150 @@ +/* + * 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.repo; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.tmatesoft.hg.core.Nodeid; + +/** + * @see http://mercurial.selenic.com/wiki/TagDesign + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgTags { + // global tags come from ".hgtags" + // local come from ".hg/localtags" + + private final Map> globalToName; + private final Map> localToName; + private final Map> globalFromName; + private final Map> localFromName; + + + /*package-local*/ HgTags() { + globalToName = new HashMap>(); + localToName = new HashMap>(); + globalFromName = new TreeMap>(); + localFromName = new TreeMap>(); + } + + /*package-local*/ void readLocal(File localTags) throws IOException { + if (localTags == null || localTags.isDirectory()) { + throw new IllegalArgumentException(String.valueOf(localTags)); + } + read(localTags, localToName, localFromName); + } + + /*package-local*/ void readGlobal(File globalTags) throws IOException { + if (globalTags == null || globalTags.isDirectory()) { + throw new IllegalArgumentException(String.valueOf(globalTags)); + } + read(globalTags, globalToName, globalFromName); + } + + private void read(File f, Map> nid2name, Map> name2nid) throws IOException { + if (!f.canRead()) { + return; + } + BufferedReader r = null; + try { + r = new BufferedReader(new FileReader(f)); + read(r, nid2name, name2nid); + } finally { + if (r != null) { + r.close(); + } + } + } + + private void read(BufferedReader reader, Map> nid2name, Map> name2nid) throws IOException { + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + if (line.length() == 0) { + continue; + } + if (line.length() < 40+2 /*nodeid, space and at least single-char tagname*/) { + System.out.println("Bad tags line:" + line); // FIXME log or otherwise report (IStatus analog?) + continue; + } + int spacePos = line.indexOf(' '); + if (spacePos != -1) { + assert spacePos == 40; + final byte[] nodeidBytes = line.substring(0, spacePos).getBytes(); + Nodeid nid = Nodeid.fromAscii(nodeidBytes, 0, nodeidBytes.length); + String tagName = line.substring(spacePos+1); + List nids = name2nid.get(tagName); + if (nids == null) { + nids = new LinkedList(); + // tagName is substring of full line, thus need a copy to let the line be GC'ed + // new String(tagName.toCharArray()) is more expressive, but results in 1 extra arraycopy + tagName = new String(tagName); + name2nid.put(tagName, nids); + } + // XXX repo.getNodeidCache().nodeid(nid); + ((LinkedList) nids).addFirst(nid); + List revTags = nid2name.get(nid); + if (revTags == null) { + revTags = new LinkedList(); + nid2name.put(nid, revTags); + } + revTags.add(tagName); + } else { + System.out.println("Bad tags line:" + line); // FIXME see above + } + } + } + + public List tags(Nodeid nid) { + ArrayList rv = new ArrayList(5); + List l; + if ((l = localToName.get(nid)) != null) { + rv.addAll(l); + } + if ((l = globalToName.get(nid)) != null) { + rv.addAll(l); + } + return rv; + } + + public boolean isTagged(Nodeid nid) { + return localToName.containsKey(nid) || globalToName.containsKey(nid); + } + + public List tagged(String tagName) { + ArrayList rv = new ArrayList(5); + List l; + if ((l = localFromName.get(tagName)) != null) { + rv.addAll(l); + } + if ((l = globalFromName.get(tagName)) != null) { + rv.addAll(l); + } + return rv; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,337 @@ +/* + * 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.repo; + +import static java.lang.Math.max; +import static java.lang.Math.min; +import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; +import static org.tmatesoft.hg.repo.HgRepository.TIP; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.Collections; +import java.util.Set; +import java.util.TreeSet; + +import org.tmatesoft.hg.core.HgDataStreamException; +import org.tmatesoft.hg.core.HgException; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.ByteArrayChannel; +import org.tmatesoft.hg.internal.FilterByteChannel; +import org.tmatesoft.hg.repo.HgStatusCollector.ManifestRevisionInspector; +import org.tmatesoft.hg.util.ByteChannel; +import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.util.FileIterator; +import org.tmatesoft.hg.util.Path; +import org.tmatesoft.hg.util.PathPool; +import org.tmatesoft.hg.util.PathRewrite; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgWorkingCopyStatusCollector { + + private final HgRepository repo; + private final FileIterator repoWalker; + private HgDirstate dirstate; + private HgStatusCollector baseRevisionCollector; + private PathPool pathPool; + + public HgWorkingCopyStatusCollector(HgRepository hgRepo) { + this(hgRepo, hgRepo.createWorkingDirWalker()); + } + + HgWorkingCopyStatusCollector(HgRepository hgRepo, FileIterator hgRepoWalker) { + this.repo = hgRepo; + this.repoWalker = hgRepoWalker; + } + + /** + * Optionally, supply a collector instance that may cache (or have already cached) base revision + * @param sc may be null + */ + public void setBaseRevisionCollector(HgStatusCollector sc) { + baseRevisionCollector = sc; + } + + /*package-local*/ PathPool getPathPool() { + if (pathPool == null) { + if (baseRevisionCollector == null) { + pathPool = new PathPool(new PathRewrite.Empty()); + } else { + return baseRevisionCollector.getPathPool(); + } + } + return pathPool; + } + + public void setPathPool(PathPool pathPool) { + this.pathPool = pathPool; + } + + + private HgDirstate getDirstate() { + if (dirstate == null) { + dirstate = repo.loadDirstate(); + } + return dirstate; + } + + // may be invoked few times + public void walk(int baseRevision, HgStatusInspector inspector) { + final HgIgnore hgIgnore = repo.getIgnore(); + TreeSet knownEntries = getDirstate().all(); + final boolean isTipBase; + if (baseRevision == TIP) { + baseRevision = repo.getManifest().getRevisionCount() - 1; + isTipBase = true; + } else { + isTipBase = baseRevision == repo.getManifest().getRevisionCount() - 1; + } + HgStatusCollector.ManifestRevisionInspector collect = null; + Set baseRevFiles = Collections.emptySet(); + if (!isTipBase) { + if (baseRevisionCollector != null) { + collect = baseRevisionCollector.raw(baseRevision); + } else { + collect = new HgStatusCollector.ManifestRevisionInspector(null, null); + repo.getManifest().walk(baseRevision, baseRevision, collect); + } + baseRevFiles = new TreeSet(collect.files()); + } + if (inspector instanceof HgStatusCollector.Record) { + HgStatusCollector sc = baseRevisionCollector == null ? new HgStatusCollector(repo) : baseRevisionCollector; + ((HgStatusCollector.Record) inspector).init(baseRevision, BAD_REVISION, sc); + } + repoWalker.reset(); + final PathPool pp = getPathPool(); + while (repoWalker.hasNext()) { + repoWalker.next(); + Path fname = repoWalker.name(); + File f = repoWalker.file(); + if (hgIgnore.isIgnored(fname)) { + inspector.ignored(pp.path(fname)); + } else if (knownEntries.remove(fname.toString())) { + // modified, added, removed, clean + if (collect != null) { // need to check against base revision, not FS file + checkLocalStatusAgainstBaseRevision(baseRevFiles, collect, baseRevision, fname, f, inspector); + baseRevFiles.remove(fname.toString()); + } else { + checkLocalStatusAgainstFile(fname, f, inspector); + } + } else { + inspector.unknown(pp.path(fname)); + } + } + if (collect != null) { + for (String r : baseRevFiles) { + inspector.removed(pp.path(r)); + } + } + for (String m : knownEntries) { + // missing known file from a working dir + if (getDirstate().checkRemoved(m) == null) { + // not removed from the repository = 'deleted' + inspector.missing(pp.path(m)); + } else { + // removed from the repo + // if we check against non-tip revision, do not report files that were added past that revision and now removed. + if (collect == null || baseRevFiles.contains(m)) { + inspector.removed(pp.path(m)); + } + } + } + } + + public HgStatusCollector.Record status(int baseRevision) { + HgStatusCollector.Record rv = new HgStatusCollector.Record(); + walk(baseRevision, rv); + return rv; + } + + //******************************************** + + + private void checkLocalStatusAgainstFile(Path fname, File f, HgStatusInspector inspector) { + HgDirstate.Record r; + if ((r = getDirstate().checkNormal(fname)) != null) { + // either clean or modified + if (f.lastModified() / 1000 == r.time && r.size == f.length()) { + inspector.clean(getPathPool().path(fname)); + } else { + // check actual content to avoid false modified files + HgDataFile df = repo.getFileNode(fname); + if (!areTheSame(f, df, HgRepository.TIP)) { + inspector.modified(df.getPath()); + } else { + inspector.clean(df.getPath()); + } + } + } else if ((r = getDirstate().checkAdded(fname)) != null) { + if (r.name2 == null) { + inspector.added(getPathPool().path(fname)); + } else { + inspector.copied(getPathPool().path(r.name2), getPathPool().path(fname)); + } + } else if ((r = getDirstate().checkRemoved(fname)) != null) { + inspector.removed(getPathPool().path(fname)); + } else if ((r = getDirstate().checkMerged(fname)) != null) { + inspector.modified(getPathPool().path(fname)); + } + } + + // XXX refactor checkLocalStatus methods in more OO way + private void checkLocalStatusAgainstBaseRevision(Set baseRevNames, ManifestRevisionInspector collect, int baseRevision, Path fname, File f, HgStatusInspector inspector) { + // fname is in the dirstate, either Normal, Added, Removed or Merged + Nodeid nid1 = collect.nodeid(fname.toString()); + String flags = collect.flags(fname.toString()); + HgDirstate.Record r; + if (nid1 == null) { + // normal: added? + // added: not known at the time of baseRevision, shall report + // merged: was not known, report as added? + if ((r = getDirstate().checkNormal(fname)) != null) { + try { + Path origin = HgStatusCollector.getOriginIfCopy(repo, fname, baseRevNames, baseRevision); + if (origin != null) { + inspector.copied(getPathPool().path(origin), getPathPool().path(fname)); + return; + } + } catch (HgDataStreamException ex) { + ex.printStackTrace(); + // FIXME report to a mediator, continue status collection + } + } else if ((r = getDirstate().checkAdded(fname)) != null) { + if (r.name2 != null && baseRevNames.contains(r.name2)) { + baseRevNames.remove(r.name2); // XXX surely I shall not report rename source as Removed? + inspector.copied(getPathPool().path(r.name2), getPathPool().path(fname)); + return; + } + // fall-through, report as added + } else if (getDirstate().checkRemoved(fname) != null) { + // removed: removed file was not known at the time of baseRevision, and we should not report it as removed + return; + } + inspector.added(getPathPool().path(fname)); + } else { + // was known; check whether clean or modified + // when added - seems to be the case of a file added once again, hence need to check if content is different + if ((r = getDirstate().checkNormal(fname)) != null || (r = getDirstate().checkMerged(fname)) != null || (r = getDirstate().checkAdded(fname)) != null) { + // either clean or modified + HgDataFile fileNode = repo.getFileNode(fname); + final int lengthAtRevision = fileNode.length(nid1); + if (r.size /* XXX File.length() ?! */ != lengthAtRevision || flags != todoGenerateFlags(fname /*java.io.File*/)) { + inspector.modified(getPathPool().path(fname)); + } else { + // check actual content to see actual changes + if (areTheSame(f, fileNode, fileNode.getLocalRevision(nid1))) { + inspector.clean(getPathPool().path(fname)); + } else { + inspector.modified(getPathPool().path(fname)); + } + } + } + // only those left in idsMap after processing are reported as removed + } + + // TODO think over if content comparison may be done more effectively by e.g. calculating nodeid for a local file and comparing it with nodeid from manifest + // we don't need to tell exact difference, hash should be enough to detect difference, and it doesn't involve reading historical file content, and it's relatively + // cheap to calc hash on a file (no need to keep it completely in memory). OTOH, if I'm right that the next approach is used for nodeids: + // changeset nodeid + hash(actual content) => entry (Nodeid) in the next Manifest + // then it's sufficient to check parents from dirstate, and if they do not match parents from file's baseRevision (non matching parents means different nodeids). + // The question is whether original Hg treats this case (same content, different parents and hence nodeids) as 'modified' or 'clean' + } + + private boolean areTheSame(File f, HgDataFile dataFile, int localRevision) { + // XXX consider adding HgDataDile.compare(File/byte[]/whatever) operation to optimize comparison + ByteArrayChannel bac = new ByteArrayChannel(); + boolean ioFailed = false; + try { + // need content with metadata striped off - although theoretically chances are metadata may be different, + // WC doesn't have it anyway + dataFile.content(localRevision, bac); + } catch (CancelledException ex) { + // silently ignore - can't happen, ByteArrayChannel is not cancellable + } catch (IOException ex) { + ioFailed = true; + } catch (HgException ex) { + ioFailed = true; + } + return !ioFailed && areTheSame(f, bac.toArray(), dataFile.getPath()); + } + + private boolean areTheSame(File f, final byte[] data, Path p) { + FileInputStream fis = null; + try { + try { + fis = new FileInputStream(f); + FileChannel fc = fis.getChannel(); + ByteBuffer fb = ByteBuffer.allocate(min(data.length, 8192)); + final boolean[] checkValue = new boolean[] { true }; + ByteChannel check = new ByteChannel() { + int x = 0; + final boolean debug = false; // XXX may want to add global variable to allow clients to turn + public int write(ByteBuffer buffer) { + for (int i = buffer.remaining(); i > 0; i--, x++) { + if (data[x] != buffer.get()) { + if (debug) { + byte[] xx = new byte[15]; + if (buffer.position() > 5) { + buffer.position(buffer.position() - 5); + } + buffer.get(xx); + System.out.print("expected >>" + new String(data, max(0, x - 4), 20) + "<< but got >>"); + System.out.println(new String(xx) + "<<"); + } + checkValue[0] = false; + break; + } + } + buffer.position(buffer.limit()); // mark as read + return buffer.limit(); + } + }; + FilterByteChannel filters = new FilterByteChannel(check, repo.getFiltersFromWorkingDirToRepo(p)); + while (fc.read(fb) != -1 && checkValue[0]) { + fb.flip(); + filters.write(fb); + fb.compact(); + } + return checkValue[0]; + } catch (IOException ex) { + if (fis != null) { + fis.close(); + } + ex.printStackTrace(); // log warn + } + } catch (/*TODO typed*/Exception ex) { + ex.printStackTrace(); + } + return false; + } + + private static String todoGenerateFlags(Path fname) { + // FIXME implement + return null; + } + +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/Revlog.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/Revlog.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,447 @@ +/* + * Copyright (c) 2010-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.repo; + +import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; +import static org.tmatesoft.hg.repo.HgRepository.TIP; + +import java.io.IOException; +import java.nio.ByteBuffer; +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.Nodeid; +import org.tmatesoft.hg.internal.DataAccess; +import org.tmatesoft.hg.internal.RevlogStream; +import org.tmatesoft.hg.util.ByteChannel; +import org.tmatesoft.hg.util.CancelSupport; +import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.util.ProgressSupport; + + +/** + * Base class for all Mercurial entities that are serialized in a so called revlog format (changelog, manifest, data files). + * + * Implementation note: + * Hides actual actual revlog stream implementation and its access methods (i.e. RevlogStream.Inspector), iow shall not expose anything internal + * in public methods. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +abstract class Revlog { + + private final HgRepository repo; + protected final RevlogStream content; + + protected Revlog(HgRepository hgRepo, RevlogStream contentStream) { + if (hgRepo == null) { + throw new IllegalArgumentException(); + } + if (contentStream == null) { + throw new IllegalArgumentException(); + } + repo = hgRepo; + content = contentStream; + } + + // invalid Revlog + protected Revlog(HgRepository hgRepo) { + repo = hgRepo; + content = null; + } + + public final HgRepository getRepo() { + return repo; + } + + public final int getRevisionCount() { + return content.revisionCount(); + } + + public final int getLastRevision() { + return content.revisionCount() - 1; + } + + public final Nodeid getRevision(int revision) { + // XXX cache nodeids? + return Nodeid.fromBinary(content.nodeid(revision), 0); + } + + public final int getLocalRevision(Nodeid nid) { + int revision = content.findLocalRevisionNumber(nid); + if (revision == BAD_REVISION) { + throw new IllegalArgumentException(String.format("%s doesn't represent a revision of %s", nid.toString(), this /*XXX HgDataFile.getPath might be more suitable here*/)); + } + return revision; + } + + // Till now, i follow approach that NULL nodeid is never part of revlog + public final boolean isKnown(Nodeid nodeid) { + final int rn = content.findLocalRevisionNumber(nodeid); + if (Integer.MIN_VALUE == rn) { + return false; + } + if (rn < 0 || rn >= content.revisionCount()) { + // Sanity check + throw new IllegalStateException(); + } + 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 + */ + protected void rawContent(Nodeid nodeid, ByteChannel sink) throws HgException, IOException, CancelledException { + rawContent(getLocalRevision(nodeid), sink); + } + + /** + * @param revision - repo-local index of this file change (not a changelog revision number!) + */ + protected void rawContent(int revision, ByteChannel sink) throws HgException, IOException, CancelledException { + if (sink == null) { + throw new IllegalArgumentException(); + } + ContentPipe insp = new ContentPipe(sink, 0); + insp.checkCancelled(); + content.iterate(revision, revision, true, insp); + insp.checkFailed(); + } + + /** + * XXX perhaps, return value Nodeid[2] and boolean needNodeids is better (and higher level) API for this query? + * + * @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 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 + * @return + */ + public void parents(int revision, int[] parentRevisions, byte[] parent1, byte[] parent2) { + if (revision != TIP && !(revision >= 0 && revision < content.revisionCount())) { + throw new IllegalArgumentException(String.valueOf(revision)); + } + if (parentRevisions == null || parentRevisions.length < 2) { + throw new IllegalArgumentException(String.valueOf(parentRevisions)); + } + if (parent1 != null && parent1.length < 20) { + throw new IllegalArgumentException(parent1.toString()); + } + if (parent2 != null && parent2.length < 20) { + throw new IllegalArgumentException(parent2.toString()); + } + class ParentCollector implements RevlogStream.Inspector { + public int p1 = -1; + public int p2 = -1; + public byte[] nodeid; + + public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) { + p1 = parent1Revision; + p2 = parent2Revision; + this.nodeid = new byte[20]; + // nodeid arg now comes in 32 byte from (as in file format description), however upper 12 bytes are zeros. + System.arraycopy(nodeid, nodeid.length > 20 ? nodeid.length - 20 : 0, this.nodeid, 0, 20); + } + }; + ParentCollector pc = new ParentCollector(); + content.iterate(revision, revision, false, pc); + parentRevisions[0] = pc.p1; + parentRevisions[1] = pc.p2; + if (parent1 != null) { + if (parentRevisions[0] == -1) { + Arrays.fill(parent1, 0, 20, (byte) 0); + } else { + content.iterate(parentRevisions[0], parentRevisions[0], false, pc); + System.arraycopy(pc.nodeid, 0, parent1, 0, 20); + } + } + if (parent2 != null) { + if (parentRevisions[1] == -1) { + Arrays.fill(parent2, 0, 20, (byte) 0); + } else { + content.iterate(parentRevisions[1], parentRevisions[1], false, pc); + System.arraycopy(pc.nodeid, 0, parent2, 0, 20); + } + } + } + + /* + * 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 { + + + 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 init() { + final RevlogStream stream = Revlog.this.content; + final int revisionCount = stream.revisionCount(); + firstParent = new Nodeid[revisionCount]; + // although branches/merges are less frequent, and most of secondParent would be -1/null, some sort of + // SparseOrderedList might be handy, provided its inner structures do not overweight simplicity of an array + secondParent = new Nodeid[revisionCount]; + // + sequential = new Nodeid[revisionCount]; + sorted = new Nodeid[revisionCount]; + + RevlogStream.Inspector insp = new RevlogStream.Inspector() { + + int ix = 0; + public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) { + if (ix != revisionNumber) { + // XXX temp code, just to make sure I understand what's going on here + throw new IllegalStateException(); + } + if (parent1Revision >= revisionNumber || parent2Revision >= revisionNumber) { + throw new IllegalStateException(); // sanity, revisions are sequential + } + final Nodeid nid = new Nodeid(nodeid, true); + sequential[ix] = sorted[ix] = nid; + if (parent1Revision != -1) { + assert parent1Revision < ix; + firstParent[ix] = sequential[parent1Revision]; + } + if (parent2Revision != -1) { // revlog of DataAccess.java has p2 set when p1 is -1 + assert parent2Revision < ix; + secondParent[ix] = sequential[parent2Revision]; + } + ix++; + } + }; + stream.iterate(0, TIP, false, insp); + 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(); + } + } + + // FIXME need to decide whether Nodeid(00 * 20) is always known or not + // right now Nodeid.NULL is not recognized as known if passed to this method, + // caller is supposed to make explicit check + 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; + } + + /** + * @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++) { + // FIXME 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 static class ContentPipe implements RevlogStream.Inspector, CancelSupport { + private final ByteChannel sink; + private final CancelSupport cancelSupport; + private Exception failure; + private final int offset; + + /** + * @param _sink - cannot be null + * @param seekOffset - when positive, orders to pipe bytes to the sink starting from specified offset, not from the first byte available in DataAccess + */ + public ContentPipe(ByteChannel _sink, int seekOffset) { + assert _sink != null; + sink = _sink; + cancelSupport = CancelSupport.Factory.get(_sink); + offset = seekOffset; + } + + protected void prepare(int revisionNumber, DataAccess da) throws HgException, IOException { + if (offset > 0) { // save few useless reset/rewind operations + da.seek(offset); + } + } + + public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) { + try { + prepare(revisionNumber, da); // XXX perhaps, prepare shall return DA (sliced, if needed) + final ProgressSupport progressSupport = ProgressSupport.Factory.get(sink); + ByteBuffer buf = ByteBuffer.allocate(512); + progressSupport.start(da.length()); + while (!da.isEmpty()) { + cancelSupport.checkCancelled(); + da.readBytes(buf); + buf.flip(); + // XXX I may not rely on returned number of bytes but track change in buf position instead. + int consumed = sink.write(buf); + // FIXME in fact, bad sink implementation (that consumes no bytes) would result in endless loop. Need to account for this + buf.compact(); + progressSupport.worked(consumed); + } + progressSupport.done(); // XXX shall specify whether #done() is invoked always or only if completed successfully. + } catch (IOException ex) { + recordFailure(ex); + } catch (CancelledException ex) { + recordFailure(ex); + } catch (HgException ex) { + recordFailure(ex); + } + } + + public void checkCancelled() throws CancelledException { + cancelSupport.checkCancelled(); + } + + protected void recordFailure(Exception ex) { + assert failure == null; + failure = ex; + } + + public void checkFailed() throws HgException, IOException, CancelledException { + if (failure == null) { + return; + } + if (failure instanceof IOException) { + throw (IOException) failure; + } + if (failure instanceof CancelledException) { + throw (CancelledException) failure; + } + if (failure instanceof HgException) { + throw (HgException) failure; + } + throw new HgBadStateException(failure); + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/repo/package.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/repo/package.html Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,5 @@ + + +Low-level API operations + + \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/util/Adaptable.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/util/Adaptable.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,28 @@ +/* + * 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; + +/** + * Extension mechanism. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public interface Adaptable { + + T getAdapter(Class adapterClass); +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/util/ByteChannel.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/util/ByteChannel.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,35 @@ +/* + * 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.io.IOException; +import java.nio.ByteBuffer; + +/** + * Much like {@link java.nio.channels.WritableByteChannel} except for thrown exception + * + * XXX Perhaps, we'll add CharChannel in the future to deal with character conversions/encodings + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public interface ByteChannel { + // XXX does int return value makes any sense given buffer keeps its read state + // not clear what retvalue should be in case some filtering happened inside write - i.e. return + // number of bytes consumed in + int write(ByteBuffer buffer) throws IOException, CancelledException; +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/util/CancelSupport.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/util/CancelSupport.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,61 @@ +/* + * 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; + +/** + * Mix-in for objects that support cancellation. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public interface CancelSupport { + + /** + * This method is invoked to check if target had been brought to canceled state. Shall silently return if target is + * in regular state. + * @throws CancelledException when target internal state has been changed to canceled. + */ + void checkCancelled() throws CancelledException; + + + // Yeah, this factory class looks silly now, but perhaps in the future I'll need wrappers for other cancellation sources? + // just don't want to have general Utils class with methods like get() below + static class Factory { + + /** + * Obtain non-null cancel support object. + * + * @param target any object (or null) that might have cancel support + * @return target if it's capable checking cancellation status or no-op implementation that never cancels. + */ + public static CancelSupport get(Object target) { + if (target instanceof CancelSupport) { + return (CancelSupport) target; + } + if (target instanceof Adaptable) { + CancelSupport cs = ((Adaptable) target).getAdapter(CancelSupport.class); + if (cs != null) { + return cs; + } + } + return new CancelSupport() { + public void checkCancelled() { + } + }; + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/util/CancelledException.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/util/CancelledException.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,29 @@ +/* + * 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; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@SuppressWarnings("serial") +public class CancelledException extends Exception { + + public CancelledException() { + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/util/FileIterator.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/util/FileIterator.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,53 @@ +/* + * 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.io.File; + +/** + * Abstracts iteration over file system. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public interface FileIterator { + + /** + * Brings iterator into initial state to facilitate new use. + */ + void reset(); + + /** + * @return whether can shift to next element + */ + boolean hasNext(); + + /** + * Shift to next element + */ + void next(); + + /** + * @return repository-local path to the current element. + */ + Path name(); + + /** + * @return filesystem element. + */ + File file(); +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/util/FileWalker.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/util/FileWalker.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,100 @@ +/* + * 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.io.File; +import java.util.LinkedList; +import java.util.NoSuchElementException; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class FileWalker implements FileIterator { + + private final File startDir; + private final Path.Source pathHelper; + private final LinkedList dirQueue; + private final LinkedList fileQueue; + private File nextFile; + private Path nextPath; + + public FileWalker(File dir, Path.Source pathFactory) { + startDir = dir; + pathHelper = pathFactory; + dirQueue = new LinkedList(); + fileQueue = new LinkedList(); + reset(); + } + + public void reset() { + fileQueue.clear(); + dirQueue.clear(); + dirQueue.add(startDir); + nextFile = null; + nextPath = null; + } + + public boolean hasNext() { + return fill(); + } + + public void next() { + if (!fill()) { + throw new NoSuchElementException(); + } + nextFile = fileQueue.removeFirst(); + nextPath = pathHelper.path(nextFile.getPath()); + } + + public Path name() { + return nextPath; + } + + public File file() { + return nextFile; + } + + private File[] listFiles(File f) { + // in case we need to solve os-related file issues (mac with some encodings?) + return f.listFiles(); + } + + // return true when fill added any elements to fileQueue. + private boolean fill() { + while (fileQueue.isEmpty()) { + if (dirQueue.isEmpty()) { + return false; + } + while (!dirQueue.isEmpty()) { + File dir = dirQueue.removeFirst(); + for (File f : listFiles(dir)) { + if (f.isDirectory()) { + if (!".hg".equals(f.getName())) { + dirQueue.addLast(f); + } + } else { + fileQueue.addLast(f); + } + } + break; + } + } + return !fileQueue.isEmpty(); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/util/Path.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/util/Path.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,121 @@ +/* + * 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; + +/** + * Identify repository files (not String nor io.File). Convenient for pattern matching. Memory-friendly. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public final class Path implements CharSequence, Comparable/*Cloneable? - although clone for paths make no sense*/{ +// private String[] segments; +// private int flags; // dir, unparsed + private String path; + + /*package-local*/Path(String p) { + path = p; + } + + /** + * Check if this is directory's path. + * Note, this method doesn't perform any file system operation. + * + * @return true when this path points to a directory + */ + public boolean isDirectory() { + // XXX simple logic for now. Later we may decide to have an explicit factory method to create directory paths + return path.charAt(path.length() - 1) == '/'; + } + + public int length() { + return path.length(); + } + + public char charAt(int index) { + return path.charAt(index); + } + + public CharSequence subSequence(int start, int end) { + // new Path if start-end matches boundaries of any subpath + return path.substring(start, end); + } + + @Override + public String toString() { + return path; // CharSequence demands toString() impl + } + + public int compareTo(Path o) { + return path.compareTo(o.path); + } + + @Override + public boolean equals(Object obj) { + if (obj != null && getClass() == obj.getClass()) { + return this == obj || path.equals(((Path) obj).path); + } + return false; + } + @Override + public int hashCode() { + return path.hashCode(); + } + + public static Path create(String path) { + if (path == null) { + throw new IllegalArgumentException(); + } + if (path.indexOf('\\') != -1) { + throw new IllegalArgumentException(); + } + Path rv = new Path(path); + return rv; + } + + /** + * Path filter. + */ + public interface Matcher { + boolean accept(Path path); + } + + /** + * Factory for paths + */ + public interface Source { + Path path(String p); + } + + /** + * Straightforward {@link Source} implementation that creates new Path instance for each supplied string + */ + public static class SimpleSource implements Source { + private final PathRewrite normalizer; + + public SimpleSource(PathRewrite pathRewrite) { + if (pathRewrite == null) { + throw new IllegalArgumentException(); + } + normalizer = pathRewrite; + } + + public Path path(String p) { + return Path.create(normalizer.rewrite(p)); + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/util/PathPool.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/util/PathPool.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,81 @@ +/* + * 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); + return get(p, true); + } + + // pipes path object through cache to reuse instance, if possible + 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); + } + + 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 edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/util/PathRewrite.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/util/PathRewrite.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,61 @@ +/* + * 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.util.LinkedList; +import java.util.List; + +/** + * File names often need transformations, like Windows-style path to Unix or human-readable data file name to storage location. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public interface PathRewrite { + + // XXX think over CharSequence use instead of String + public String rewrite(String path); + + public static class Empty implements PathRewrite { + public String rewrite(String path) { + return path; + } + } + + public class Composite implements PathRewrite { + private List chain; + + public Composite(PathRewrite... e) { + LinkedList r = new LinkedList(); + for (int i = 0; e != null && i < e.length; i++) { + r.addLast(e[i]); + } + chain = r; + } + public Composite chain(PathRewrite e) { + chain.add(e); + return this; + } + + public String rewrite(String path) { + for (PathRewrite pr : chain) { + path = pr.rewrite(path); + } + return path; + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/main/java/org/tmatesoft/hg/util/ProgressSupport.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/main/java/org/tmatesoft/hg/util/ProgressSupport.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,57 @@ +/* + * 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; + +/** + * Mix-in to report progress of a long-running operation + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public interface ProgressSupport { + + public void start(long totalUnits); + public void worked(int units); + public void done(); + + static class Factory { + + /** + * @param target object that might be capable to report progress. Can be null + * @return support object extracted from target or an empty, no-op implementation + */ + public static ProgressSupport get(Object target) { + if (target instanceof ProgressSupport) { + return (ProgressSupport) target; + } + if (target instanceof Adaptable) { + ProgressSupport ps = ((Adaptable) target).getAdapter(ProgressSupport.class); + if (ps != null) { + return ps; + } + } + return new ProgressSupport() { + public void start(long totalUnits) { + } + public void worked(int units) { + } + public void done() { + } + }; + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/java/org/tmatesoft/hg/test/Configuration.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/test/java/org/tmatesoft/hg/test/Configuration.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,101 @@ +/* + * 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.test; + +import static org.junit.Assert.*; + +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.tmatesoft.hg.repo.HgLookup; +import org.tmatesoft.hg.repo.HgRemoteRepository; +import org.tmatesoft.hg.repo.HgRepository; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class Configuration { + + private static Configuration inst; + private File root; + private final HgLookup lookup; + private File tempDir; + private List remoteServers; + + private Configuration() { + lookup = new HgLookup(); + } + + private File getRoot() { + if (root == null) { + String repo2 = System.getProperty("hg4j.tests.repos"); + assertNotNull("System property hg4j.tests.repos is undefined", repo2); + root = new File(repo2); + assertTrue(root.exists()); + } + return root; + } + + public static Configuration get() { + if (inst == null) { + inst = new Configuration(); + } + return inst; + } + + public HgRepository own() throws Exception { + return lookup.detectFromWorkingDir(); + } + + // fails if repo not found + public HgRepository find(String key) throws Exception { + HgRepository rv = lookup.detect(new File(getRoot(), key)); + assertNotNull(rv); + assertFalse(rv.isInvalid()); + return rv; + } + + // easy override for manual test runs + public void remoteServers(String... keys) { + remoteServers = Arrays.asList(keys); + } + + public List allRemote() throws Exception { + if (remoteServers == null) { + String rr = System.getProperty("hg4j.tests.remote"); + assertNotNull("System property hg4j.tests.remote is undefined", rr); + remoteServers = Arrays.asList(rr.split(" ")); + } + ArrayList rv = new ArrayList(remoteServers.size()); + for (String key : remoteServers) { + rv.add(lookup.detectRemote(key, null)); + } + return rv; + } + + public File getTempDir() { + if (tempDir == null) { + String td = System.getProperty("hg4j.tests.tmpdir", System.getProperty("java.io.tmpdir")); + tempDir = new File(td); + } + return tempDir; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/java/org/tmatesoft/hg/test/ErrorCollectorExt.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/test/java/org/tmatesoft/hg/test/ErrorCollectorExt.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,45 @@ +/* + * 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.test; + +import static org.junit.Assert.assertThat; + +import java.util.concurrent.Callable; + +import org.hamcrest.Matcher; +import org.junit.rules.ErrorCollector; + +/** + * Expose verify method for allow not-junit runs to check test outcome + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +final class ErrorCollectorExt extends ErrorCollector { + public void verify() throws Throwable { + super.verify(); + } + + public void checkThat(final String reason, final T value, final Matcher matcher) { + checkSucceeds(new Callable() { + public Object call() throws Exception { + assertThat(reason, value, matcher); + return value; + } + }); + } +} \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/java/org/tmatesoft/hg/test/ExecHelper.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/test/java/org/tmatesoft/hg/test/ExecHelper.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,101 @@ +/* + * 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.test; + +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.StringTokenizer; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class ExecHelper { + + private final OutputParser parser; + private File dir; + private int exitValue; + + public ExecHelper(OutputParser outParser, File workingDir) { + parser = outParser; + dir = workingDir; + } + + public void run(String... cmd) throws IOException, InterruptedException { + ProcessBuilder pb = null; + 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; + } + // PATHEXT controls precedence of .exe, .bat and .cmd files, ususlly .exe wins + if (new File(pe, cmd[0] + ".bat").exists() || new File(pe, cmd[0] + ".cmd").exists()) { + ArrayList command = new ArrayList(); + command.add("cmd.exe"); + command.add("/C"); + command.addAll(Arrays.asList(cmd)); + pb = new ProcessBuilder(command); + break; + } + } + } + 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(); + parser.parse(res); + } + + public int getExitValue() { + return exitValue; + } + + public void cwd(File wd) { + dir = wd; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/java/org/tmatesoft/hg/test/LogOutputParser.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/test/java/org/tmatesoft/hg/test/LogOutputParser.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,106 @@ +/* + * 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.test; + +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.tmatesoft.hg.repo.HgRepository; + + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class LogOutputParser implements OutputParser { + private final List result = new LinkedList(); + private Pattern pattern1; + private Pattern pattern2; + private Pattern pattern3; + private Pattern pattern4; + private Pattern pattern5; + + public LogOutputParser(boolean outputWithDebug) { + if (outputWithDebug) { + pattern1 = Pattern.compile("^changeset:\\s+(\\d+):([a-f0-9]{40})\n(^tag:(.+)$)?", Pattern.MULTILINE); + pattern2 = Pattern.compile("^parent:\\s+(-?\\d+):([a-f0-9]{40})\n", Pattern.MULTILINE); + pattern3 = Pattern.compile("^manifest:\\s+(\\d+):([a-f0-9]{40})\nuser:\\s+(\\S.+)\ndate:\\s+(\\S.+)\n", Pattern.MULTILINE); + pattern4 = Pattern.compile("^description:\\n", Pattern.MULTILINE); + pattern5 = Pattern.compile("\\n\\n"); + //p = "^manifest:\\s+(\\d+):([a-f0-9]{40})\nuser:(.+)$"; + } else { + throw HgRepository.notImplemented(); + } + } + + public void reset() { + result.clear(); + } + + public List getResult() { + return result; + } + + public void parse(CharSequence seq) { + Matcher m = pattern1.matcher(seq); + while (m.find()) { + Record r = new Record(); + r.changesetIndex = Integer.parseInt(m.group(1)); + r.changesetNodeid = m.group(2); + //tags = m.group(4); + m.usePattern(pattern2); + if (m.find()) { + r.parent1Index = Integer.parseInt(m.group(1)); + r.parent1Nodeid = m.group(2); + } + if (m.find()) { + r.parent2Index = Integer.parseInt(m.group(1)); + r.parent2Nodeid = m.group(2); + } + m.usePattern(pattern3); + if (m.find()) { + r.user = m.group(3); + r.date = m.group(4); + } + m.usePattern(pattern4); + if (m.find()) { + int commentStart = m.end(); + m.usePattern(pattern5); + if (m.find()) { + r.description = seq.subSequence(commentStart, m.start()).toString(); + } + } + result.add(r); + m.usePattern(pattern1); + } + } + + public static class Record { + public int changesetIndex; + public String changesetNodeid; + public int parent1Index; + public int parent2Index; + public String parent1Nodeid; + public String parent2Nodeid; + public String user; + public String date; + public String description; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/java/org/tmatesoft/hg/test/ManifestOutputParser.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/test/java/org/tmatesoft/hg/test/ManifestOutputParser.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,56 @@ +/* + * 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.test; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.util.Path; + + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class ManifestOutputParser implements OutputParser { + + private final Pattern pattern; + private final LinkedHashMap result = new LinkedHashMap(); + + public ManifestOutputParser() { + pattern = Pattern.compile("^([a-f0-9]{40}) (\\d{3}) (.+)$", Pattern.MULTILINE); + } + + public void reset() { + result.clear(); + } + + public Map getResult() { + return result; + } + + public void parse(CharSequence seq) { + Matcher m = pattern.matcher(seq); + while (m.find()) { + result.put(Path.create(m.group(3)), Nodeid.fromAscii(m.group(1).getBytes(), 0, 40)); + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/java/org/tmatesoft/hg/test/OutputParser.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/test/java/org/tmatesoft/hg/test/OutputParser.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,43 @@ +/* + * 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.test; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public interface OutputParser { + + public void parse(CharSequence seq); + + public class Stub implements OutputParser { + private boolean shallDump; + public Stub() { + this(false); + } + public Stub(boolean dump) { + shallDump = dump; + } + public void parse(CharSequence seq) { + if (shallDump) { + System.out.println(seq); + } + // else no-op + } + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/java/org/tmatesoft/hg/test/StatusOutputParser.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/test/java/org/tmatesoft/hg/test/StatusOutputParser.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,148 @@ +/* + * 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.test; + +import java.io.File; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.tmatesoft.hg.repo.HgStatusCollector; +import org.tmatesoft.hg.util.Path; +import org.tmatesoft.hg.util.PathPool; +import org.tmatesoft.hg.util.PathRewrite; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class StatusOutputParser implements OutputParser { + + private final Pattern pattern; + // although using StatusCollector.Record is not really quite honest for testing, + // it's deemed acceptable as long as that class is primitive 'collect all results' + private HgStatusCollector.Record result = new HgStatusCollector.Record(); + private final PathPool pathHelper; + + public StatusOutputParser() { +// pattern = Pattern.compile("^([MAR?IC! ]) ([\\w \\.-/\\\\]+)$", Pattern.MULTILINE); + pattern = Pattern.compile("^([MAR?IC! ]) (.+)$", Pattern.MULTILINE); + pathHelper = new PathPool(new PathRewrite() { + + private final boolean winPathSeparator = File.separatorChar == '\\'; + + public String rewrite(String s) { + if (winPathSeparator) { + // Java impl always give slashed path, while Hg uses local, os-specific convention + s = s.replace('\\', '/'); + } + return s; + } + }); + } + + public void reset() { + result = new HgStatusCollector.Record(); + } + + public void parse(CharSequence seq) { + Matcher m = pattern.matcher(seq); + Path lastEntry = null; + while (m.find()) { + Path fname = pathHelper.path(m.group(2)); + switch ((int) m.group(1).charAt(0)) { + case (int) 'M' : { + result.modified(fname); + lastEntry = fname; // for files modified through merge there's also 'copy' source + break; + } + case (int) 'A' : { + result.added(fname); + lastEntry = fname; + break; + } + case (int) 'R' : { + result.removed(fname); + break; + } + case (int) '?' : { + result.unknown(fname); + break; + } + case (int) 'I' : { + result.ignored(fname); + break; + } + case (int) 'C' : { + result.clean(fname); + break; + } + case (int) '!' : { + result.missing(fname); + break; + } + case (int) ' ' : { + // last added is copy destination + // to get or to remove it - depends on what StatusCollector does in this case + result.copied(fname, lastEntry); + lastEntry = null; + break; + } + } + } + } + + // + public List getModified() { + return result.getModified(); + } + + public List getAdded() { + List rv = new LinkedList(result.getAdded()); + for (Path p : result.getCopied().keySet()) { + rv.remove(p); // remove only one duplicate + } + return rv; + } + + public List getRemoved() { + return result.getRemoved(); + } + + public Map getCopied() { + return result.getCopied(); + } + + public List getClean() { + return result.getClean(); + } + + public List getMissing() { + return result.getMissing(); + } + + public List getUnknown() { + return result.getUnknown(); + } + + public List getIgnored() { + return result.getIgnored(); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/java/org/tmatesoft/hg/test/TestByteChannel.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/test/java/org/tmatesoft/hg/test/TestByteChannel.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,87 @@ +/* + * 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.test; + +import static org.junit.Assert.assertArrayEquals; + +import org.junit.Assert; +import org.junit.Test; +import org.tmatesoft.hg.internal.ByteArrayChannel; +import org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgRepository; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class TestByteChannel { + + private HgRepository repo; + + public static void main(String[] args) throws Exception { +// HgRepoFacade rf = new HgRepoFacade(); +// rf.init(); +// HgDataFile file = rf.getRepository().getFileNode("src/org/tmatesoft/hg/internal/KeywordFilter.java"); +// for (int i = file.getLastRevision(); i >= 0; i--) { +// System.out.print("Content for revision:" + i); +// compareContent(file, i); +// System.out.println(" OK"); +// } + //CatCommand cmd = rf.createCatCommand(); + } + +// private static void compareContent(HgDataFile file, int rev) throws Exception { +// byte[] oldAccess = file.content(rev); +// ByteArrayChannel ch = new ByteArrayChannel(); +// file.content(rev, ch); +// byte[] newAccess = ch.toArray(); +// Assert.assertArrayEquals(oldAccess, newAccess); +// // don't trust anyone (even JUnit) +// if (!Arrays.equals(oldAccess, newAccess)) { +// throw new RuntimeException("Failed:" + rev); +// } +// } + + @Test + public void testContent() throws Exception { + repo = Configuration.get().find("log-1"); + final byte[] expectedContent = new byte[] { 'a', ' ', 13, 10 }; + ByteArrayChannel ch = new ByteArrayChannel(); + repo.getFileNode("dir/b").content(0, ch); + assertArrayEquals(expectedContent, ch.toArray()); + repo.getFileNode("d").content(HgRepository.TIP, ch = new ByteArrayChannel() ); + assertArrayEquals(expectedContent, ch.toArray()); + } + + @Test + public void testStripMetadata() throws Exception { + repo = Configuration.get().find("log-1"); + ByteArrayChannel ch = new ByteArrayChannel(); + HgDataFile dir_b = repo.getFileNode("dir/b"); + Assert.assertTrue(dir_b.isCopy()); + Assert.assertEquals("b", dir_b.getCopySourceName().toString()); + Assert.assertEquals("e44751cdc2d14f1eb0146aa64f0895608ad15917", dir_b.getCopySourceRevision().toString()); + dir_b.content(0, ch); + // assert rawContent has 1 10 ... 1 10 + assertArrayEquals("a \r\n".getBytes(), ch.toArray()); + // + // try once again to make sure metadata records/extracts correct offsets + dir_b.content(0, ch = new ByteArrayChannel()); + assertArrayEquals("a \r\n".getBytes(), ch.toArray()); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/java/org/tmatesoft/hg/test/TestClone.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/test/java/org/tmatesoft/hg/test/TestClone.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,94 @@ +/* + * 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.test; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import org.hamcrest.CoreMatchers; +import org.junit.Rule; +import org.junit.Test; +import org.tmatesoft.hg.core.HgCloneCommand; +import org.tmatesoft.hg.repo.HgRemoteRepository; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class TestClone { + + @Rule + public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); + + public static void main(String[] args) throws Throwable { + TestClone t = new TestClone(); + t.testSimpleClone(); + t.errorCollector.verify(); + } + + public TestClone() { + } + + @Test + public void testSimpleClone() throws Exception { + int x = 0; + final File tempDir = Configuration.get().getTempDir(); + for (HgRemoteRepository hgRemote : Configuration.get().allRemote()) { + HgCloneCommand cmd = new HgCloneCommand(); + cmd.source(hgRemote); + File dest = new File(tempDir, "test-clone-" + x++); + if (dest.exists()) { + rmdir(dest); + } + cmd.destination(dest); + cmd.execute(); + verify(hgRemote, dest); + } + } + + private void verify(HgRemoteRepository hgRemote, File dest) throws Exception { + ExecHelper eh = new ExecHelper(new OutputParser.Stub(), dest); + eh.run("hg", "verify"); + errorCollector.checkThat("Verify", eh.getExitValue(), CoreMatchers.equalTo(0)); + eh.run("hg", "out", hgRemote.getLocation()); + errorCollector.checkThat("Outgoing", eh.getExitValue(), CoreMatchers.equalTo(1)); + eh.run("hg", "in", hgRemote.getLocation()); + errorCollector.checkThat("Incoming", eh.getExitValue(), CoreMatchers.equalTo(1)); + } + + static void rmdir(File dest) throws IOException { + LinkedList queue = new LinkedList(); + queue.addAll(Arrays.asList(dest.listFiles())); + while (!queue.isEmpty()) { + File next = queue.removeFirst(); + if (next.isDirectory()) { + List files = Arrays.asList(next.listFiles()); + if (!files.isEmpty()) { + queue.addAll(files); + queue.add(next); + } + // fall through + } + next.delete(); + } + dest.delete(); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/java/org/tmatesoft/hg/test/TestHistory.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/test/java/org/tmatesoft/hg/test/TestHistory.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,240 @@ +/* + * 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.test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import org.junit.Rule; +import org.junit.Test; +import org.tmatesoft.hg.core.HgChangeset; +import org.tmatesoft.hg.core.HgLogCommand; +import org.tmatesoft.hg.core.HgLogCommand.CollectHandler; +import org.tmatesoft.hg.core.HgLogCommand.FileHistoryHandler; +import org.tmatesoft.hg.core.HgLogCommand.FileRevision; +import org.tmatesoft.hg.repo.HgLookup; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.test.LogOutputParser.Record; +import org.tmatesoft.hg.util.Path; + + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class TestHistory { + + @Rule + public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); + + private HgRepository repo; + private final ExecHelper eh; + private LogOutputParser changelogParser; + + public static void main(String[] args) throws Throwable { + TestHistory th = new TestHistory(); + th.testCompleteLog(); + th.testFollowHistory(); + th.errorCollector.verify(); +// th.testPerformance(); + th.testOriginalTestLogRepo(); + th.testUsernames(); + th.testBranches(); + // + th.errorCollector.verify(); + } + + public TestHistory() throws Exception { + this(new HgLookup().detectFromWorkingDir()); + } + + private TestHistory(HgRepository hgRepo) { + repo = hgRepo; + eh = new ExecHelper(changelogParser = new LogOutputParser(true), null); + + } + + @Test + public void testCompleteLog() throws Exception { + changelogParser.reset(); + eh.run("hg", "log", "--debug"); + List r = new HgLogCommand(repo).execute(); + report("hg log - COMPLETE REPO HISTORY", r, true); + } + + @Test + public void testFollowHistory() throws Exception { + final Path f = Path.create("cmdline/org/tmatesoft/hg/console/Remote.java"); + try { + if (repo.getFileNode(f).exists()) { // FIXME getFileNode shall not fail with IAE + changelogParser.reset(); + eh.run("hg", "log", "--debug", "--follow", f.toString()); + + class H extends CollectHandler implements FileHistoryHandler { + boolean copyReported = false; + boolean fromMatched = false; + public void copy(FileRevision from, FileRevision to) { + copyReported = true; + fromMatched = "src/com/tmate/hgkit/console/Remote.java".equals(from.getPath().toString()); + } + }; + H h = new H(); + new HgLogCommand(repo).file(f, true).execute(h); + String what = "hg log - FOLLOW FILE HISTORY"; + errorCollector.checkThat(what + "#copyReported ", h.copyReported, is(true)); + errorCollector.checkThat(what + "#copyFromMatched", h.fromMatched, is(true)); + // + // cmdline always gives in changesets in order from newest (bigger rev number) to oldest. + // LogCommand does other way round, from oldest to newest, follewed by revisions of copy source, if any + // (apparently older than oldest of the copy target). Hence need to sort Java results according to rev numbers + 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; + } + }); + report(what, sorted, false); + } + } catch (IllegalArgumentException ex) { + System.out.println("Can't test file history with follow because need to query specific file with history"); + } + } + + private void report(String what, List r, boolean reverseConsoleResult) { + final List consoleResult = changelogParser.getResult(); + report(what, r, consoleResult, reverseConsoleResult, errorCollector); + } + + static void report(String what, List hg4jResult, List consoleResult, boolean reverseConsoleResult, ErrorCollectorExt errorCollector) { + consoleResult = new ArrayList(consoleResult); // need a copy in case callee would use result again + if (reverseConsoleResult) { + Collections.reverse(consoleResult); + } + errorCollector.checkThat(what + ". Number of changeset reported didn't match", consoleResult.size(), equalTo(hg4jResult.size())); + Iterator consoleResultItr = consoleResult.iterator(); + for (HgChangeset cs : hg4jResult) { + if (!consoleResultItr.hasNext()) { + errorCollector.addError(new AssertionError("Ran out of console results while there are still hg4j results")); + break; + } + Record cr = consoleResultItr.next(); + int x = cs.getRevision() == cr.changesetIndex ? 0x1 : 0; + x |= cs.getDate().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)); + consoleResultItr.remove(); + } + errorCollector.checkThat(what + ". Unprocessed results in console left (insufficient from hg4j)", consoleResultItr.hasNext(), equalTo(false)); + } + + public void testPerformance() throws Exception { + final int runs = 10; + final long start1 = System.currentTimeMillis(); + for (int i = 0; i < runs; i++) { + changelogParser.reset(); + eh.run("hg", "log", "--debug"); + } + final long start2 = System.currentTimeMillis(); + for (int i = 0; i < runs; i++) { + new HgLogCommand(repo).execute(); + } + final long end = System.currentTimeMillis(); + System.out.printf("'hg log --debug', %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); + } + + @Test + public void testOriginalTestLogRepo() throws Exception { + 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 + // + changelogParser.reset(); + eh.run("hg", "log", "--debug", "a", "--cwd", repo.getLocation()); + 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); + // + changelogParser.reset(); + eh.run("hg", "log", "--debug", "-f", "e", "--cwd", repo.getLocation()); + report("log -f e", cmd.file("e", true).execute(), false /*#1, below*/); + // + changelogParser.reset(); + eh.run("hg", "log", "--debug", "dir/b", "--cwd", repo.getLocation()); + 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*/); + /* + * #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. + */ + } + + @Test + public void testUsernames() throws Exception { + repo = Configuration.get().find("log-users"); + final String user1 = "User One "; + // + changelogParser.reset(); + eh.run("hg", "log", "--debug", "-u", user1, "--cwd", repo.getLocation()); + report("log -u " + user1, new HgLogCommand(repo).user(user1).execute(), true); + // + changelogParser.reset(); + eh.run("hg", "log", "--debug", "-u", "user1", "-u", "user2", "--cwd", repo.getLocation()); + report("log -u user1 -u user2", new HgLogCommand(repo).user("user1").user("user2").execute(), true); + // + changelogParser.reset(); + eh.run("hg", "log", "--debug", "-u", "user3", "--cwd", repo.getLocation()); + report("log -u user3", new HgLogCommand(repo).user("user3").execute(), true); + } + + @Test + public void testBranches() throws Exception { + repo = Configuration.get().find("log-branches"); + changelogParser.reset(); + eh.run("hg", "log", "--debug", "-b", "default", "--cwd", repo.getLocation()); + report("log -b default" , new HgLogCommand(repo).branch("default").execute(), true); + // + changelogParser.reset(); + eh.run("hg", "log", "--debug", "-b", "test", "--cwd", repo.getLocation()); + report("log -b test" , new HgLogCommand(repo).branch("test").execute(), true); + // + assertTrue("log -b dummy shall yeild empty result", new HgLogCommand(repo).branch("dummy").execute().isEmpty()); + // + changelogParser.reset(); + eh.run("hg", "log", "--debug", "-b", "default", "-b", "test", "--cwd", repo.getLocation()); + report("log -b default -b test" , new HgLogCommand(repo).branch("default").branch("test").execute(), true); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/java/org/tmatesoft/hg/test/TestIncoming.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/test/java/org/tmatesoft/hg/test/TestIncoming.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,142 @@ +/* + * 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.test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.tmatesoft.hg.internal.RequiresFile.*; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.tmatesoft.hg.core.HgChangeset; +import org.tmatesoft.hg.core.HgIncomingCommand; +import org.tmatesoft.hg.core.HgLogCommand; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.Internals; +import org.tmatesoft.hg.repo.HgLookup; +import org.tmatesoft.hg.repo.HgRemoteRepository; +import org.tmatesoft.hg.repo.HgRepository; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class TestIncoming { + + @Rule + public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); + + public static void main(String[] args) throws Throwable { + Configuration.get().remoteServers("http://localhost:8000/"); + TestIncoming t = new TestIncoming(); + t.testSimple(); + t.errorCollector.verify(); + } + + public TestIncoming() { +// Configuration.get().remoteServers("http://localhost:8000/"); + } + + @Test + public void testSimple() throws Exception { + int x = 0; + HgLookup lookup = new HgLookup(); + for (HgRemoteRepository hgRemote : Configuration.get().allRemote()) { + File dest = initEmptyTempRepo("test-incoming-" + x++); + HgRepository localRepo = lookup.detect(dest); + // Idea: + // hg in, hg4j in, compare + // hg pull total/2 + // hg in, hg4j in, compare + List incoming = runAndCompareIncoming(localRepo, hgRemote); + Assert.assertTrue("Need remote repository of reasonable size to test incoming command for partially filled case", incoming.size() >= 5); + // + Nodeid median = incoming.get(incoming.size() / 2); + System.out.println("About to pull up to revision " + median.shortNotation()); + new ExecHelper(new OutputParser.Stub(), dest).run("hg", "pull", "-r", median.toString(), hgRemote.getLocation()); + // + // shall re-read repository to pull up new changes + localRepo = lookup.detect(dest); + runAndCompareIncoming(localRepo, hgRemote); + } + } + + private List runAndCompareIncoming(HgRepository localRepo, HgRemoteRepository hgRemote) throws Exception { + // need new command instance as subsequence exec[Lite|Full] on the same command would yield same result, + // regardless of the pull in between. + HgIncomingCommand cmd = new HgIncomingCommand(localRepo); + cmd.against(hgRemote); + HgLogCommand.CollectHandler collector = new HgLogCommand.CollectHandler(); + LogOutputParser outParser = new LogOutputParser(true); + ExecHelper eh = new ExecHelper(outParser, new File(localRepo.getLocation())); + cmd.executeFull(collector); + eh.run("hg", "incoming", "--debug", hgRemote.getLocation()); + List liteResult = cmd.executeLite(null); + report(collector, outParser, liteResult, errorCollector); + return liteResult; + } + + static void report(HgLogCommand.CollectHandler collector, LogOutputParser outParser, List liteResult, ErrorCollectorExt errorCollector) { + TestHistory.report("hg vs execFull", collector.getChanges(), outParser.getResult(), false, errorCollector); + // + ArrayList expected = new ArrayList(outParser.getResult().size()); + for (LogOutputParser.Record r : outParser.getResult()) { + Nodeid nid = Nodeid.fromAscii(r.changesetNodeid); + expected.add(nid); + } + checkNodeids("hg vs execLite:", liteResult, expected, errorCollector); + // + expected = new ArrayList(outParser.getResult().size()); + for (HgChangeset cs : collector.getChanges()) { + expected.add(cs.getNodeid()); + } + checkNodeids("execFull vs execLite:", liteResult, expected, errorCollector); + } + + static void checkNodeids(String what, List liteResult, List expected, ErrorCollectorExt errorCollector) { + HashSet set = new HashSet(liteResult); + for (Nodeid nid : expected) { + boolean removed = set.remove(nid); + errorCollector.checkThat(what + " Missing " + nid.shortNotation() + " in HgIncomingCommand.execLite result", removed, equalTo(true)); + } + errorCollector.checkThat(what + " Superfluous cset reported by HgIncomingCommand.execLite", set.isEmpty(), equalTo(true)); + } + + static File createEmptyDir(String dirName) throws IOException { + File dest = new File(Configuration.get().getTempDir(), dirName); + if (dest.exists()) { + TestClone.rmdir(dest); + } + dest.mkdirs(); + return dest; + } + + static File initEmptyTempRepo(String dirName) throws IOException { + File dest = createEmptyDir(dirName); + Internals implHelper = new Internals(); + implHelper.setStorageConfig(1, STORE | FNCACHE | DOTENCODE); + implHelper.initEmptyRepository(new File(dest, ".hg")); + return dest; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/java/org/tmatesoft/hg/test/TestManifest.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/test/java/org/tmatesoft/hg/test/TestManifest.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,120 @@ +/* + * 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.test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.junit.Assert.assertTrue; +import static org.tmatesoft.hg.repo.HgRepository.TIP; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.Map; + +import org.junit.Rule; +import org.junit.Test; +import org.tmatesoft.hg.core.HgLogCommand.FileRevision; +import org.tmatesoft.hg.core.HgManifestCommand; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.HgLookup; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.Path; + + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class TestManifest { + + @Rule + public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); + + private final HgRepository repo; + private ManifestOutputParser manifestParser; + private ExecHelper eh; + final LinkedList revisions = new LinkedList(); + private HgManifestCommand.Handler handler = new HgManifestCommand.Handler() { + + public void file(FileRevision fileRevision) { + revisions.add(fileRevision); + } + + public void end(Nodeid manifestRevision) {} + public void dir(Path p) {} + public void begin(Nodeid manifestRevision) {} + }; + + public static void main(String[] args) throws Throwable { + TestManifest tm = new TestManifest(); + tm.testTip(); + tm.testFirstRevision(); + tm.testRevisionInTheMiddle(); + tm.errorCollector.verify(); + } + + public TestManifest() throws Exception { + this(new HgLookup().detectFromWorkingDir()); + } + + private TestManifest(HgRepository hgRepo) { + repo = hgRepo; + assertTrue(!repo.isInvalid()); + eh = new ExecHelper(manifestParser = new ManifestOutputParser(), null); + } + + @Test + public void testTip() throws Exception { + testRevision(TIP); + } + + @Test + public void testFirstRevision() throws Exception { + testRevision(0); + } + + @Test + public void testRevisionInTheMiddle() throws Exception { + int rev = repo.getManifest().getRevisionCount() / 2; + if (rev == 0) { + throw new IllegalStateException("Need manifest with few revisions"); + } + testRevision(rev); + } + + private void testRevision(int rev) throws Exception { + manifestParser.reset(); + eh.run("hg", "manifest", "--debug", "--rev", String.valueOf(rev == TIP ? -1 : rev)); + revisions.clear(); + new HgManifestCommand(repo).revision(rev).execute(handler); + report("manifest " + (rev == TIP ? "TIP:" : "--rev " + rev)); + } + + private void report(String what) throws Exception { + final Map cmdLineResult = new LinkedHashMap(manifestParser.getResult()); + for (FileRevision fr : revisions) { + Nodeid nid = cmdLineResult.remove(fr.getPath()); + errorCollector.checkThat("Extra " + fr.getPath() + " in Java result", nid, notNullValue()); + if (nid != null) { + errorCollector.checkThat("Non-matching nodeid:" + nid, nid, equalTo(fr.getRevision())); + } + } + errorCollector.checkThat("Non-matched entries from command line:", cmdLineResult, equalTo(Collections.emptyMap())); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/java/org/tmatesoft/hg/test/TestOutgoing.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/test/java/org/tmatesoft/hg/test/TestOutgoing.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,94 @@ +/* + * 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.test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.util.List; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.tmatesoft.hg.core.HgLogCommand; +import org.tmatesoft.hg.core.HgOutgoingCommand; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.HgLookup; +import org.tmatesoft.hg.repo.HgRemoteRepository; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class TestOutgoing { + + @Rule + public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); + + public static void main(String[] args) throws Throwable { + Configuration.get().remoteServers("http://localhost:8000/"); + TestOutgoing t = new TestOutgoing(); + t.testSimple(); + t.errorCollector.verify(); + } + + public TestOutgoing() { + } + + @Test + public void testSimple() throws Exception { + int x = 0; + HgLookup lookup = new HgLookup(); + for (HgRemoteRepository hgRemote : Configuration.get().allRemote()) { + File dest = TestIncoming.createEmptyDir("test-outgoing-" + x++); + ExecHelper eh0 = new ExecHelper(new OutputParser.Stub(false), null); + eh0.run("hg", "clone", hgRemote.getLocation(), dest.toString()); + eh0.cwd(dest); + Assert.assertEquals("initial clone failed", 0, eh0.getExitValue()); + HgOutgoingCommand cmd = new HgOutgoingCommand(lookup.detect(dest)).against(hgRemote); + LogOutputParser outParser = new LogOutputParser(true); + ExecHelper eh = new ExecHelper(outParser, dest); + HgLogCommand.CollectHandler collector = new HgLogCommand.CollectHandler(); + // + cmd.executeFull(collector); + List liteResult = cmd.executeLite(null); + eh.run("hg", "outgoing", "--debug", hgRemote.getLocation()); + TestIncoming.report(collector, outParser, liteResult, errorCollector); + // + File f = new File(dest, "Test.txt"); + append(f, "1"); + eh0.run("hg", "add"); + eh0.run("hg", "commit", "-m", "1"); + append(f, "2"); + eh0.run("hg", "commit", "-m", "2"); + // + cmd = new HgOutgoingCommand(lookup.detect(dest)).against(hgRemote); + cmd.executeFull(collector = new HgLogCommand.CollectHandler()); + liteResult = cmd.executeLite(null); + outParser.reset(); + eh.run("hg", "outgoing", "--debug", hgRemote.getLocation()); + TestIncoming.report(collector, outParser, liteResult, errorCollector); + } + } + + static void append(File f, String s) throws IOException { + FileWriter fw = new FileWriter(f); + fw.append(s); + fw.close(); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/java/org/tmatesoft/hg/test/TestStatus.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/test/java/org/tmatesoft/hg/test/TestStatus.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,242 @@ +/* + * 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.test; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.tmatesoft.hg.core.HgStatus.*; +import static org.tmatesoft.hg.core.HgStatus.Kind.*; +import static org.tmatesoft.hg.repo.HgRepository.TIP; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.junit.Assume; +import org.junit.Rule; +import org.junit.Test; +import org.tmatesoft.hg.core.HgStatus; +import org.tmatesoft.hg.core.HgStatusCommand; +import org.tmatesoft.hg.repo.HgLookup; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgStatusCollector; +import org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector; +import org.tmatesoft.hg.util.Path; + + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class TestStatus { + + @Rule + public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); + + private final HgRepository repo; + private StatusOutputParser statusParser; + private ExecHelper eh; + + public static void main(String[] args) throws Throwable { + TestStatus test = new TestStatus(); + test.testLowLevel(); + test.testStatusCommand(); + test.testPerformance(); + test.errorCollector.verify(); + } + + public TestStatus() throws Exception { + this(new HgLookup().detectFromWorkingDir()); + } + + private TestStatus(HgRepository hgRepo) { + repo = hgRepo; + Assume.assumeTrue(!repo.isInvalid()); + statusParser = new StatusOutputParser(); + eh = new ExecHelper(statusParser, null); + } + + @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); + // + 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); + // + 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); + // + 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); + } + + @Test + public void testStatusCommand() throws Exception { + final HgStatusCommand sc = new HgStatusCommand(repo).all(); + StatusCollector r; + statusParser.reset(); + eh.run("hg", "status", "-A"); + sc.execute(r = new StatusCollector()); + 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); + // + 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); + + // TODO check not -A, but defaults()/custom set of modifications + } + + private static class StatusCollector implements HgStatusCommand.Handler { + private final Map> map = new TreeMap>(); + + public void handleStatus(HgStatus s) { + List l = map.get(s.getKind()); + if (l == null) { + l = new LinkedList(); + map.put(s.getKind(), l); + } + l.add(s.getPath()); + } + + public List get(Kind k) { + List rv = map.get(k); + if (rv == null) { + return Collections.emptyList(); + } + return rv; + } + } + + public void testRemovedAgainstNonTip() { + /* + status --rev N when a file added past revision N was removed ((both physically and in dirstate), but not yet committed + + Reports extra REMOVED file (the one added and removed in between). Shall not + */ + } + + /* + * 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) + * 'hg status -A --rev 3:80': Native client total 1828 (182 per run), Java client 235 (23) + * '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 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) + */ + public void testPerformance() throws Exception { + final int runs = 10; + final long start1 = System.currentTimeMillis(); + for (int i = 0; i < runs; i++) { + statusParser.reset(); + eh.run("hg", "status", "-A", "--rev", "3:80"); + } + final long start2 = System.currentTimeMillis(); + for (int i = 0; i < runs; i++) { + StatusCollector r = new StatusCollector(); + 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); + } + + private void report(String what, StatusCollector r) { + 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 + } + + 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); + } + } 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())); + } + + private void reportNotEqual(String what, Collection l1, Collection l2) { + List diff = difference(l1, l2); + errorCollector.checkThat(what, diff, equalTo(Collections.emptyList())); + } + + 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); + } + } + return result; + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/java/org/tmatesoft/hg/test/TestStorePath.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/hg4j/src/test/java/org/tmatesoft/hg/test/TestStorePath.java Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,73 @@ +/* + * 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.test; + +import static org.hamcrest.CoreMatchers.equalTo; +import junit.framework.Assert; + +import org.junit.Rule; +import org.junit.Test; +import org.tmatesoft.hg.internal.Internals; +import org.tmatesoft.hg.util.PathRewrite; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class TestStorePath { + + @Rule + public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); + + private PathRewrite storePathHelper; + + public static void main(String[] args) throws Throwable { + final TestStorePath test = new TestStorePath(); + test.testWindowsFilenames(); + test.testHashLongPath(); + test.errorCollector.verify(); + } + + public TestStorePath() { + final Internals i = new Internals(); + i.setStorageConfig(1, 0x7); + storePathHelper = i.buildDataFilesHelper(); + } + + @Test + public void testWindowsFilenames() { + // see http://mercurial.selenic.com/wiki/fncacheRepoFormat#Encoding_of_Windows_reserved_names + String n1 = "aux.bla/bla.aux/prn/PRN/lpt/com3/nul/coma/foo.NUL/normal.c"; + String r1 = "store/data/au~78.bla/bla.aux/pr~6e/_p_r_n/lpt/co~6d3/nu~6c/coma/foo._n_u_l/normal.c.i"; + Assert.assertEquals("Windows filenames are ", r1, storePathHelper.rewrite(n1)); + } + + @Test + public void testHashLongPath() { + String n1 = "AUX/SECOND/X.PRN/FOURTH/FI:FTH/SIXTH/SEVENTH/EIGHTH/NINETH/TENTH/ELEVENTH/LOREMIPSUM.TXT"; + String r1 = "store/dh/au~78/second/x.prn/fourth/fi~3afth/sixth/seventh/eighth/nineth/tenth/loremia20419e358ddff1bf8751e38288aff1d7c32ec05.i"; + String n2 = "enterprise/openesbaddons/contrib-imola/corba-bc/netbeansplugin/wsdlExtension/src/main/java/META-INF/services/org.netbeans.modules.xml.wsdl.bindingsupport.spi.ExtensibilityElementTemplateProvider"; + String r2 = "store/dh/enterpri/openesba/contrib-/corba-bc/netbeans/wsdlexte/src/main/java/org.net7018f27961fdf338a598a40c4683429e7ffb9743.i"; + String n3 = "AUX.THE-QUICK-BROWN-FOX-JU:MPS-OVER-THE-LAZY-DOG-THE-QUICK-BROWN-FOX-JUMPS-OVER-THE-LAZY-DOG.TXT"; + String r3 = "store/dh/au~78.the-quick-brown-fox-ju~3amps-over-the-lazy-dog-the-quick-brown-fox-jud4dcadd033000ab2b26eb66bae1906bcb15d4a70.i"; + // TODO segment[8] == [. ], segment[8] in the middle of windows reserved name or character (to see if ~xx is broken) + errorCollector.checkThat(storePathHelper.rewrite(n1), equalTo(r1)); + errorCollector.checkThat(storePathHelper.rewrite(n2), equalTo(r2)); + errorCollector.checkThat(storePathHelper.rewrite(n3), equalTo(r3)); + } +} diff -r edb2e2829352 -r 6ec4af642ba8 hg4j/src/test/resources/test-repos.jar Binary file hg4j/src/test/resources/test-repos.jar has changed diff -r edb2e2829352 -r 6ec4af642ba8 lib/junit-4.8.2-src.jar Binary file lib/junit-4.8.2-src.jar has changed diff -r edb2e2829352 -r 6ec4af642ba8 lib/junit-4.8.2.jar Binary file lib/junit-4.8.2.jar has changed diff -r edb2e2829352 -r 6ec4af642ba8 settings.gradle --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/settings.gradle Tue May 10 10:52:53 2011 +0200 @@ -0,0 +1,4 @@ +rootProject.name = 'org.tmatesoft.hg4j' + +include 'hg4j' +include 'hg4j-cli' \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/ChangesetTransformer.java --- a/src/org/tmatesoft/hg/core/ChangesetTransformer.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,65 +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.util.Set; - -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.util.PathPool; -import org.tmatesoft.hg.util.PathRewrite; - -/** - * Bridges {@link HgChangelog.RawChangeset} with high-level {@link HgChangeset} API - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -/*package-local*/ class ChangesetTransformer implements HgChangelog.Inspector { - private final HgChangesetHandler handler; - private final HgChangeset changeset; - private Set branches; - - // repo and delegate can't be null, parent walker can - public ChangesetTransformer(HgRepository hgRepo, HgChangesetHandler delegate, HgChangelog.ParentWalker pw) { - if (hgRepo == null || delegate == null) { - throw new IllegalArgumentException(); - } - HgStatusCollector statusCollector = new HgStatusCollector(hgRepo); - // files listed in a changeset don't need their names to be rewritten (they are normalized already) - PathPool pp = new PathPool(new PathRewrite.Empty()); - statusCollector.setPathPool(pp); - changeset = new HgChangeset(statusCollector, pp); - changeset.setParentHelper(pw); - handler = delegate; - } - - public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { - if (branches != null && !branches.contains(cset.branch())) { - return; - } - - changeset.init(revisionNumber, nodeid, cset); - handler.next(changeset); - } - - public void limitBranches(Set branches) { - this.branches = branches; - } -} \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgBadArgumentException.java --- a/src/org/tmatesoft/hg/core/HgBadArgumentException.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,30 +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; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -@SuppressWarnings("serial") -public class HgBadArgumentException extends HgException { - - public HgBadArgumentException(String message, Throwable cause) { - super(message, cause); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgBadStateException.java --- a/src/org/tmatesoft/hg/core/HgBadStateException.java Mon May 09 11:49:23 2011 +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 edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgCatCommand.java --- a/src/org/tmatesoft/hg/core/HgCatCommand.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,123 +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 static org.tmatesoft.hg.repo.HgInternals.wrongLocalRevision; -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 org.tmatesoft.hg.repo.HgDataFile; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.ByteChannel; -import org.tmatesoft.hg.util.CancelledException; -import org.tmatesoft.hg.util.Path; - -/** - * Command to obtain content of a file, 'hg cat' counterpart. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgCatCommand { - - private final HgRepository repo; - private Path file; - private int localRevision = TIP; - private Nodeid revision; - - public HgCatCommand(HgRepository hgRepo) { - repo = hgRepo; - } - - /** - * File to read, required parameter - * @param fname path to a repository file, can't be null - * @return this for convenience - * @throws IllegalArgumentException if supplied fname is null or points to directory - */ - public HgCatCommand file(Path fname) { - if (fname == null || fname.isDirectory()) { - throw new IllegalArgumentException(String.valueOf(fname)); - } - file = fname; - return this; - } - - /** - * Invocation of this method clears revision set with {@link #revision(Nodeid)} or {@link #revision(int)} earlier. - * XXX rev can't be WORKING_COPY (if allowed, need to implement in #execute()) - * @param rev local revision number, non-negative, or one of predefined constants. Note, use of {@link HgRepository#BAD_REVISION}, - * although possible, makes little sense (command would fail if executed). - * @return this for convenience - */ - public HgCatCommand revision(int rev) { - if (wrongLocalRevision(rev)) { - throw new IllegalArgumentException(String.valueOf(rev)); - } - localRevision = rev; - revision = null; - return this; - } - - /** - * Select revision to read. Invocation of this method clears revision set with {@link #revision(int)} or {@link #revision(Nodeid)} earlier. - * - * @param nodeid - unique revision identifier, Note, use of null or {@link Nodeid#NULL} is senseless - * @return this for convenience - */ - public HgCatCommand revision(Nodeid nodeid) { - if (nodeid != null && nodeid.isNull()) { - nodeid = null; - } - revision = nodeid; - localRevision = BAD_REVISION; - return this; - } - - /** - * 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 IllegalArgumentException when command arguments are incomplete or wrong - */ - public void execute(ByteChannel sink) throws HgDataStreamException, IOException, CancelledException { - if (localRevision == BAD_REVISION && revision == null) { - throw new IllegalArgumentException("Either local file revision number or nodeid shall be specified"); - } - if (file == null) { - throw new IllegalArgumentException("Name of the file is missing"); - } - if (sink == null) { - throw new IllegalArgumentException("Need an output channel"); - } - HgDataFile dataFile = repo.getFileNode(file); - if (!dataFile.exists()) { - throw new HgDataStreamException(file.toString(), new FileNotFoundException(file.toString())); - } - int revToExtract; - if (revision != null) { - revToExtract = dataFile.getLocalRevision(revision); - } else { - revToExtract = localRevision; - } - dataFile.contentWithFilters(revToExtract, sink); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgChangeset.java --- a/src/org/tmatesoft/hg/core/HgChangeset.java Mon May 09 11:49:23 2011 +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 static org.tmatesoft.hg.core.Nodeid.NULL; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.tmatesoft.hg.core.HgLogCommand.FileRevision; -import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; -import org.tmatesoft.hg.repo.HgChangelog; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.repo.HgStatusCollector; -import org.tmatesoft.hg.util.Path; - - -/** - * Record in the Mercurial changelog, describing single commit. - * - * Not thread-safe, don't try to read from different threads - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgChangeset implements Cloneable { - private final HgStatusCollector statusHelper; - private final Path.Source pathHelper; - - private HgChangelog.ParentWalker parentHelper; - - // - private RawChangeset changeset; - private Nodeid nodeid; - - // - private List modifiedFiles, addedFiles; - private List deletedFiles; - private int revNumber; - private byte[] parent1, parent2; - - // XXX consider CommandContext with StatusCollector, PathPool etc. Commands optionally get CC through a cons or create new - // and pass it around - /*package-local*/HgChangeset(HgStatusCollector statusCollector, Path.Source pathFactory) { - statusHelper = statusCollector; - pathHelper = pathFactory; - } - - /*package-local*/ void init(int localRevNumber, Nodeid nid, RawChangeset rawChangeset) { - revNumber = localRevNumber; - nodeid = nid; - changeset = rawChangeset.clone(); - modifiedFiles = addedFiles = null; - deletedFiles = null; - parent1 = parent2 = null; - // keep references to parentHelper, statusHelper and pathHelper - } - - /*package-local*/ void setParentHelper(HgChangelog.ParentWalker pw) { - parentHelper = pw; - if (parentHelper != null) { - if (parentHelper.getRepo() != statusHelper.getRepo()) { - throw new IllegalArgumentException(); - } - } - } - - public int getRevision() { - return revNumber; - } - public Nodeid getNodeid() { - return nodeid; - } - public String getUser() { - return changeset.user(); - } - public String getComment() { - return changeset.comment(); - } - public String getBranch() { - return changeset.branch(); - } - - /** - * @return used to be String, now {@link HgDate}, use {@link HgDate#toString()} to get same result as before - */ - public HgDate getDate() { - return new HgDate(changeset.date().getTime(), changeset.timezone()); - } - public Nodeid getManifestRevision() { - return changeset.manifest(); - } - - 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 - // #getModifiedFiles() would return list with merged file(s) (because it uses status to get 'em, not - // what #files() gives). - ArrayList rv = new ArrayList(changeset.files().size()); - for (String name : changeset.files()) { - rv.add(pathHelper.path(name)); - } - return rv; - } - - public List getModifiedFiles() { - if (modifiedFiles == null) { - initFileChanges(); - } - return modifiedFiles; - } - - public List getAddedFiles() { - if (addedFiles == null) { - initFileChanges(); - } - return addedFiles; - } - - public List getRemovedFiles() { - if (deletedFiles == null) { - initFileChanges(); - } - return deletedFiles; - } - - public boolean isMerge() { - // p1 == -1 and p2 != -1 is legitimate case - return !NULL.equals(getFirstParentRevision()) && !NULL.equals(getSecondParentRevision()); - } - - public Nodeid getFirstParentRevision() { - if (parentHelper != null) { - return parentHelper.safeFirstParent(nodeid); - } - // read once for both p1 and p2 - if (parent1 == null) { - parent1 = new byte[20]; - parent2 = new byte[20]; - statusHelper.getRepo().getChangelog().parents(revNumber, new int[2], parent1, parent2); - } - return Nodeid.fromBinary(parent1, 0); - } - - public Nodeid getSecondParentRevision() { - if (parentHelper != null) { - return parentHelper.safeSecondParent(nodeid); - } - if (parent2 == null) { - parent1 = new byte[20]; - parent2 = new byte[20]; - statusHelper.getRepo().getChangelog().parents(revNumber, new int[2], parent1, parent2); - } - return Nodeid.fromBinary(parent2, 0); - } - - @Override - public HgChangeset clone() { - try { - HgChangeset copy = (HgChangeset) super.clone(); - // copy.changeset references this.changeset, doesn't need own copy - return copy; - } catch (CloneNotSupportedException ex) { - throw new InternalError(ex.toString()); - } - } - - private /*synchronized*/ void initFileChanges() { - ArrayList deleted = new ArrayList(); - ArrayList modified = new ArrayList(); - ArrayList added = new ArrayList(); - HgStatusCollector.Record r = new HgStatusCollector.Record(); - statusHelper.change(revNumber, r); - final HgRepository repo = statusHelper.getRepo(); - for (Path s : r.getModified()) { - Nodeid nid = r.nodeidAfterChange(s); - if (nid == null) { - throw new HgBadStateException(); - } - modified.add(new FileRevision(repo, nid, s)); - } - for (Path s : r.getAdded()) { - Nodeid nid = r.nodeidAfterChange(s); - if (nid == null) { - throw new HgBadStateException(); - } - added.add(new FileRevision(repo, nid, s)); - } - for (Path s : r.getRemoved()) { - // with Path from getRemoved, may just copy - deleted.add(s); - } - modified.trimToSize(); - added.trimToSize(); - deleted.trimToSize(); - modifiedFiles = Collections.unmodifiableList(modified); - addedFiles = Collections.unmodifiableList(added); - deletedFiles = Collections.unmodifiableList(deleted); - } -} \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgChangesetHandler.java --- a/src/org/tmatesoft/hg/core/HgChangesetHandler.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,30 +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; - -/** - * Callback to process {@link HgChangeset changesets}. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public interface HgChangesetHandler { - /** - * @param changeset not necessarily a distinct instance each time, {@link HgChangeset#clone() clone()} if need a copy. - */ - void next(HgChangeset changeset); -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgCloneCommand.java --- a/src/org/tmatesoft/hg/core/HgCloneCommand.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,333 +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 static org.tmatesoft.hg.core.Nodeid.NULL; -import static org.tmatesoft.hg.internal.RequiresFile.*; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.TreeMap; -import java.util.zip.DeflaterOutputStream; - -import org.tmatesoft.hg.internal.ByteArrayDataAccess; -import org.tmatesoft.hg.internal.DataAccess; -import org.tmatesoft.hg.internal.DigestHelper; -import org.tmatesoft.hg.internal.Internals; -import org.tmatesoft.hg.repo.HgBundle; -import org.tmatesoft.hg.repo.HgBundle.GroupElement; -import org.tmatesoft.hg.repo.HgLookup; -import org.tmatesoft.hg.repo.HgRemoteRepository; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.CancelledException; -import org.tmatesoft.hg.util.PathRewrite; - -/** - * WORK IN PROGRESS, DO NOT USE - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgCloneCommand { - - private File destination; - private HgRemoteRepository srcRepo; - - public HgCloneCommand() { - } - - /** - * @param folder location to become root of the repository (i.e. where .hg folder would reside). Either - * shall not exist or be empty otherwise. - * @return this for convenience - */ - public HgCloneCommand destination(File folder) { - destination = folder; - return this; - } - - public HgCloneCommand source(HgRemoteRepository hgRemote) { - srcRepo = hgRemote; - return this; - } - - public HgRepository execute() throws HgException, CancelledException { - if (destination == null) { - throw new HgBadArgumentException("Destination not set", null); - } - if (srcRepo == null || srcRepo.isInvalid()) { - throw new HgBadArgumentException("Bad source repository", null); - } - if (destination.exists()) { - if (!destination.isDirectory()) { - throw new HgBadArgumentException(String.format("%s is not a directory", destination), null); - } else if (destination.list().length > 0) { - throw new HgBadArgumentException(String.format("% shall be empty", destination), null); - } - } else { - destination.mkdirs(); - } - // if cloning remote repo, which can stream and no revision is specified - - // can use 'stream_out' wireproto - // - // pull all changes from the very beginning - // XXX consult getContext() if by any chance has a bundle ready, if not, then read and register - HgBundle completeChanges = srcRepo.getChanges(Collections.singletonList(NULL)); - WriteDownMate mate = new WriteDownMate(destination); - try { - // instantiate new repo in the destdir - mate.initEmptyRepository(); - // pull changes - completeChanges.inspectAll(mate); - mate.complete(); - } catch (IOException ex) { - throw new HgException(ex); - } finally { - completeChanges.unlink(); - } - return new HgLookup().detect(destination); - } - - - // 1. process changelog, memorize nodeids to index - // 2. process manifest, using map from step 3, collect manifest nodeids - // 3. process every file, using map from 3, and consult set from step 4 to ensure repo is correct - private static class WriteDownMate implements HgBundle.Inspector { - private final File hgDir; - private final PathRewrite storagePathHelper; - private FileOutputStream indexFile; - private String filename; // human-readable name of the file being written, for log/exception purposes - - private final TreeMap changelogIndexes = new TreeMap(); - private boolean collectChangelogIndexes = false; - - private int base = -1; - private long offset = 0; - private DataAccess prevRevContent; - private final DigestHelper dh = new DigestHelper(); - private final ArrayList revisionSequence = new ArrayList(); // last visited nodes first - - private final LinkedList fncacheFiles = new LinkedList(); - private Internals implHelper; - - public WriteDownMate(File destDir) { - hgDir = new File(destDir, ".hg"); - implHelper = new Internals(); - implHelper.setStorageConfig(1, STORE | FNCACHE | DOTENCODE); - storagePathHelper = implHelper.buildDataFilesHelper(); - } - - public void initEmptyRepository() throws IOException { - implHelper.initEmptyRepository(hgDir); - } - - public void complete() throws IOException { - FileOutputStream fncacheFile = new FileOutputStream(new File(hgDir, "store/fncache")); - for (String s : fncacheFiles) { - fncacheFile.write(s.getBytes()); - fncacheFile.write(0x0A); // http://mercurial.selenic.com/wiki/fncacheRepoFormat - } - fncacheFile.close(); - } - - public void changelogStart() { - try { - base = -1; - offset = 0; - revisionSequence.clear(); - indexFile = new FileOutputStream(new File(hgDir, filename = "store/00changelog.i")); - collectChangelogIndexes = true; - } catch (IOException ex) { - throw new HgBadStateException(ex); - } - } - - public void changelogEnd() { - try { - if (prevRevContent != null) { - prevRevContent.done(); - prevRevContent = null; - } - collectChangelogIndexes = false; - indexFile.close(); - indexFile = null; - filename = null; - } catch (IOException ex) { - throw new HgBadStateException(ex); - } - } - - public void manifestStart() { - try { - base = -1; - offset = 0; - revisionSequence.clear(); - indexFile = new FileOutputStream(new File(hgDir, filename = "store/00manifest.i")); - } catch (IOException ex) { - throw new HgBadStateException(ex); - } - } - - public void manifestEnd() { - try { - if (prevRevContent != null) { - prevRevContent.done(); - prevRevContent = null; - } - indexFile.close(); - indexFile = null; - filename = null; - } catch (IOException ex) { - throw new HgBadStateException(ex); - } - } - - public void fileStart(String name) { - try { - base = -1; - offset = 0; - revisionSequence.clear(); - fncacheFiles.add("data/" + name + ".i"); // FIXME this is pure guess, - // need to investigate more how filenames are kept in fncache - File file = new File(hgDir, filename = storagePathHelper.rewrite(name)); - file.getParentFile().mkdirs(); - indexFile = new FileOutputStream(file); - } catch (IOException ex) { - throw new HgBadStateException(ex); - } - } - - public void fileEnd(String name) { - try { - if (prevRevContent != null) { - prevRevContent.done(); - prevRevContent = null; - } - indexFile.close(); - indexFile = null; - filename = null; - } catch (IOException ex) { - throw new HgBadStateException(ex); - } - } - - private int knownRevision(Nodeid p) { - if (NULL.equals(p)) { - return -1; - } else { - for (int i = revisionSequence.size() - 1; i >= 0; i--) { - if (revisionSequence.get(i).equals(p)) { - return i; - } - } - } - throw new HgBadStateException(String.format("Can't find index of %s for file %s", p.shortNotation(), filename)); - } - - public boolean element(GroupElement ge) { - try { - assert indexFile != null; - boolean writeComplete = false; - Nodeid p1 = ge.firstParent(); - Nodeid p2 = ge.secondParent(); - if (NULL.equals(p1) && NULL.equals(p2) /* or forced flag, does REVIDX_PUNCHED_FLAG indicate that? */) { - prevRevContent = new ByteArrayDataAccess(new byte[0]); - writeComplete = true; - } - byte[] content = ge.apply(prevRevContent); - 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)); - } - final int link; - if (collectChangelogIndexes) { - changelogIndexes.put(node, revisionSequence.size()); - link = revisionSequence.size(); - } 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)); - } - link = csRev.intValue(); - } - final int p1Rev = knownRevision(p1), p2Rev = knownRevision(p2); - DataAccess patchContent = ge.rawData(); - writeComplete = writeComplete || patchContent.length() >= (/* 3/4 of actual */content.length - (content.length >>> 2)); - if (writeComplete) { - base = revisionSequence.size(); - } - final byte[] sourceData = writeComplete ? content : patchContent.byteArray(); - final byte[] data; - ByteArrayOutputStream bos = new ByteArrayOutputStream(content.length); - DeflaterOutputStream dos = new DeflaterOutputStream(bos); - dos.write(sourceData); - dos.close(); - final byte[] compressedData = bos.toByteArray(); - dos = null; - bos = null; - final Byte dataPrefix; - if (compressedData.length >= (sourceData.length - (sourceData.length >>> 2))) { - // compression wasn't too effective, - data = sourceData; - dataPrefix = 'u'; - } else { - data = compressedData; - dataPrefix = null; - } - - ByteBuffer header = ByteBuffer.allocate(64 /* REVLOGV1_RECORD_SIZE */); - if (offset == 0) { - final int INLINEDATA = 1 << 16; - header.putInt(1 /* RevlogNG */ | INLINEDATA); - header.putInt(0); - } else { - header.putLong(offset << 16); - } - final int compressedLen = data.length + (dataPrefix == null ? 0 : 1); - header.putInt(compressedLen); - header.putInt(content.length); - header.putInt(base); - header.putInt(link); - header.putInt(p1Rev); - header.putInt(p2Rev); - header.put(node.toByteArray()); - // assume 12 bytes left are zeros - indexFile.write(header.array()); - if (dataPrefix != null) { - indexFile.write(dataPrefix.byteValue()); - } - indexFile.write(data); - // - offset += compressedLen; - revisionSequence.add(node); - prevRevContent.done(); - prevRevContent = new ByteArrayDataAccess(content); - } catch (IOException ex) { - throw new HgBadStateException(ex); - } - return true; - } - } - -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgDataStreamException.java --- a/src/org/tmatesoft/hg/core/HgDataStreamException.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,33 +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; - -/** - * Any erroneous state with @link {@link HgDataFile} input/output, read/write operations - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -@SuppressWarnings("serial") -public class HgDataStreamException extends HgException { - - public HgDataStreamException(String message, Throwable cause) { - super(message, cause); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgDate.java --- a/src/org/tmatesoft/hg/core/HgDate.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,111 +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.util.Calendar; -import java.util.Formatter; -import java.util.Locale; -import java.util.TimeZone; - -/** - * Compound object to keep time and time zone of a change. Time zone is not too useful unless you'd like to indicate where - * the change was made (original hg shows date of a change in its original time zone) - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public final class HgDate implements Comparable, Cloneable { - private final long time; - private final TimeZone tzone; - - - /** - * @param millis UTC, milliseconds - * @param timezone zone offset in seconds, UTC - local == timezone. I.e. positive in the Western Hemisphere. - */ - public HgDate(long millis, int timezone) { - time = millis; - // @see http://pydoc.org/2.5.1/time.html time.timezone -- difference in seconds between UTC and local standard time - // UTC - local = timezone. local = UTC - timezone - // In Java, timezone is positive right of Greenwich, UTC+timezone = local - String[] available = TimeZone.getAvailableIDs(-timezone * 1000); - assert available != null && available.length > 0 : String.valueOf(timezone); - // this is sort of hach, I don't know another way how to get - // abbreviated name from zone offset (other than to have own mapping) - // And I can't use any id, because e.g. zone with id "US/Mountain" - // gives incorrect (according to hg cmdline) result, unlike MST or US/Arizona (all ids for zone -0700) - // use 1125044450000L to see the difference - String shortID = TimeZone.getTimeZone(available[0]).getDisplayName(false, TimeZone.SHORT); - // XXX in fact, might need to handle daylight saving time, but not sure how, - // getTimeZone(GMT-timezone*1000).inDaylightTime()? - TimeZone tz = TimeZone.getTimeZone(shortID); - tzone = tz; - } - - public long getRawTime() { - return time; - } - - /** - * @return zone object by reference, do not alter it (make own copy by {@link TimeZone#clone()}, to modify). - */ - public TimeZone getTimeZone() { - return tzone; - } - - @Override - public String toString() { - // format the same way hg does - return toString(Locale.US); - } - - public String toString(Locale l) { - Calendar c = Calendar.getInstance(getTimeZone()); - c.setTimeInMillis(getRawTime()); - Formatter f = new Formatter(new StringBuilder(), l); - f.format("%ta %> 32); - } - - @Override - protected Object clone() { - try { - return super.clone(); - } catch (CloneNotSupportedException ex) { - throw new InternalError(ex.toString()); - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgException.java --- a/src/org/tmatesoft/hg/core/HgException.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,45 +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; - -/** - * Root class for all hg4j exceptions. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -@SuppressWarnings("serial") -public class HgException extends Exception { - - public HgException(String reason) { - super(reason); - } - - public HgException(String reason, Throwable cause) { - super(reason, cause); - } - - public HgException(Throwable cause) { - super(cause); - } - -// /* XXX CONSIDER capability to pass extra information about errors */ -// public static class Status { -// public Status(String message, Throwable cause, int errorCode, Object extraData) { -// } -// } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgIncomingCommand.java --- a/src/org/tmatesoft/hg/core/HgIncomingCommand.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,199 +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.IOException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; - -import org.tmatesoft.hg.internal.RepositoryComparator; -import org.tmatesoft.hg.internal.RepositoryComparator.BranchChain; -import org.tmatesoft.hg.repo.HgBundle; -import org.tmatesoft.hg.repo.HgChangelog; -import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; -import org.tmatesoft.hg.repo.HgRemoteRepository; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.CancelledException; - -/** - * Command to find out changes available in a remote repository, missing locally. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgIncomingCommand { - - private final HgRepository localRepo; - private HgRemoteRepository remoteRepo; - @SuppressWarnings("unused") - private boolean includeSubrepo; - private RepositoryComparator comparator; - private List missingBranches; - private HgChangelog.ParentWalker parentHelper; - private Set branches; - - public HgIncomingCommand(HgRepository hgRepo) { - localRepo = hgRepo; - } - - public HgIncomingCommand against(HgRemoteRepository hgRemote) { - remoteRepo = hgRemote; - comparator = null; - missingBranches = null; - return this; - } - - /** - * 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. - * - * @param branch - branch name, case-sensitive, non-null. - * @return this for convenience - * @throws IllegalArgumentException when branch argument is null - */ - public HgIncomingCommand branch(String branch) { - if (branch == null) { - throw new IllegalArgumentException(); - } - if (branches == null) { - branches = new TreeSet(); - } - branches.add(branch); - return this; - } - - /** - * PLACEHOLDER, NOT IMPLEMENTED YET. - * - * Whether to include sub-repositories when collecting changes, default is true XXX or false? - * @return this for convenience - */ - public HgIncomingCommand subrepo(boolean include) { - includeSubrepo = include; - throw HgRepository.notImplemented(); - } - - /** - * Lightweight check for incoming changes, gives only list of revisions to pull. - * Reported changes are from any branch (limits set by {@link #branch(String)} are not taken into account. - * - * @param context anything hg4j can use to get progress and/or cancel support - * @return list of nodes present at remote and missing locally - * @throws HgException - * @throws CancelledException - */ - public List executeLite(Object context) throws HgException, CancelledException { - LinkedHashSet result = new LinkedHashSet(); - RepositoryComparator repoCompare = getComparator(context); - for (BranchChain bc : getMissingBranches(context)) { - 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; - } - - /** - * Full information about incoming changes - * - * @throws HgException - * @throws CancelledException - */ - public void executeFull(final HgChangesetHandler handler) throws HgException, CancelledException { - if (handler == null) { - throw new IllegalArgumentException("Delegate can't be null"); - } - final List common = getCommon(handler); - HgBundle changegroup = remoteRepo.getChanges(common); - try { - changegroup.changes(localRepo, new HgChangelog.Inspector() { - private int localIndex; - private final HgChangelog.ParentWalker parentHelper; - private final ChangesetTransformer transformer; - - { - transformer = new ChangesetTransformer(localRepo, handler, getParentHelper()); - transformer.limitBranches(branches); - parentHelper = getParentHelper(); - // new revisions, if any, would be added after all existing, and would get numbered started with last+1 - localIndex = localRepo.getChangelog().getRevisionCount(); - } - - 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"); - } - return; - } - transformer.next(localIndex++, nodeid, cset); - } - }); - } catch (IOException ex) { - throw new HgException(ex); - } - } - - private RepositoryComparator getComparator(Object context) throws HgException, CancelledException { - if (remoteRepo == null) { - throw new HgBadArgumentException("Shall specify remote repository to compare against", null); - } - if (comparator == null) { - comparator = new RepositoryComparator(getParentHelper(), remoteRepo); -// comparator.compare(context); // XXX meanwhile we use distinct path to calculate common - } - return comparator; - } - - private HgChangelog.ParentWalker getParentHelper() { - if (parentHelper == null) { - parentHelper = localRepo.getChangelog().new ParentWalker(); - parentHelper.init(); - } - return parentHelper; - } - - private List getMissingBranches(Object context) throws HgException, CancelledException { - if (missingBranches == null) { - missingBranches = getComparator(context).calculateMissingBranches(); - } - return missingBranches; - } - - private List getCommon(Object context) throws HgException, CancelledException { -// return getComparator(context).getCommon(); - final LinkedHashSet common = new LinkedHashSet(); - // XXX common can be obtained from repoCompare, but at the moment it would almost duplicate work of calculateMissingBranches - // once I refactor latter, common shall be taken from repoCompare. - RepositoryComparator repoCompare = getComparator(context); - for (BranchChain bc : getMissingBranches(context)) { - repoCompare.collectKnownRoots(bc, common); - } - return new LinkedList(common); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgLogCommand.java --- a/src/org/tmatesoft/hg/core/HgLogCommand.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,325 +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 static org.tmatesoft.hg.repo.HgRepository.TIP; - -import java.io.IOException; -import java.util.Calendar; -import java.util.Collections; -import java.util.ConcurrentModificationException; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; - -import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; -import org.tmatesoft.hg.repo.HgChangelog; -import org.tmatesoft.hg.repo.HgDataFile; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.ByteChannel; -import org.tmatesoft.hg.util.CancelledException; -import org.tmatesoft.hg.util.Path; - - -/** - * Access to changelog, 'hg log' command counterpart. - * - *
- * Usage:
- *   new LogCommand().limit(20).branch("maintenance-2.1").user("me").execute(new MyHandler());
- * 
- * Not thread-safe (each thread has to use own {@link HgLogCommand} instance). - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgLogCommand implements HgChangelog.Inspector { - - private final HgRepository repo; - private Set users; - private Set branches; - private int limit = 0, count = 0; - private int startRev = 0, endRev = TIP; - private Calendar date; - private Path file; - private boolean followHistory; // makes sense only when file != null - private ChangesetTransformer csetTransform; - private HgChangelog.ParentWalker parentHelper; - - public HgLogCommand(HgRepository hgRepo) { - repo = hgRepo; - } - - /** - * Limit search to specified user. Multiple user names may be specified. Once set, user names can't be - * cleared, use new command instance in such cases. - * @param user - full or partial name of the user, case-insensitive, non-null. - * @return this instance for convenience - * @throws IllegalArgumentException when argument is null - */ - public HgLogCommand user(String user) { - if (user == null) { - throw new IllegalArgumentException(); - } - if (users == null) { - users = new TreeSet(); - } - users.add(user.toLowerCase()); - return this; - } - - /** - * Limit search to specified branch. Multiple branch specification possible (changeset from any of these - * would be included in result). If unspecified, all branches are considered. There's no way to clean branch selection - * once set, create fresh new command instead. - * @param branch - branch name, case-sensitive, non-null. - * @return this instance for convenience - * @throws IllegalArgumentException when branch argument is null - */ - public HgLogCommand branch(String branch) { - if (branch == null) { - throw new IllegalArgumentException(); - } - if (branches == null) { - branches = new TreeSet(); - } - branches.add(branch); - return this; - } - - // limit search to specific date - // multiple? - public HgLogCommand date(Calendar date) { - this.date = date; - // FIXME implement - // isSet(field) - false => don't use in detection of 'same date' - throw HgRepository.notImplemented(); - } - - /** - * - * @param num - number of changeset to produce. Pass 0 to clear the limit. - * @return this instance for convenience - */ - public HgLogCommand limit(int num) { - limit = num; - return this; - } - - /** - * Limit to specified subset of Changelog, [min(rev1,rev2), max(rev1,rev2)], inclusive. - * Revision may be specified with {@link HgRepository#TIP} - * @param rev1 - local revision number - * @param rev2 - local revision number - * @return this instance for convenience - */ - public HgLogCommand range(int rev1, int rev2) { - if (rev1 != TIP && rev2 != TIP) { - startRev = rev2 < rev1 ? rev2 : rev1; - endRev = startRev == rev2 ? rev1 : rev2; - } else if (rev1 == TIP && rev2 != TIP) { - startRev = rev2; - endRev = rev1; - } else { - startRev = rev1; - endRev = rev2; - } - return this; - } - - /** - * Visit history of a given file only. - * @param file path relative to repository root. Pass null to reset. - * @param followCopyRename true to report changesets of the original file(-s), if copy/rename ever occured to the file. - */ - public HgLogCommand file(Path file, boolean followCopyRename) { - // multiple? Bad idea, would need to include extra method into Handler to tell start of next file - this.file = file; - followHistory = followCopyRename; - return this; - } - - /** - * Handy analog of {@link #file(Path, boolean)} when clients' paths come from filesystem and need conversion to repository's - */ - public HgLogCommand file(String file, boolean followCopyRename) { - return file(Path.create(repo.getToRepoPathHelper().rewrite(file)), followCopyRename); - } - - /** - * Similar to {@link #execute(org.tmatesoft.hg.repo.RawChangeset.Inspector)}, collects and return result as a list. - */ - public List execute() throws HgException { - CollectHandler collector = new CollectHandler(); - execute(collector); - return collector.getChanges(); - } - - /** - * - * @param handler callback to process changesets. - * @throws IllegalArgumentException when inspector argument is null - * @throws ConcurrentModificationException if this log command instance is already running - */ - public void execute(HgChangesetHandler handler) throws HgException { - if (handler == null) { - throw new IllegalArgumentException(); - } - if (csetTransform != null) { - throw new ConcurrentModificationException(); - } - try { - count = 0; - HgChangelog.ParentWalker pw = parentHelper; // leave it uninitialized unless we iterate whole repo - if (file == null) { - pw = getParentHelper(); - } - // 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); - if (file == null) { - repo.getChangelog().range(startRev, endRev, this); - } else { - HgDataFile fileNode = repo.getFileNode(file); - fileNode.history(startRev, endRev, this); - if (fileNode.isCopy()) { - // even if we do not follow history, report file rename - do { - if (handler instanceof FileHistoryHandler) { - FileRevision src = new FileRevision(repo, fileNode.getCopySourceRevision(), fileNode.getCopySourceName()); - FileRevision dst = new FileRevision(repo, fileNode.getRevision(0), fileNode.getPath()); - ((FileHistoryHandler) handler).copy(src, dst); - } - if (limit > 0 && count >= limit) { - // if limit reach, follow is useless. - break; - } - if (followHistory) { - fileNode = repo.getFileNode(fileNode.getCopySourceName()); - fileNode.history(this); - } - } while (followHistory && fileNode.isCopy()); - } - } - } finally { - csetTransform = null; - } - } - - // - - public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { - if (limit > 0 && count >= limit) { - return; - } - if (branches != null && !branches.contains(cset.branch())) { - return; - } - if (users != null) { - String csetUser = cset.user().toLowerCase(); - boolean found = false; - for (String u : users) { - if (csetUser.indexOf(u) != -1) { - found = true; - break; - } - } - if (!found) { - return; - } - } - if (date != null) { - // FIXME - } - count++; - csetTransform.next(revisionNumber, nodeid, cset); - } - - private HgChangelog.ParentWalker getParentHelper() { - if (parentHelper == null) { - parentHelper = repo.getChangelog().new ParentWalker(); - parentHelper.init(); - } - return parentHelper; - } - - - /** - * @deprecated Use {@link HgChangesetHandler} instead. This interface is left temporarily for compatibility. - */ - @Deprecated() - public interface Handler extends HgChangesetHandler { - } - - /** - * 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(FileRevision, FileRevision)} 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(FileRevision, FileRevision)} 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 { - // XXX perhaps, should distinguish copy from rename? And what about merged revisions and following them? - void copy(FileRevision from, FileRevision to); - } - - public static class CollectHandler implements HgChangesetHandler { - private final List result = new LinkedList(); - - public List getChanges() { - return Collections.unmodifiableList(result); - } - - public void next(HgChangeset changeset) { - result.add(changeset.clone()); - } - } - - public static final class FileRevision { - private final HgRepository repo; - private final Nodeid revision; - private final Path path; - - /*package-local*/FileRevision(HgRepository hgRepo, Nodeid rev, 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 HgBadStateException(); - } - repo = hgRepo; - revision = rev; - path = p; - } - - public Path getPath() { - return path; - } - public Nodeid getRevision() { - return revision; - } - public void putContentTo(ByteChannel sink) throws HgDataStreamException, IOException, CancelledException { - HgDataFile fn = repo.getFileNode(path); - int localRevision = fn.getLocalRevision(revision); - fn.contentWithFilters(localRevision, sink); - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgManifestCommand.java --- a/src/org/tmatesoft/hg/core/HgManifestCommand.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,196 +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 static org.tmatesoft.hg.repo.HgRepository.*; -import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; -import static org.tmatesoft.hg.repo.HgRepository.TIP; - -import java.util.ConcurrentModificationException; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; - -import org.tmatesoft.hg.core.HgLogCommand.FileRevision; -import org.tmatesoft.hg.repo.HgManifest; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.PathPool; -import org.tmatesoft.hg.util.PathRewrite; - - -/** - * Gives access to list of files in each revision (Mercurial manifest information), 'hg manifest' counterpart. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgManifestCommand { - - private final HgRepository repo; - private Path.Matcher matcher; - private int startRev = 0, endRev = TIP; - private Handler visitor; - private boolean needDirs = false; - - private final Mediator mediator = new Mediator(); - - public HgManifestCommand(HgRepository hgRepo) { - repo = hgRepo; - } - - /** - * Parameterize command to visit revisions [rev1..rev2]. - * @param rev1 - local revision number to start from. Non-negative. May be {@link HgRepository#TIP} (rev2 argument shall be {@link HgRepository#TIP} as well, then) - * @param rev2 - local revision number to end with, inclusive. Non-negative, greater or equal to rev1. May be {@link HgRepository#TIP}. - * @return this for convenience. - * @throws IllegalArgumentException if revision arguments are incorrect (see above). - */ - public HgManifestCommand range(int rev1, int rev2) { - // XXX if manifest range is different from that of changelog, need conversion utils (external?) - boolean badArgs = rev1 == BAD_REVISION || rev2 == BAD_REVISION || rev1 == WORKING_COPY || rev2 == WORKING_COPY; - badArgs |= rev2 != TIP && rev2 < rev1; // range(3, 1); - badArgs |= rev1 == TIP && rev2 != TIP; // range(TIP, 2), although this may be legitimate when TIP points to 2 - if (badArgs) { - throw new IllegalArgumentException(String.format("Bad range: [%d, %d]", rev1, rev2)); - } - startRev = rev1; - endRev = rev2; - return this; - } - - public HgManifestCommand revision(int rev) { - startRev = endRev = rev; - return this; - } - - public HgManifestCommand dirs(boolean include) { - // XXX whether directories with directories only are include or not - // now lists only directories with files - needDirs = include; - return this; - } - - /** - * Limit manifest walk to a subset of files. - * @param pathMatcher - filter, pass null to clear. - * @return this instance for convenience - */ - public HgManifestCommand match(Path.Matcher pathMatcher) { - matcher = pathMatcher; - return this; - } - - /** - * Runs the command. - * @param handler - callback to get the outcome - * @throws IllegalArgumentException if handler is null - * @throws ConcurrentModificationException if this command is already in use (running) - */ - public void execute(Handler handler) { - if (handler == null) { - throw new IllegalArgumentException(); - } - if (visitor != null) { - throw new ConcurrentModificationException(); - } - try { - visitor = handler; - mediator.start(); - repo.getManifest().walk(startRev, endRev, mediator); - } 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(FileRevision 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.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; - - public void start() { - // Manifest keeps normalized paths - pathPool = new PathPool(new PathRewrite.Empty()); - } - - public void done() { - manifestContent = null; - pathPool = null; - } - - public boolean begin(int revision, Nodeid nid) { - if (needDirs && manifestContent == null) { - manifestContent = new LinkedList(); - } - visitor.begin(manifestNodeid = nid); - return true; - } - public boolean end(int revision) { - if (needDirs) { - LinkedHashMap> breakDown = new LinkedHashMap>(); - for (FileRevision 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); - } - for (Path dir : breakDown.keySet()) { - visitor.dir(dir); - for (FileRevision fr : breakDown.get(dir)) { - visitor.file(fr); - } - } - manifestContent.clear(); - } - visitor.end(manifestNodeid); - manifestNodeid = null; - return true; - } - public boolean next(Nodeid nid, String fname, String flags) { - Path p = pathPool.path(fname); - if (matcher != null && !matcher.accept(p)) { - return true; - } - FileRevision fr = new FileRevision(repo, nid, p); - if (needDirs) { - manifestContent.add(fr); - } else { - visitor.file(fr); - } - return true; - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgOutgoingCommand.java --- a/src/org/tmatesoft/hg/core/HgOutgoingCommand.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,133 +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.util.List; -import java.util.Set; -import java.util.TreeSet; - -import org.tmatesoft.hg.internal.RepositoryComparator; -import org.tmatesoft.hg.repo.HgChangelog; -import org.tmatesoft.hg.repo.HgRemoteRepository; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.CancelledException; - -/** - * Command to find out changes made in a local repository and missing at remote repository. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgOutgoingCommand { - - private final HgRepository localRepo; - private HgRemoteRepository remoteRepo; - @SuppressWarnings("unused") - private boolean includeSubrepo; - private RepositoryComparator comparator; - private HgChangelog.ParentWalker parentHelper; - private Set branches; - - public HgOutgoingCommand(HgRepository hgRepo) { - localRepo = hgRepo; - } - - /** - * @param hgRemote remoteRepository to compare against - * @return this for convenience - */ - public HgOutgoingCommand against(HgRemoteRepository hgRemote) { - remoteRepo = hgRemote; - comparator = null; - return this; - } - - /** - * 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. - * - * @param branch - branch name, case-sensitive, non-null. - * @return this for convenience - * @throws IllegalArgumentException when branch argument is null - */ - public HgOutgoingCommand branch(String branch) { - if (branch == null) { - throw new IllegalArgumentException(); - } - if (branches == null) { - branches = new TreeSet(); - } - branches.add(branch); - return this; - } - - /** - * PLACEHOLDER, NOT IMPLEMENTED YET. - * - * @return this for convenience - */ - public HgOutgoingCommand subrepo(boolean include) { - includeSubrepo = include; - throw HgRepository.notImplemented(); - } - - /** - * Lightweight check for outgoing changes. - * Reported changes are from any branch (limits set by {@link #branch(String)} are not taken into account. - * - * @param context - * @return list on local nodes known to be missing at remote server - */ - public List executeLite(Object context) throws HgException, CancelledException { - return getComparator(context).getLocalOnlyRevisions(); - } - - /** - * Complete information about outgoing changes - * - * @param handler delegate to process changes - */ - public void executeFull(final HgChangesetHandler handler) throws HgException, CancelledException { - if (handler == null) { - throw new IllegalArgumentException("Delegate can't be null"); - } - ChangesetTransformer inspector = new ChangesetTransformer(localRepo, handler, getParentHelper()); - inspector.limitBranches(branches); - getComparator(handler).visitLocalOnlyRevisions(inspector); - } - - private RepositoryComparator getComparator(Object context) throws HgException, CancelledException { - if (remoteRepo == null) { - throw new IllegalArgumentException("Shall specify remote repository to compare against"); - } - if (comparator == null) { - comparator = new RepositoryComparator(getParentHelper(), remoteRepo); - comparator.compare(context); - } - return comparator; - } - - private HgChangelog.ParentWalker getParentHelper() { - if (parentHelper == null) { - parentHelper = localRepo.getChangelog().new ParentWalker(); - parentHelper.init(); - } - return parentHelper; - } - -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgRepoFacade.java --- a/src/org/tmatesoft/hg/core/HgRepoFacade.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,113 +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.repo.HgRepository; -import org.tmatesoft.hg.repo.HgLookup; - -/** - * Starting point for the library. - *

Sample use: - *

- *  HgRepoFacade f = new HgRepoFacade();
- *  f.initFrom(System.getenv("whatever.repo.location"));
- *  HgStatusCommand statusCmd = f.createStatusCommand();
- *  HgStatusCommand.Handler handler = ...;
- *  statusCmd.execute(handler);
- * 
- * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgRepoFacade { - private HgRepository repo; - - public HgRepoFacade() { - } - - /** - * @param hgRepo - * @return true on successful initialization - * @throws IllegalArgumentException when argument is null - */ - public boolean init(HgRepository hgRepo) { - if (hgRepo == null) { - throw new IllegalArgumentException(); - } - repo = hgRepo; - return !repo.isInvalid(); - } - - /** - * Tries to find repository starting from the current working directory. - * @return true if found valid repository - * @throws HgException in case of errors during repository initialization - */ - public boolean init() throws HgException { - repo = new HgLookup().detectFromWorkingDir(); - return repo != null && !repo.isInvalid(); - } - - /** - * Looks up Mercurial repository starting from specified location and up to filesystem root. - * - * @param repoLocation path to any folder within structure of a Mercurial repository. - * @return true if found valid repository - * @throws HgException if there are errors accessing specified location - * @throws IllegalArgumentException if argument is null - */ - public boolean initFrom(File repoLocation) throws HgException { - if (repoLocation == null) { - throw new IllegalArgumentException(); - } - repo = new HgLookup().detect(repoLocation); - return repo != null && !repo.isInvalid(); - } - - public HgRepository getRepository() { - if (repo == null) { - throw new IllegalStateException("Call any of #init*() methods first first"); - } - return repo; - } - - public HgLogCommand createLogCommand() { - return new HgLogCommand(repo/*, getCommandContext()*/); - } - - public HgStatusCommand createStatusCommand() { - return new HgStatusCommand(repo/*, getCommandContext()*/); - } - - public HgCatCommand createCatCommand() { - return new HgCatCommand(repo); - } - - public HgManifestCommand createManifestCommand() { - return new HgManifestCommand(repo); - } - - public HgOutgoingCommand createOutgoingCommand() { - return new HgOutgoingCommand(repo); - } - - public HgIncomingCommand createIncomingCommand() { - return new HgIncomingCommand(repo); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgStatus.java --- a/src/org/tmatesoft/hg/core/HgStatus.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,95 +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.util.Date; - -import org.tmatesoft.hg.internal.ChangelogHelper; -import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; -import org.tmatesoft.hg.util.Path; - -/** - * Repository file status and extra handy information. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgStatus { - - public enum Kind { - Modified, Added, Removed, Missing, Unknown, Clean, Ignored - // I'd refrain from changing order of these constants, just in case someone (erroneously, of course ;), uses .ordinal() - }; - - private final HgStatus.Kind kind; - private final Path path; - private final Path origin; - private final ChangelogHelper logHelper; - - HgStatus(HgStatus.Kind kind, Path path, ChangelogHelper changelogHelper) { - this(kind, path, null, changelogHelper); - } - - HgStatus(HgStatus.Kind kind, Path path, Path copyOrigin, ChangelogHelper changelogHelper) { - this.kind = kind; - this.path = path; - origin = copyOrigin; - logHelper = changelogHelper; - } - - public HgStatus.Kind getKind() { - return kind; - } - - public Path getPath() { - return path; - } - - public Path getOriginalPath() { - return origin; - } - - public boolean isCopy() { - return origin != null; - } - - /** - * @return null if author for the change can't be deduced (e.g. for clean files it's senseless) - */ - public String getModificationAuthor() { - RawChangeset cset = logHelper.findLatestChangeWith(path); - if (cset == null) { - if (kind == Kind.Modified || kind == Kind.Added || kind == Kind.Removed /*&& RightBoundary is TIP*/) { - // perhaps, also for Kind.Missing? - return logHelper.getNextCommitUsername(); - } - } else { - return cset.user(); - } - return null; - } - - public Date getModificationDate() { - RawChangeset cset = logHelper.findLatestChangeWith(path); - if (cset == null) { - // FIXME check dirstate and/or local file for tstamp - return new Date(); // what's correct - } else { - return cset.date(); - } - } -} \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/HgStatusCommand.java --- a/src/org/tmatesoft/hg/core/HgStatusCommand.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,285 +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 static org.tmatesoft.hg.core.HgStatus.Kind.*; -import static org.tmatesoft.hg.repo.HgInternals.wrongLocalRevision; -import static org.tmatesoft.hg.repo.HgRepository.*; - -import java.util.ConcurrentModificationException; - -import org.tmatesoft.hg.internal.ChangelogHelper; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.repo.HgStatusCollector; -import org.tmatesoft.hg.repo.HgStatusInspector; -import org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector; -import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.Path.Matcher; - -/** - * Command to obtain file status information, 'hg status' counterpart. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgStatusCommand { - private final HgRepository repo; - - private int startRevision = TIP; - private int endRevision = WORKING_COPY; - - private final Mediator mediator = new Mediator(); - - public HgStatusCommand(HgRepository hgRepo) { - repo = hgRepo; - defaults(); - } - - public HgStatusCommand defaults() { - final Mediator m = mediator; - m.needModified = m.needAdded = m.needRemoved = m.needUnknown = m.needMissing = true; - m.needCopies = m.needClean = m.needIgnored = false; - return this; - } - public HgStatusCommand all() { - final Mediator m = mediator; - m.needModified = m.needAdded = m.needRemoved = m.needUnknown = m.needMissing = true; - m.needCopies = m.needClean = m.needIgnored = true; - return this; - } - - - public HgStatusCommand modified(boolean include) { - mediator.needModified = include; - return this; - } - public HgStatusCommand added(boolean include) { - mediator.needAdded = include; - return this; - } - public HgStatusCommand removed(boolean include) { - mediator.needRemoved = include; - return this; - } - public HgStatusCommand deleted(boolean include) { - mediator.needMissing = include; - return this; - } - public HgStatusCommand unknown(boolean include) { - mediator.needUnknown = include; - return this; - } - public HgStatusCommand clean(boolean include) { - mediator.needClean = include; - return this; - } - public HgStatusCommand ignored(boolean include) { - mediator.needIgnored = include; - return this; - } - - /** - * If set, either base:revision or base:workingdir - * to unset, pass {@link HgRepository#TIP} or {@link HgRepository#BAD_REVISION} - * @param revision - local revision number to base status from - * @return this for convenience - * @throws IllegalArgumentException when revision is negative or {@link HgRepository#WORKING_COPY} - */ - public HgStatusCommand base(int revision) { - if (revision == WORKING_COPY || wrongLocalRevision(revision)) { - throw new IllegalArgumentException(String.valueOf(revision)); - } - if (revision == BAD_REVISION) { - revision = TIP; - } - startRevision = revision; - return this; - } - - /** - * Revision without base == --change - * Pass {@link HgRepository#WORKING_COPY} or {@link HgRepository#BAD_REVISION} to reset - * @param revision - non-negative local revision number, or any of {@link HgRepository#BAD_REVISION}, {@link HgRepository#WORKING_COPY} or {@link HgRepository#TIP} - * @return this for convenience - * @throws IllegalArgumentException if local revision number doesn't specify legitimate revision. - */ - public HgStatusCommand revision(int revision) { - if (revision == BAD_REVISION) { - revision = WORKING_COPY; - } - if (wrongLocalRevision(revision)) { - throw new IllegalArgumentException(String.valueOf(revision)); - } - endRevision = revision; - return this; - } - - /** - * Shorthand for {@link #base(int) cmd.base(BAD_REVISION)}{@link #change(int) .revision(revision)} - * - * @param revision compare given revision against its parent - * @return this for convenience - */ - public HgStatusCommand change(int revision) { - base(BAD_REVISION); - return revision(revision); - } - - /** - * Limit status operation to certain sub-tree. - * - * @param pathMatcher - matcher to use, pass null/ to reset - * @return this for convenience - */ - public HgStatusCommand match(Path.Matcher pathMatcher) { - mediator.matcher = pathMatcher; - return this; - } - - public HgStatusCommand subrepo(boolean visit) { - throw HgRepository.notImplemented(); - } - - /** - * Perform status operation according to parameters set. - * - * @param handler callback to get status information - * @throws IllegalArgumentException if handler is null - * @throws ConcurrentModificationException if this command already runs (i.e. being used from another thread) - */ - public void execute(Handler statusHandler) { - if (statusHandler == null) { - throw new IllegalArgumentException(); - } - if (mediator.busy()) { - throw new ConcurrentModificationException(); - } - HgStatusCollector sc = new HgStatusCollector(repo); // TODO from CommandContext -// PathPool pathHelper = new PathPool(repo.getPathHelper()); // TODO from CommandContext - 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)); - if (endRevision == WORKING_COPY) { - HgWorkingCopyStatusCollector wcsc = new HgWorkingCopyStatusCollector(repo); - wcsc.setBaseRevisionCollector(sc); - wcsc.walk(startRevision, mediator); - } else { - if (startRevision == TIP) { - sc.change(endRevision, mediator); - } else { - sc.walk(startRevision, endRevision, mediator); - } - } - } finally { - mediator.done(); - } - } - - public interface Handler { - void handleStatus(HgStatus s); - } - - private class Mediator implements HgStatusInspector { - boolean needModified; - boolean needAdded; - boolean needRemoved; - boolean needUnknown; - boolean needMissing; - boolean needClean; - boolean needIgnored; - boolean needCopies; - Matcher matcher; - Handler handler; - private ChangelogHelper logHelper; - - Mediator() { - } - - public void start(Handler h, ChangelogHelper changelogHelper) { - handler = h; - logHelper = changelogHelper; - } - - public void done() { - handler = null; - logHelper = null; - } - - public boolean busy() { - return handler != null; - } - - public void modified(Path fname) { - if (needModified) { - if (matcher == null || matcher.accept(fname)) { - handler.handleStatus(new HgStatus(Modified, fname, logHelper)); - } - } - } - public void added(Path fname) { - if (needAdded) { - if (matcher == null || matcher.accept(fname)) { - handler.handleStatus(new HgStatus(Added, fname, logHelper)); - } - } - } - public void removed(Path fname) { - if (needRemoved) { - if (matcher == null || matcher.accept(fname)) { - handler.handleStatus(new HgStatus(Removed, fname, logHelper)); - } - } - } - public void copied(Path fnameOrigin, Path fnameAdded) { - if (needCopies) { - if (matcher == null || matcher.accept(fnameAdded)) { - // 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)); - } - } - } - public void missing(Path fname) { - if (needMissing) { - if (matcher == null || matcher.accept(fname)) { - handler.handleStatus(new HgStatus(Missing, fname, logHelper)); - } - } - } - public void unknown(Path fname) { - if (needUnknown) { - if (matcher == null || matcher.accept(fname)) { - handler.handleStatus(new HgStatus(Unknown, fname, logHelper)); - } - } - } - public void clean(Path fname) { - if (needClean) { - if (matcher == null || matcher.accept(fname)) { - handler.handleStatus(new HgStatus(Clean, fname, logHelper)); - } - } - } - public void ignored(Path fname) { - if (needIgnored) { - if (matcher == null || matcher.accept(fname)) { - handler.handleStatus(new HgStatus(Ignored, fname, logHelper)); - } - } - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/Nodeid.java --- a/src/org/tmatesoft/hg/core/Nodeid.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,187 +0,0 @@ -/* - * Copyright (c) 2010-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 static org.tmatesoft.hg.internal.DigestHelper.toHexString; - -import java.util.Arrays; - - - -/** - * A 20-bytes (40 characters) long hash value to identify a revision. - * @see http://mercurial.selenic.com/wiki/Nodeid - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - * - */ -public final class Nodeid implements Comparable { - - /** - * nullid, empty root revision. - */ - public static final Nodeid NULL = new Nodeid(new byte[20], false); - - private final byte[] binaryData; - - /** - * @param binaryRepresentation - array of exactly 20 bytes - * @param shallClone - true if array is subject to future modification and shall be copied, not referenced - * @throws IllegalArgumentException if supplied binary representation doesn't correspond to 20 bytes of sha1 digest - */ - public Nodeid(byte[] binaryRepresentation, boolean shallClone) { - // 5 int fields => 32 bytes - // byte[20] => 48 bytes (16 bytes is Nodeid with one field, 32 bytes for byte[20] - if (binaryRepresentation == null || binaryRepresentation.length != 20) { - throw new IllegalArgumentException(); - } - /* - * byte[].clone() is not reflected when ran with -agentlib:hprof=heap=sites - * thus not to get puzzled why there are N Nodeids and much less byte[] instances, - * may use following code to see N byte[] as well. - * - if (shallClone) { - binaryData = new byte[20]; - System.arraycopy(binaryRepresentation, 0, binaryData, 0, 20); - } else { - binaryData = binaryRepresentation; - } - */ - binaryData = shallClone ? binaryRepresentation.clone() : binaryRepresentation; - } - - @Override - public int hashCode() { - // digest (part thereof) seems to be nice candidate for the hashCode - byte[] b = binaryData; - return b[0] << 24 | (b[1] & 0xFF) << 16 | (b[2] & 0xFF) << 8 | (b[3] & 0xFF); - } - - @Override - public boolean equals(Object o) { - if (o instanceof Nodeid) { - return this == o || equalsTo(((Nodeid) o).binaryData); - } - return false; - } - - public boolean equalsTo(byte[] buf) { - return Arrays.equals(this.binaryData, buf); - } - - public int compareTo(Nodeid o) { - if (this == o) { - return 0; - } - for (int i = 0; i < 20; i++) { - if (binaryData[i] != o.binaryData[i]) { - return binaryData[i] < o.binaryData[i] ? -1 : 1; - } - } - return 0; - } - - @Override - public String toString() { - // XXX may want to output just single 0 for the NULL id? - return toHexString(binaryData, 0, binaryData.length); - } - - public String shortNotation() { - return toHexString(binaryData, 0, 6); - } - - public boolean isNull() { - if (this == NULL) { - return true; - } - for (int i = 0; i < 20; i++) { - if (this.binaryData[i] != 0) { - return false; - } - } - return true; - } - - // copy - public byte[] toByteArray() { - return binaryData.clone(); - } - - /** - * Factory for {@link Nodeid Nodeids}. - * Primary difference with cons is handling of NULL id (this method returns constant) and control over array - * duplication - this method always makes a copy of an array passed - * @param binaryRepresentation - byte array of a length at least offset + 20 - * @param offset - index in the array to start from - * @throws IllegalArgumentException when arguments don't select 20 bytes - */ - public static Nodeid fromBinary(byte[] binaryRepresentation, int offset) { - if (binaryRepresentation == null || binaryRepresentation.length - offset < 20) { - throw new IllegalArgumentException(); - } - int i = 0; - while (i < 20 && binaryRepresentation[offset+i] == 0) i++; - if (i == 20) { - return NULL; - } - if (offset == 0 && binaryRepresentation.length == 20) { - return new Nodeid(binaryRepresentation, true); - } - byte[] b = new byte[20]; // create new instance if no other reasonable guesses possible - System.arraycopy(binaryRepresentation, offset, b, 0, 20); - return new Nodeid(b, false); - } - - /** - * Parse encoded representation. - * - * @param asciiRepresentation - encoded form of the Nodeid. - * @return object representation - * @throws IllegalArgumentException when argument doesn't match encoded form of 20-bytes sha1 digest. - */ - public static Nodeid fromAscii(String asciiRepresentation) { - if (asciiRepresentation.length() != 40) { - throw new IllegalArgumentException(); - } - // XXX is better impl for String possible? - return fromAscii(asciiRepresentation.getBytes(), 0, 40); - } - - /** - * Parse encoded representation. Similar to {@link #fromAscii(String)}. - */ - public static Nodeid fromAscii(byte[] asciiRepresentation, int offset, int length) { - if (length != 40) { - throw new IllegalArgumentException(); - } - byte[] data = new byte[20]; - boolean zeroBytes = true; - for (int i = 0, j = offset; i < data.length; i++) { - int hiNibble = Character.digit(asciiRepresentation[j++], 16); - int lowNibble = Character.digit(asciiRepresentation[j++], 16); - byte b = (byte) (((hiNibble << 4) | lowNibble) & 0xFF); - data[i] = b; - zeroBytes = zeroBytes && b == 0; - } - if (zeroBytes) { - return NULL; - } - return new Nodeid(data, false); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/core/package.html --- a/src/org/tmatesoft/hg/core/package.html Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ - - -Hi-level API - - \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/ByteArrayChannel.java --- a/src/org/tmatesoft/hg/internal/ByteArrayChannel.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,91 +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 java.nio.ByteBuffer; -import java.util.LinkedList; -import java.util.List; - -import org.tmatesoft.hg.util.ByteChannel; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class ByteArrayChannel implements ByteChannel { - private final List buffers; - private ByteBuffer target; - private byte[] result; - - public ByteArrayChannel() { - this(-1); - } - - public ByteArrayChannel(int size) { - if (size == -1) { - buffers = new LinkedList(); - } else { - if (size < 0) { - throw new IllegalArgumentException(String.valueOf(size)); - } - buffers = null; - target = ByteBuffer.allocate(size); - } - } - - // TODO document what happens on write after toArray() in each case - public int write(ByteBuffer buffer) { - int rv = buffer.remaining(); - if (buffers == null) { - target.put(buffer); - } else { - ByteBuffer copy = ByteBuffer.allocate(rv); - copy.put(buffer); - buffers.add(copy); - } - return rv; - } - - public byte[] toArray() { - if (result != null) { - return result; - } - if (buffers == null) { - assert target.hasArray(); - // int total = target.position(); - // System.arraycopy(target.array(), new byte[total]); - // I don't want to duplicate byte[] for now - // although correct way of doing things is to make a copy and discard target - return target.array(); - } else { - int total = 0; - for (ByteBuffer bb : buffers) { - bb.flip(); - total += bb.limit(); - } - result = new byte[total]; - int off = 0; - for (ByteBuffer bb : buffers) { - bb.get(result, off, bb.limit()); - off += bb.limit(); - } - buffers.clear(); - return result; - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/ByteArrayDataAccess.java --- a/src/org/tmatesoft/hg/internal/ByteArrayDataAccess.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,90 +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 java.io.IOException; - - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class ByteArrayDataAccess extends DataAccess { - - private final byte[] data; - private final int offset; - private final int length; - private int pos; - - public ByteArrayDataAccess(byte[] data) { - this(data, 0, data.length); - } - - public ByteArrayDataAccess(byte[] data, int offset, int length) { - this.data = data; - this.offset = offset; - this.length = length; - pos = 0; - } - - @Override - public byte readByte() throws IOException { - if (pos >= length) { - throw new IOException(); - } - return data[offset + pos++]; - } - @Override - public void readBytes(byte[] buf, int off, int len) throws IOException { - if (len > (this.length - pos)) { - throw new IOException(); - } - System.arraycopy(data, pos, buf, off, len); - pos += len; - } - - @Override - public ByteArrayDataAccess reset() { - pos = 0; - return this; - } - @Override - public int length() { - return length; - } - @Override - public void seek(int offset) { - pos = (int) offset; - } - @Override - public void skip(int bytes) throws IOException { - seek(pos + bytes); - } - @Override - public boolean isEmpty() { - return pos >= length; - } - - // - - // when byte[] needed from DA, we may save few cycles and some memory giving this (otherwise unsafe) access to underlying data - @Override - public byte[] byteArray() { - return data; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/ChangelogHelper.java --- a/src/org/tmatesoft/hg/internal/ChangelogHelper.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,83 +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 java.util.TreeMap; - -import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; -import org.tmatesoft.hg.repo.HgDataFile; -import org.tmatesoft.hg.repo.HgInternals; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.Path; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class ChangelogHelper { - private final int leftBoundary; - private final HgRepository repo; - private final TreeMap cache = new TreeMap(); - private String nextCommitAuthor; - - /** - * @param hgRepo - * @param leftBoundaryRevision walker never visits revisions with local numbers less than specified, - * IOW only revisions [leftBoundaryRevision..TIP] are considered. - */ - public ChangelogHelper(HgRepository hgRepo, int leftBoundaryRevision) { - repo = hgRepo; - leftBoundary = leftBoundaryRevision; - } - - /** - * @return the repo - */ - public HgRepository getRepo() { - return repo; - } - - /** - * Walks changelog in reverse order - * @param file - * @return changeset where specified file is mentioned among affected files, or - * null if none found up to leftBoundary - */ - public RawChangeset findLatestChangeWith(Path file) { - HgDataFile df = repo.getFileNode(file); - int changelogRev = df.getChangesetLocalRevision(HgRepository.TIP); - if (changelogRev >= leftBoundary) { - // the method is likely to be invoked for different files, - // while changesets might be the same. Cache 'em not to read too much. - RawChangeset cs = cache.get(changelogRev); - if (cs == null) { - cs = repo.getChangelog().range(changelogRev, changelogRev).get(0); - cache.put(changelogRev, cs); - } - return cs; - } - return null; - } - - public String getNextCommitUsername() { - if (nextCommitAuthor == null) { - nextCommitAuthor = new HgInternals(repo).getNextCommitUsername(); - } - return nextCommitAuthor; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/ConfigFile.java --- a/src/org/tmatesoft/hg/internal/ConfigFile.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,151 +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 java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class ConfigFile { - - private List sections; - private List> content; - - ConfigFile() { - } - - public void addLocation(File path) { - read(path); - } - - public boolean hasSection(String sectionName) { - return sections == null ? false : sections.indexOf(sectionName) == -1; - } - - // XXX perhaps, should be moved to subclass HgRepoConfig, as it is not common operation for any config file - public boolean hasEnabledExtension(String extensionName) { - int x = sections != null ? sections.indexOf("extensions") : -1; - if (x == -1) { - return false; - } - String value = content.get(x).get(extensionName); - return value != null && !"!".equals(value); - } - - public List getSectionNames() { - return sections == null ? Collections.emptyList() : Collections.unmodifiableList(sections); - } - - public Map getSection(String sectionName) { - if (sections == null) { - return Collections.emptyMap(); - } - int x = sections.indexOf(sectionName); - if (x == -1) { - return Collections.emptyMap(); - } - return Collections.unmodifiableMap(content.get(x)); - } - - public boolean getBoolean(String sectionName, String key, boolean defaultValue) { - String value = getSection(sectionName).get(key); - if (value == null) { - return defaultValue; - } - for (String s : new String[] { "true", "yes", "on", "1" }) { - if (s.equalsIgnoreCase(value)) { - return true; - } - } - return false; - } - - public String getString(String sectionName, String key, String defaultValue) { - String value = getSection(sectionName).get(key); - return value == null ? defaultValue : value; - } - - // TODO handle %include and %unset directives - // TODO "" and lists - private void read(File f) { - if (f == null || !f.canRead()) { - return; - } - if (sections == null) { - sections = new ArrayList(); - content = new ArrayList>(); - } - try { - BufferedReader br = new BufferedReader(new FileReader(f)); - String line; - String sectionName = ""; - Map section = new LinkedHashMap(); - while ((line = br.readLine()) != null) { - line = line.trim(); - int x; - if ((x = line.indexOf('#')) != -1) { - // do not keep comments in memory, get new, shorter string - line = new String(line.substring(0, x).trim()); - } - if (line.length() <= 2) { // a=b or [a] are at least of length 3 - continue; - } - if (line.charAt(0) == '[' && line.charAt(line.length() - 1) == ']') { - sectionName = line.substring(1, line.length() - 1); - if (sections.indexOf(sectionName) == -1) { - sections.add(sectionName); - content.add(section = new LinkedHashMap()); - } else { - section = null; // drop cached value - } - } else if ((x = line.indexOf('=')) != -1) { - // share char[] of the original string - String key = line.substring(0, x).trim(); - String value = line.substring(x+1).trim(); - if (section == null) { - int i = sections.indexOf(sectionName); - assert i >= 0; - section = content.get(i); - } - if (sectionName.length() == 0) { - // add fake section only if there are any values - sections.add(sectionName); - content.add(section); - } - section.put(key, value); - } - } - br.close(); - } catch (IOException ex) { - ex.printStackTrace(); // XXX shall outer world care? - } - ((ArrayList) sections).trimToSize(); - ((ArrayList) content).trimToSize(); - assert sections.size() == content.size(); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/DataAccess.java --- a/src/org/tmatesoft/hg/internal/DataAccess.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,102 +0,0 @@ -/* - * Copyright (c) 2010-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 java.io.IOException; -import java.nio.ByteBuffer; - -/** - * relevant parts of DataInput, non-stream nature (seek operation), explicit check for end of data. - * convenient skip (+/- bytes) - * Primary goal - effective file read, so that clients don't need to care whether to call few - * distinct getInt() or readBytes(totalForFewInts) and parse themselves instead in an attempt to optimize. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class DataAccess { - public boolean isEmpty() { - return true; - } - public int length() { - return 0; - } - /** - * get this instance into initial state - * @throws IOException - * @return this for convenience - */ - public DataAccess reset() throws IOException { - // nop, empty instance is always in the initial state - return this; - } - // absolute positioning - public void seek(int offset) throws IOException { - throw new UnsupportedOperationException(); - } - // relative positioning - public void skip(int bytes) throws IOException { - throw new UnsupportedOperationException(); - } - // shall be called once this object no longer needed - public void done() { - // no-op in this empty implementation - } - public int readInt() throws IOException { - byte[] b = new byte[4]; - readBytes(b, 0, 4); - return b[0] << 24 | (b[1] & 0xFF) << 16 | (b[2] & 0xFF) << 8 | (b[3] & 0xFF); - } - public long readLong() throws IOException { - byte[] b = new byte[8]; - readBytes(b, 0, 8); - int i1 = b[0] << 24 | (b[1] & 0xFF) << 16 | (b[2] & 0xFF) << 8 | (b[3] & 0xFF); - int i2 = b[4] << 24 | (b[5] & 0xFF) << 16 | (b[6] & 0xFF) << 8 | (b[7] & 0xFF); - return ((long) i1) << 32 | ((long) i2 & 0xFFFFFFFFl); - } - public void readBytes(byte[] buf, int offset, int length) throws IOException { - throw new UnsupportedOperationException(); - } - // 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) - public void readBytes(ByteBuffer buf) throws IOException { -// int toRead = Math.min(buf.remaining(), (int) length()); -// if (buf.hasArray()) { -// readBytes(buf.array(), buf.arrayOffset(), toRead); -// } else { -// byte[] bb = new byte[toRead]; -// readBytes(bb, 0, bb.length); -// buf.put(bb); -// } - // FIXME optimize to read as much as possible at once - while (!isEmpty() && buf.hasRemaining()) { - buf.put(readByte()); - } - } - public byte readByte() throws IOException { - throw new UnsupportedOperationException(); - } - - // 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 - public byte[] byteArray() throws IOException { - reset(); - byte[] rv = new byte[length()]; - readBytes(rv, 0, rv.length); - return rv; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/DataAccessProvider.java --- a/src/org/tmatesoft/hg/internal/DataAccessProvider.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,307 +0,0 @@ -/* - * Copyright (c) 2010-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 java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; - -import org.tmatesoft.hg.core.HgBadStateException; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class DataAccessProvider { - - private final int mapioMagicBoundary; - private final int bufferSize; - - public DataAccessProvider() { - this(100 * 1024, 8 * 1024); - } - - public DataAccessProvider(int mapioBoundary, int regularBufferSize) { - mapioMagicBoundary = mapioBoundary; - bufferSize = regularBufferSize; - } - - public DataAccess create(File f) { - if (!f.exists()) { - return new DataAccess(); - } - 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"); - } - if (flen > mapioMagicBoundary) { - // TESTS: bufLen of 1024 was used to test MemMapFileAccess - return new MemoryMapFileAccess(fc, flen, mapioMagicBoundary); - } 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; - // 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); - } - } catch (IOException ex) { - // unlikely to happen, we've made sure file exists. - ex.printStackTrace(); // FIXME log error - } - 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 final int memBufferSize; - private MappedByteBuffer buffer; - - public MemoryMapFileAccess(FileChannel fc, int channelSize, int /*long?*/ bufferSize) { - fileChannel = fc; - size = channelSize; - memBufferSize = bufferSize; - } - - @Override - public boolean isEmpty() { - return position + (buffer == null ? 0 : buffer.position()) >= size; - } - - @Override - public int length() { - return size; - } - - @Override - public DataAccess reset() throws IOException { - seek(0); - return this; - } - - @Override - public void seek(int 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)); - } else { - position = offset; - buffer = null; - } - } - - @Override - public void skip(int bytes) throws IOException { - assert bytes >= 0; - if (buffer == null) { - position += bytes; - return; - } - if (buffer.remaining() > bytes) { - buffer.position(buffer.position() + bytes); - } else { - position += buffer.position() + bytes; - buffer = null; - } - } - - private void fill() throws IOException { - if (buffer != null) { - position += buffer.position(); - } - long left = size - position; - buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, position, left < memBufferSize ? left : memBufferSize); - } - - @Override - public void readBytes(byte[] buf, int offset, int length) throws IOException { - if (buffer == null || !buffer.hasRemaining()) { - fill(); - } - // XXX in fact, we may try to create a MappedByteBuffer of exactly length size here, and read right away - while (length > 0) { - int tail = buffer.remaining(); - if (tail == 0) { - throw new IOException(); - } - if (tail >= length) { - buffer.get(buf, offset, length); - } else { - buffer.get(buf, offset, tail); - fill(); - } - offset += tail; - length -= tail; - } - } - - @Override - public byte readByte() throws IOException { - if (buffer == null || !buffer.hasRemaining()) { - fill(); - } - if (buffer.hasRemaining()) { - return buffer.get(); - } - throw new IOException(); - } - - @Override - public void done() { - buffer = null; - if (fileChannel != null) { - try { - fileChannel.close(); - } catch (IOException ex) { - ex.printStackTrace(); // log debug - } - fileChannel = null; - } - } - } - - // (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. - - public FileAccess(FileChannel fc, int channelSize, int bufferSizeHint, boolean useDirect) { - fileChannel = fc; - size = channelSize; - final int capacity = size < bufferSizeHint ? size : bufferSizeHint; - buffer = useDirect ? ByteBuffer.allocateDirect(capacity) : ByteBuffer.allocate(capacity); - buffer.flip(); // or .limit(0) to indicate it's empty - } - - @Override - public boolean isEmpty() { - return bufferStartInFile + buffer.position() >= size; - } - - @Override - public int length() { - return size; - } - - @Override - public DataAccess reset() throws IOException { - seek(0); - return this; - } - - @Override - public void seek(int offset) throws IOException { - if (offset > size) { - throw new IllegalArgumentException(); - } - if (offset < bufferStartInFile + buffer.limit() && offset >= bufferStartInFile) { - buffer.position((int) (offset - bufferStartInFile)); - } else { - // out of current buffer, invalidate it (force re-read) - // XXX or ever re-read it right away? - bufferStartInFile = offset; - buffer.clear(); - buffer.limit(0); // or .flip() to indicate we switch to reading - fileChannel.position(offset); - } - } - - @Override - public void skip(int bytes) throws IOException { - final int newPos = buffer.position() + bytes; - if (newPos >= 0 && newPos < buffer.limit()) { - // no need to move file pointer, just rewind/seek buffer - buffer.position(newPos); - } else { - // - seek(bufferStartInFile + newPos); - } - } - - private boolean fill() throws IOException { - if (!buffer.hasRemaining()) { - bufferStartInFile += buffer.limit(); - buffer.clear(); - if (bufferStartInFile < size) { // just in case there'd be any exception on EOF, not -1 - fileChannel.read(buffer); - // may return -1 when EOF, but empty will reflect this, hence no explicit support here - } - buffer.flip(); - } - return buffer.hasRemaining(); - } - - @Override - public void readBytes(byte[] buf, int offset, int length) throws IOException { - if (!buffer.hasRemaining()) { - fill(); - } - while (length > 0) { - int tail = buffer.remaining(); - if (tail == 0) { - throw new IOException(); // shall not happen provided stream contains expected data and no attempts to read past isEmpty() == true are made. - } - if (tail >= length) { - buffer.get(buf, offset, length); - } else { - buffer.get(buf, offset, tail); - fill(); - } - offset += tail; - length -= tail; - } - } - - @Override - public byte readByte() throws IOException { - if (buffer.hasRemaining()) { - return buffer.get(); - } - if (fill()) { - return buffer.get(); - } - throw new IOException(); - } - - @Override - public void done() { - if (buffer != null) { - buffer = null; - } - if (fileChannel != null) { - try { - fileChannel.close(); - } catch (IOException ex) { - ex.printStackTrace(); // log debug - } - fileChannel = null; - } - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/DigestHelper.java --- a/src/org/tmatesoft/hg/internal/DigestHelper.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,125 +0,0 @@ -/* - * Copyright (c) 2010-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 java.io.IOException; -import java.io.InputStream; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -import org.tmatesoft.hg.core.Nodeid; - - -/** - *
- * DigestHelper dh;
- * dh.sha1(...).asHexString();
- *  or 
- * dh = dh.sha1(...);
- * nodeid.equalsTo(dh.asBinary());
- * 
- * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class DigestHelper { - private MessageDigest sha1; - private byte[] digest; - - public DigestHelper() { - } - - private MessageDigest getSHA1() { - if (sha1 == null) { - try { - sha1 = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException ex) { - // could hardly happen, JDK from Sun always has sha1. - ex.printStackTrace(); // FIXME log error - } - } - return sha1; - } - - - public DigestHelper sha1(Nodeid nodeid1, Nodeid nodeid2, byte[] data) { - return sha1(nodeid1.toByteArray(), nodeid2.toByteArray(), data); - } - - // sha1_digest(min(p1,p2) ++ max(p1,p2) ++ final_text) - public DigestHelper sha1(byte[] nodeidParent1, byte[] nodeidParent2, byte[] data) { - MessageDigest alg = getSHA1(); - if ((nodeidParent1[0] & 0x00FF) < (nodeidParent2[0] & 0x00FF)) { - alg.update(nodeidParent1); - alg.update(nodeidParent2); - } else { - alg.update(nodeidParent2); - alg.update(nodeidParent1); - } - digest = alg.digest(data); - assert digest.length == 20; - return this; - } - - public String asHexString() { - if (digest == null) { - throw new IllegalStateException("Shall init with sha1() call first"); - } - return toHexString(digest, 0, digest.length); - } - - // by reference, be careful not to modify (or #clone() if needed) - public byte[] asBinary() { - if (digest == null) { - throw new IllegalStateException("Shall init with sha1() call first"); - } - return digest; - } - - // XXX perhaps, digest functions should throw an exception, as it's caller responsibility to deal with eof, etc - public DigestHelper sha1(InputStream is /*ByteBuffer*/) throws IOException { - MessageDigest alg = getSHA1(); - byte[] buf = new byte[1024]; - int c; - while ((c = is.read(buf)) != -1) { - alg.update(buf, 0, c); - } - digest = alg.digest(); - return this; - } - - public DigestHelper sha1(CharSequence... seq) { - MessageDigest alg = getSHA1(); - for (CharSequence s : seq) { - byte[] b = s.toString().getBytes(); - alg.update(b); - } - digest = alg.digest(); - return this; - } - - public static String toHexString(byte[] data, final int offset, final int count) { - char[] result = new char[count << 1]; - final String hexDigits = "0123456789abcdef"; - final int end = offset+count; - for (int i = offset, j = 0; i < end; i++) { - result[j++] = hexDigits.charAt((data[i] >>> 4) & 0x0F); - result[j++] = hexDigits.charAt(data[i] & 0x0F); - } - return new String(result); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/Filter.java --- a/src/org/tmatesoft/hg/internal/Filter.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +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 java.nio.ByteBuffer; - -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.Path; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public interface Filter { - - // returns a buffer ready to be read. may return original buffer. - // original buffer may not be fully consumed, #compact() might be operation to perform - ByteBuffer filter(ByteBuffer src); - - interface Factory { - void initialize(HgRepository hgRepo, ConfigFile cfg); - // may return null if for a given path and/or options this filter doesn't make any sense - Filter create(Path path, Options opts); - } - - enum Direction { - FromRepo, ToRepo - } - - public class Options { - - private final Direction direction; - public Options(Direction dir) { - direction = dir; - } - - Direction getDirection() { - return direction; - } - - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/FilterByteChannel.java --- a/src/org/tmatesoft/hg/internal/FilterByteChannel.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,66 +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 java.io.IOException; -import java.nio.ByteBuffer; -import java.util.Collection; - -import org.tmatesoft.hg.util.Adaptable; -import org.tmatesoft.hg.util.ByteChannel; -import org.tmatesoft.hg.util.CancelledException; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class FilterByteChannel implements ByteChannel, Adaptable { - private final Filter[] filters; - private final ByteChannel delegate; - - public FilterByteChannel(ByteChannel delegateChannel, Collection filtersToApply) { - if (delegateChannel == null || filtersToApply == null) { - throw new IllegalArgumentException(); - } - delegate = delegateChannel; - filters = filtersToApply.toArray(new Filter[filtersToApply.size()]); - } - - public int write(ByteBuffer buffer) throws IOException, CancelledException { - final int srcPos = buffer.position(); - ByteBuffer processed = buffer; - for (Filter f : filters) { - // each next filter consumes not more than previous - // hence total consumed equals position shift in the original buffer - processed = f.filter(processed); - } - delegate.write(processed); - return buffer.position() - srcPos; // consumed as much from original buffer - } - - // adapters or implemented interfaces of the original class shall not be obfuscated by filter - public T getAdapter(Class adapterClass) { - if (delegate instanceof Adaptable) { - return ((Adaptable) delegate).getAdapter(adapterClass); - } - if (adapterClass != null && adapterClass.isInstance(delegate)) { - return adapterClass.cast(delegate); - } - return null; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/FilterDataAccess.java --- a/src/org/tmatesoft/hg/internal/FilterDataAccess.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,116 +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 java.io.IOException; - - -/** - * XXX Perhaps, DataAccessSlice? Unlike FilterInputStream, we limit amount of data read from DataAccess being filtered. - * - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class FilterDataAccess extends DataAccess { - private final DataAccess dataAccess; - private final int offset; - private final int length; - private int count; - - public FilterDataAccess(DataAccess dataAccess, int offset, int length) { - this.dataAccess = dataAccess; - this.offset = offset; - this.length = length; - count = length; - } - - protected int available() { - return count; - } - - @Override - public FilterDataAccess reset() throws IOException { - count = length; - return this; - } - - @Override - public boolean isEmpty() { - return count <= 0; - } - - @Override - public int length() { - return length; - } - - @Override - public void seek(int localOffset) throws IOException { - if (localOffset < 0 || localOffset > length) { - throw new IllegalArgumentException(); - } - dataAccess.seek(offset + localOffset); - count = (int) (length - localOffset); - } - - @Override - public void skip(int bytes) throws IOException { - int newCount = count - bytes; - if (newCount < 0 || newCount > length) { - throw new IllegalArgumentException(); - } - seek(length - newCount); - /* - can't use next code because don't want to rewind backing DataAccess on reset() - i.e. this.reset() modifies state of this instance only, while filtered DA may go further. - Only actual this.skip/seek/read would rewind it to desired position - dataAccess.skip(bytes); - count = newCount; - */ - - } - - @Override - public byte readByte() throws IOException { - if (count <= 0) { - throw new IllegalArgumentException("Underflow"); // XXX be descriptive - } - if (count == length) { - dataAccess.seek(offset); - } - count--; - return dataAccess.readByte(); - } - - @Override - public void readBytes(byte[] b, int off, int len) throws IOException { - if (len == 0) { - return; - } - if (count <= 0 || len > count) { - throw new IllegalArgumentException(String.format("Underflow. Bytes left: %d, asked to read %d", count, len)); - } - if (count == length) { - dataAccess.seek(offset); - } - dataAccess.readBytes(b, off, len); - count -= len; - } - - // done shall be no-op, as we have no idea what's going on with DataAccess we filter -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/InflaterDataAccess.java --- a/src/org/tmatesoft/hg/internal/InflaterDataAccess.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,185 +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 java.io.EOFException; -import java.io.IOException; -import java.util.zip.DataFormatException; -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? - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class InflaterDataAccess extends FilterDataAccess { - - private final Inflater inflater; - private final byte[] buffer; - private final byte[] singleByte = new byte[1]; - private int decompressedPos = 0; - private int decompressedLength; - - public InflaterDataAccess(DataAccess dataAccess, int offset, int compressedLength) { - this(dataAccess, offset, compressedLength, -1, new Inflater(), 512); - } - - public InflaterDataAccess(DataAccess dataAccess, int offset, int compressedLength, int actualLength) { - this(dataAccess, offset, compressedLength, actualLength, new Inflater(), 512); - } - - public InflaterDataAccess(DataAccess dataAccess, int offset, int compressedLength, int actualLength, Inflater inflater, int bufSize) { - super(dataAccess, offset, compressedLength); - this.inflater = inflater; - this.decompressedLength = actualLength; - buffer = new byte[bufSize]; - } - - @Override - public InflaterDataAccess reset() throws IOException { - super.reset(); - inflater.reset(); - decompressedPos = 0; - return this; - } - - @Override - protected int available() { - return length() - decompressedPos; - } - - @Override - public boolean isEmpty() { - // 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() { - 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; - } - 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); - } - } - 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 - } - } - - @Override - public void seek(int localOffset) throws IOException { - if (localOffset < 0 /* || localOffset >= length() */) { - throw new IllegalArgumentException(); - } - if (localOffset >= decompressedPos) { - skip((int) (localOffset - decompressedPos)); - } else { - reset(); - skip((int) localOffset); - } - } - - @Override - public void skip(int bytes) throws IOException { - if (bytes < 0) { - bytes += decompressedPos; - if (bytes < 0) { - throw new IOException("Underflow. Rewind past start of the slice."); - } - reset(); - // fall-through - } - while (!isEmpty() && bytes > 0) { - readByte(); - bytes--; - } - if (bytes != 0) { - throw new IOException("Underflow. Rewind past end of the slice"); - } - } - - @Override - public byte readByte() throws IOException { - readBytes(singleByte, 0, 1); - return singleByte[0]; - } - - @Override - public void readBytes(byte[] b, int off, int len) throws IOException { - try { - int n; - while (len > 0) { - while ((n = inflater.inflate(b, off, len)) == 0) { - // FIXME few last bytes (checksum?) may be ignored by inflater, thus inflate may return 0 in - // perfectly legal conditions (when all data already expanded, but there are still some bytes - // in the input stream - if (inflater.finished() || inflater.needsDictionary()) { - throw new EOFException(); - } - if (inflater.needsInput()) { - // fill: - int toRead = super.available(); - if (toRead > buffer.length) { - toRead = buffer.length; - } - super.readBytes(buffer, 0, toRead); - inflater.setInput(buffer, 0, toRead); - } - } - off += n; - len -= n; - decompressedPos += n; - if (len == 0) { - return; // filled - } - } - } catch (DataFormatException e) { - String s = e.getMessage(); - throw new ZipException(s != null ? s : "Invalid ZLIB data format"); - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/Internals.java --- a/src/org/tmatesoft/hg/internal/Internals.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,111 +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 static org.tmatesoft.hg.internal.RequiresFile.*; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.PathRewrite; - -/** - * Fields/members that shall not be visible - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class Internals { - - private int requiresFlags = 0; - private List filterFactories; - - - public Internals() { - } - - public/*for tests, otherwise pkg*/ void setStorageConfig(int version, int flags) { - requiresFlags = flags; - } - - // XXX perhaps, should keep both fields right here, not in the HgRepository - public PathRewrite buildDataFilesHelper() { - return new StoragePathHelper((requiresFlags & STORE) != 0, (requiresFlags & FNCACHE) != 0, (requiresFlags & DOTENCODE) != 0); - } - - public PathRewrite buildRepositoryFilesHelper() { - if ((requiresFlags & STORE) != 0) { - return new PathRewrite() { - public String rewrite(String path) { - return "store/" + path; - } - }; - } else { - return new PathRewrite() { - public String rewrite(String path) { - //no-op - return path; - } - }; - } - } - - public ConfigFile newConfigFile() { - return new ConfigFile(); - } - - public List getFilters(HgRepository hgRepo, ConfigFile cfg) { - if (filterFactories == null) { - filterFactories = new ArrayList(); - if (cfg.hasEnabledExtension("eol")) { - NewlineFilter.Factory ff = new NewlineFilter.Factory(); - ff.initialize(hgRepo, cfg); - filterFactories.add(ff); - } - if (cfg.hasEnabledExtension("keyword")) { - KeywordFilter.Factory ff = new KeywordFilter.Factory(); - ff.initialize(hgRepo, cfg); - filterFactories.add(ff); - } - } - return filterFactories; - } - - public void initEmptyRepository(File hgDir) throws IOException { - hgDir.mkdir(); - FileOutputStream requiresFile = new FileOutputStream(new File(hgDir, "requires")); - StringBuilder sb = new StringBuilder(40); - sb.append("revlogv1\n"); - if ((requiresFlags & STORE) != 0) { - sb.append("store\n"); - } - if ((requiresFlags & FNCACHE) != 0) { - sb.append("fncache\n"); - } - if ((requiresFlags & DOTENCODE) != 0) { - sb.append("dotencode\n"); - } - requiresFile.write(sb.toString().getBytes()); - requiresFile.close(); - new File(hgDir, "store").mkdir(); // with that, hg verify says ok. - } - -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/KeywordFilter.java --- a/src/org/tmatesoft/hg/internal/KeywordFilter.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,323 +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 java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Map; -import java.util.TreeMap; - -import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.Path; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class KeywordFilter implements Filter { - // present implementation is stateless, however, filter use pattern shall not assume that. In fact, Factory may us that - private final HgRepository repo; - private final boolean isExpanding; - private final TreeMap keywords; - private final int minBufferLen; - private final Path path; - private RawChangeset latestFileCset; - - /** - * - * @param hgRepo - * @param path - * @param expand true to expand keywords, false to shrink - */ - private KeywordFilter(HgRepository hgRepo, Path p, boolean expand) { - repo = hgRepo; - path = p; - isExpanding = expand; - keywords = new TreeMap(); - keywords.put("Id", "Id"); - keywords.put("Revision", "Revision"); - keywords.put("Author", "Author"); - keywords.put("Date", "Date"); - keywords.put("LastChangedRevision", "LastChangedRevision"); - keywords.put("LastChangedBy", "LastChangedBy"); - keywords.put("LastChangedDate", "LastChangedDate"); - keywords.put("Source", "Source"); - keywords.put("Header", "Header"); - - int l = 0; - for (String s : keywords.keySet()) { - if (s.length() > l) { - l = s.length(); - } - } - // FIXME 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*/); - } - - /** - * @param src buffer ready to be read - * @return buffer ready to be read and original buffer's position modified to reflect consumed bytes. IOW, if source buffer - * on return has remaining bytes, they are assumed not-read (not processed) and next chunk passed to filter is supposed to - * start with them - */ - public ByteBuffer filter(ByteBuffer src) { - if (src.capacity() < minBufferLen) { - throw new IllegalStateException(String.format("Need buffer of at least %d bytes to ensure filter won't hang", minBufferLen)); - } - ByteBuffer rv = null; - int keywordStart = -1; - int x = src.position(); - int copyFrom = x; // needs to be updated each time we copy a slice, but not each time we modify source index (x) - while (x < src.limit()) { - if (keywordStart == -1) { - int i = indexOf(src, '$', x, false); - if (i == -1) { - if (rv == null) { - return src; - } else { - copySlice(src, copyFrom, src.limit(), rv); - rv.flip(); - src.position(src.limit()); - return rv; - } - } - keywordStart = i; - // fall-through - } - if (keywordStart >= 0) { - int i = indexOf(src, '$', keywordStart+1, true); - if (i == -1) { - // 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, - // 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 - // not to run into such situation - throw new IllegalStateException("Try src buffer of a greater size"); - } - rv = ByteBuffer.allocate(keywordStart - copyFrom); - } - // copy all from source till latest possible kw start - copySlice(src, copyFrom, keywordStart, rv); - rv.flip(); - // and tell caller we've consumed only to the potential kw start - src.position(keywordStart); - return rv; - } else if (src.get(i) == '$') { - // end of keyword, or start of a new one. - String keyword; - if ((keyword = matchKeyword(src, keywordStart, i)) != null) { - if (rv == null) { - // src.remaining(), not .capacity because src is not read, and remaining represents - // actual bytes count, while capacity - potential. - // Factor of 4 is pure guess and a HACK, need to be fixed with re-expanding buffer on demand - rv = ByteBuffer.allocate(isExpanding ? src.remaining() * 4 : src.remaining()); - } - copySlice(src, copyFrom, keywordStart+1, rv); - rv.put(keyword.getBytes()); - if (isExpanding) { - rv.put((byte) ':'); - rv.put((byte) ' '); - expandKeywordValue(keyword, rv); - rv.put((byte) ' '); - } - rv.put((byte) '$'); - keywordStart = -1; - x = i+1; - copyFrom = x; - continue; - } else { - if (rv != null) { - // we've already did some substitution, thus need to copy bytes we've scanned. - copySlice(src, x, i, rv); - copyFrom = i; - } // no else in attempt to avoid rv creation if no real kw would be found - keywordStart = i; - x = i; // '$' at i wasn't consumed, hence x points to i, not i+1. This is to avoid problems with case: "sdfsd $ asdfs $Id$ sdf" - continue; - } - } else { - assert src.get(i) == '\n' || src.get(i) == '\r'; - // line break - if (rv != null) { - copySlice(src, x, i+1, rv); - copyFrom = i+1; - } - x = i+1; - keywordStart = -1; // Wasn't keyword, really - continue; // try once again - } - } - } - if (keywordStart != -1) { - if (rv == null) { - // no expansion happened yet, and we have potential kw start - rv = ByteBuffer.allocate(keywordStart - src.position()); - copySlice(src, src.position(), keywordStart, rv); - } - src.position(keywordStart); - } - if (rv != null) { - rv.flip(); - return rv; - } - return src; - } - - /** - * @param keyword - * @param rv - */ - private void expandKeywordValue(String keyword, ByteBuffer rv) { - if ("Id".equals(keyword)) { - rv.put(identityString().getBytes()); - } else if ("Revision".equals(keyword)) { - rv.put(revision().getBytes()); - } else if ("Author".equals(keyword)) { - rv.put(username().getBytes()); - } else if ("Date".equals(keyword)) { - rv.put(date().getBytes()); - } else { - throw new IllegalStateException(String.format("Keyword %s is not yet supported", keyword)); - } - } - - private String matchKeyword(ByteBuffer src, int kwStart, int kwEnd) { - assert kwEnd - kwStart - 1 > 0; - assert src.get(kwStart) == src.get(kwEnd) && src.get(kwEnd) == '$'; - char[] chars = new char[kwEnd - kwStart - 1]; - int i; - for (i = 0; i < chars.length; i++) { - char c = (char) src.get(kwStart + 1 + i); - if (c == ':') { - break; - } - chars[i] = c; - } - String kw = new String(chars, 0, i); -// XXX may use subMap to look up keywords based on few available characters (not waiting till closing $) -// System.out.println(keywords.subMap("I", "J")); -// System.out.println(keywords.subMap("A", "B")); -// System.out.println(keywords.subMap("Au", "B")); - return keywords.get(kw); - } - - // copies part of the src buffer, [from..to). doesn't modify src position - static void copySlice(ByteBuffer src, int from, int to, ByteBuffer dst) { - if (to > src.limit()) { - throw new IllegalArgumentException("Bad right boundary"); - } - if (dst.remaining() < to - from) { - throw new IllegalArgumentException("Not enough room in the destination buffer"); - } - for (int i = from; i < to; i++) { - dst.put(src.get(i)); - } - } - - private static int indexOf(ByteBuffer b, char ch, int from, boolean newlineBreaks) { - for (int i = from; i < b.limit(); i++) { - byte c = b.get(i); - if (ch == c) { - return i; - } - if (newlineBreaks && (c == '\n' || c == '\r')) { - return i; - } - } - return -1; - } - - private String identityString() { - return String.format("%s,v %s %s %s", path, revision(), date(), username()); - } - - private String revision() { - // FIXME add cset's nodeid into Changeset class - int csetRev = repo.getFileNode(path).getChangesetLocalRevision(HgRepository.TIP); - return repo.getChangelog().getRevision(csetRev).shortNotation(); - } - - private String username() { - return getChangeset().user(); - } - - private String date() { - return String.format("%tY/% patterns = new ArrayList(); - for (Map.Entry e : cfg.getSection("keyword").entrySet()) { - if (!"ignore".equalsIgnoreCase(e.getValue())) { - patterns.add(e.getKey()); - } - } - matcher = new PathGlobMatcher(patterns.toArray(new String[patterns.size()])); - // TODO read and respect keyword patterns from [keywordmaps] - } - - public Filter create(Path path, Options opts) { - if (matcher.accept(path)) { - return new KeywordFilter(repo, path, opts.getDirection() == Filter.Direction.FromRepo); - } - return null; - } - } - -// -// public static void main(String[] args) throws Exception { -// FileInputStream fis = new FileInputStream(new File("/temp/kwoutput.txt")); -// FileOutputStream fos = new FileOutputStream(new File("/temp/kwoutput2.txt")); -// ByteBuffer b = ByteBuffer.allocate(256); -// KeywordFilter kwFilter = new KeywordFilter(false); -// while (fis.getChannel().read(b) != -1) { -// b.flip(); // get ready to be read -// ByteBuffer f = kwFilter.filter(b); -// fos.getChannel().write(f); // XXX in fact, f may not be fully consumed -// if (b.hasRemaining()) { -// b.compact(); -// } else { -// b.clear(); -// } -// } -// fis.close(); -// fos.flush(); -// fos.close(); -// } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/NewlineFilter.java --- a/src/org/tmatesoft/hg/internal/NewlineFilter.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,269 +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 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 java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Map; - -import org.tmatesoft.hg.repo.HgInternals; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.Path; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class NewlineFilter implements Filter { - - // if allowInconsistent is true, filter simply pass incorrect newline characters (single \r or \r\n on *nix and single \n on Windows) as is, - // i.e. doesn't try to convert them into appropriate newline characters. XXX revisit if Keyword extension behaves differently - private final boolean allowInconsistent; - private final boolean winToNix; - - private NewlineFilter(boolean failIfInconsistent, int transform) { - winToNix = transform == 0; - allowInconsistent = !failIfInconsistent; - } - - public ByteBuffer filter(ByteBuffer src) { - if (winToNix) { - return win2nix(src); - } else { - return nix2win(src); - } - } - - private ByteBuffer win2nix(ByteBuffer src) { - int x = src.position(); // source index - int lookupStart = x; - ByteBuffer dst = null; - while (x < src.limit()) { - // x, lookupStart, ir and in are absolute positions within src buffer, which is never read with modifying operations - int ir = indexOf('\r', src, lookupStart); - int in = indexOf('\n', src, lookupStart); - if (ir == -1) { - if (in == -1 || allowInconsistent) { - if (dst != null) { - copySlice(src, x, src.limit(), dst); - x = src.limit(); // consumed all - } - break; - } else { - fail(src, in); - } - } - // in == -1 while ir != -1 may be valid case if ir is the last char of the buffer, we check below for that - if (in != -1 && in != ir+1 && !allowInconsistent) { - fail(src, in); - } - if (dst == null) { - dst = ByteBuffer.allocate(src.remaining()); - } - copySlice(src, x, ir, dst); - if (ir+1 == src.limit()) { - // last char of the buffer - - // consume src till that char and let next iteration work on it - x = ir; - break; - } - if (in != ir + 1) { - x = ir+1; // generally in, but if allowInconsistent==true and \r is not followed by \n, then - // cases like "one \r two \r\n three" shall be processed correctly (second pair would be ignored if x==in) - lookupStart = ir+1; - } else { - x = in; - lookupStart = x+1; // skip \n for next lookup - } - } - src.position(x); // mark we've consumed up to x - return dst == null ? src : (ByteBuffer) dst.flip(); - } - - private ByteBuffer nix2win(ByteBuffer src) { - int x = src.position(); - ByteBuffer dst = null; - while (x < src.limit()) { - int in = indexOf('\n', src, x); - int ir = indexOf('\r', src, x, in == -1 ? src.limit() : in); - if (in == -1) { - if (ir == -1 || allowInconsistent) { - break; - } else { - fail(src, ir); - } - } else if (ir != -1 && !allowInconsistent) { - fail(src, ir); - } - - // x <= in < src.limit - // allowInconsistent && x <= ir < in || ir == -1 - if (dst == null) { - // buffer full of \n grows as much as twice in size - dst = ByteBuffer.allocate(src.remaining() * 2); - } - copySlice(src, x, in, dst); - if (ir == -1 || ir+1 != in) { - dst.put((byte) '\r'); - } // otherwise (ir!=-1 && ir+1==in) we found \r\n pair, don't convert to \r\r\n - // we may copy \n at src[in] on the next iteration, but would need extra lookupIndex variable then. - dst.put((byte) '\n'); - x = in+1; - } - src.position(x); - return dst == null ? src : (ByteBuffer) dst.flip(); - } - - - private void fail(ByteBuffer b, int pos) { - throw new RuntimeException(String.format("Inconsistent newline characters in the stream (char 0x%x, local index:%d)", b.get(pos), pos)); - } - - private static int indexOf(char ch, ByteBuffer b, int from) { - return indexOf(ch, b, from, b.limit()); - } - - // looks up in buf[from..to) - private static int indexOf(char ch, ByteBuffer b, int from, int to) { - for (int i = from; i < to; i++) { - byte c = b.get(i); - if (ch == c) { - return i; - } - } - return -1; - } - - public static class Factory implements Filter.Factory { - private boolean failIfInconsistent = true; - private Path.Matcher lfMatcher; - private Path.Matcher crlfMatcher; - private Path.Matcher binMatcher; - private Path.Matcher nativeMatcher; - private String nativeRepoFormat; - private String nativeOSFormat; - - public void initialize(HgRepository hgRepo, ConfigFile cfg) { - failIfInconsistent = cfg.getBoolean("eol", "only-consistent", true); - File cfgFile = new File(new HgInternals(hgRepo).getRepositoryDir().getParentFile(), ".hgeol"); - if (!cfgFile.canRead()) { - return; - } - // XXX if .hgeol is not checked out, we may get it from repository -// HgDataFile cfgFileNode = hgRepo.getFileNode(".hgeol"); -// if (!cfgFileNode.exists()) { -// return; -// } - // XXX perhaps, add HgDataFile.hasWorkingCopy and workingCopyContent()? - ConfigFile hgeol = new ConfigFile(); - hgeol.addLocation(cfgFile); - nativeRepoFormat = hgeol.getSection("repository").get("native"); - if (nativeRepoFormat == null) { - nativeRepoFormat = "LF"; - } - final String os = System.getProperty("os.name"); // XXX need centralized set of properties - nativeOSFormat = os.indexOf("Windows") != -1 ? "CRLF" : "LF"; - // I assume pattern ordering in .hgeol is not important - ArrayList lfPatterns = new ArrayList(); - ArrayList crlfPatterns = new ArrayList(); - ArrayList nativePatterns = new ArrayList(); - ArrayList binPatterns = new ArrayList(); - for (Map.Entry e : hgeol.getSection("patterns").entrySet()) { - if ("CRLF".equals(e.getValue())) { - crlfPatterns.add(e.getKey()); - } else if ("LF".equals(e.getValue())) { - lfPatterns.add(e.getKey()); - } else if ("native".equals(e.getValue())) { - nativePatterns.add(e.getKey()); - } else if ("BIN".equals(e.getValue())) { - binPatterns.add(e.getKey()); - } else { - System.out.printf("Can't recognize .hgeol entry: %s for %s", e.getValue(), e.getKey()); // FIXME log warning - } - } - if (!crlfPatterns.isEmpty()) { - crlfMatcher = new PathGlobMatcher(crlfPatterns.toArray(new String[crlfPatterns.size()])); - } - if (!lfPatterns.isEmpty()) { - lfMatcher = new PathGlobMatcher(lfPatterns.toArray(new String[lfPatterns.size()])); - } - if (!binPatterns.isEmpty()) { - binMatcher = new PathGlobMatcher(binPatterns.toArray(new String[binPatterns.size()])); - } - if (!nativePatterns.isEmpty()) { - nativeMatcher = new PathGlobMatcher(nativePatterns.toArray(new String[nativePatterns.size()])); - } - } - - public Filter create(Path path, Options opts) { - if (binMatcher == null && crlfMatcher == null && lfMatcher == null && nativeMatcher == null) { - // not initialized - perhaps, no .hgeol found - return null; - } - if (binMatcher != null && binMatcher.accept(path)) { - return null; - } - if (crlfMatcher != null && crlfMatcher.accept(path)) { - return new NewlineFilter(failIfInconsistent, 1); - } else if (lfMatcher != null && lfMatcher.accept(path)) { - return new NewlineFilter(failIfInconsistent, 0); - } else if (nativeMatcher != null && nativeMatcher.accept(path)) { - if (nativeOSFormat.equals(nativeRepoFormat)) { - return null; - } - if (opts.getDirection() == FromRepo) { - int transform = "CRLF".equals(nativeOSFormat) ? 1 : 0; - return new NewlineFilter(failIfInconsistent, transform); - } else if (opts.getDirection() == ToRepo) { - int transform = "CRLF".equals(nativeOSFormat) ? 0 : 1; - return new NewlineFilter(failIfInconsistent, transform); - } - return null; - } - return null; - } - } - - public static void main(String[] args) throws Exception { - FileInputStream fis = new FileInputStream(new File("/temp/design.lf.txt")); - FileOutputStream fos = new FileOutputStream(new File("/temp/design.newline.out")); - ByteBuffer b = ByteBuffer.allocate(12); - NewlineFilter nlFilter = new NewlineFilter(true, 1); - while (fis.getChannel().read(b) != -1) { - b.flip(); // get ready to be read - ByteBuffer f = nlFilter.filter(b); - fos.getChannel().write(f); // XXX in fact, f may not be fully consumed - if (b.hasRemaining()) { - b.compact(); - } else { - b.clear(); - } - } - fis.close(); - fos.flush(); - fos.close(); - } - -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/PathGlobMatcher.java --- a/src/org/tmatesoft/hg/internal/PathGlobMatcher.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,94 +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 java.util.regex.PatternSyntaxException; - -import org.tmatesoft.hg.util.Path; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class PathGlobMatcher implements Path.Matcher { - - private final PathRegexpMatcher delegate; - - /** - * - * @param globPatterns - * @throws NullPointerException if argument is null - * @throws IllegalArgumentException if any of the patterns is not valid - */ - public PathGlobMatcher(String... globPatterns) { - String[] regexp = new String[globPatterns.length]; //deliberately let fail with NPE - int i = 0; - for (String s : globPatterns) { - regexp[i] = glob2regexp(s); - } - try { - delegate = new PathRegexpMatcher(regexp); - } catch (PatternSyntaxException ex) { - ex.printStackTrace(); - throw new IllegalArgumentException(ex); - } - } - - - // 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) { - int end = glob.length() - 1; - boolean needLineEndMatch = glob.charAt(end) != '*'; - while (end > 0 && glob.charAt(end) == '*') end--; // remove trailing * that are useless for Pattern.find() - StringBuilder sb = new StringBuilder(end*2); - if (glob.charAt(0) != '*') { - sb.append('^'); - } - for (int i = 0; i <= end; i++) { - char ch = glob.charAt(i); - if (ch == '*') { - if (glob.charAt(i+1) == '*') { // i < end because we've stripped any trailing * earlier - // any char, including path segment separator - sb.append(".*?"); - i++; - } else { - // just path segments - sb.append("[^/]*?"); - } - continue; - } else if (ch == '?') { - sb.append("[^/]"); - continue; - } else if (ch == '.' || ch == '\\') { - sb.append('\\'); - } - sb.append(ch); - } - if (needLineEndMatch) { - sb.append('$'); - } - return sb.toString(); - } - - public boolean accept(Path path) { - return delegate.accept(path); - } - -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/PathRegexpMatcher.java --- a/src/org/tmatesoft/hg/internal/PathRegexpMatcher.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +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 java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - -import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.Path.Matcher; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class PathRegexpMatcher implements Matcher { - private Pattern[] patterns; - - // disjunction, matches if any pattern found - // uses pattern.find(), not pattern.matches() - public PathRegexpMatcher(Pattern... p) { - if (p == null) { - throw new IllegalArgumentException(); - } - patterns = p; - } - - public PathRegexpMatcher(String... p) throws PatternSyntaxException { - this(compile(p)); - } - - private static Pattern[] compile(String[] p) throws PatternSyntaxException { - // deliberately do no check for null, let it fail - Pattern[] rv = new Pattern[p.length]; - int i = 0; - for (String s : p) { - rv[i++] = Pattern.compile(s); - } - return rv; - } - - public boolean accept(Path path) { - for (Pattern p : patterns) { - if (p.matcher(path).find()) { - return true; - } - } - return false; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/Pool.java --- a/src/org/tmatesoft/hg/internal/Pool.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +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 java.util.HashMap; - -/** - * Instance pooling. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class Pool { - private final HashMap unify = new HashMap(); - - public T unify(T t) { - T rv = unify.get(t); - if (rv == null) { - // first time we see a new value - unify.put(t, t); - rv = t; - } - return rv; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(Pool.class.getSimpleName()); - sb.append('<'); - if (!unify.isEmpty()) { - sb.append(unify.keySet().iterator().next().getClass().getName()); - } - sb.append('>'); - sb.append(':'); - sb.append(unify.size()); - return sb.toString(); - } -} \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/RelativePathRewrite.java --- a/src/org/tmatesoft/hg/internal/RelativePathRewrite.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,49 +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 java.io.File; - -import org.tmatesoft.hg.util.PathRewrite; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class RelativePathRewrite implements PathRewrite { - - private final String rootPath; - - public RelativePathRewrite(File root) { - this(root.getPath()); - } - - public RelativePathRewrite(String rootPath) { - this.rootPath = rootPath; - } - - public String rewrite(String path) { - if (path != null && path.startsWith(rootPath)) { - if (path.length() == rootPath.length()) { - return ""; - } - return path.substring(rootPath.length() + 1); - } - return path; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/RepositoryComparator.java --- a/src/org/tmatesoft/hg/internal/RepositoryComparator.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,570 +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 static org.tmatesoft.hg.core.Nodeid.NULL; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.ListIterator; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; - -import org.tmatesoft.hg.core.HgBadStateException; -import org.tmatesoft.hg.core.HgException; -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.repo.HgChangelog; -import org.tmatesoft.hg.repo.HgRemoteRepository; -import org.tmatesoft.hg.repo.HgRemoteRepository.Range; -import org.tmatesoft.hg.repo.HgRemoteRepository.RemoteBranch; -import org.tmatesoft.hg.util.CancelSupport; -import org.tmatesoft.hg.util.CancelledException; -import org.tmatesoft.hg.util.ProgressSupport; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class RepositoryComparator { - - private final boolean debug = Boolean.parseBoolean(System.getProperty("hg4j.remote.debug")); - private final HgChangelog.ParentWalker localRepo; - private final HgRemoteRepository remoteRepo; - private List common; - - public RepositoryComparator(HgChangelog.ParentWalker pwLocal, HgRemoteRepository hgRemote) { - localRepo = pwLocal; - remoteRepo = hgRemote; - } - - public RepositoryComparator compare(Object context) throws HgException, CancelledException { - ProgressSupport progressSupport = ProgressSupport.Factory.get(context); - CancelSupport cancelSupport = CancelSupport.Factory.get(context); - cancelSupport.checkCancelled(); - progressSupport.start(10); - common = Collections.unmodifiableList(findCommonWithRemote()); - // sanity check - for (Nodeid n : common) { - if (!localRepo.knownNode(n)) { - throw new HgBadStateException("Unknown node reported as common:" + n); - } - } - progressSupport.done(); - return this; - } - - public List getCommon() { - if (common == null) { - throw new HgBadStateException("Call #compare(Object) first"); - } - return common; - } - - /** - * @return revisions that are children of common entries, i.e. revisions that are present on the local server and not on remote. - */ - public List getLocalOnlyRevisions() { - return localRepo.childrenOf(getCommon()); - } - - /** - * Similar to @link {@link #getLocalOnlyRevisions()}, use this one if you need access to changelog entry content, not - * only its revision number. - * @param inspector delegate to analyze changesets, shall not be null - */ - public void visitLocalOnlyRevisions(HgChangelog.Inspector inspector) { - if (inspector == null) { - throw new IllegalArgumentException(); - } - // one can use localRepo.childrenOf(getCommon()) and then iterate over nodeids, but there seems to be - // another approach to get all changes after common: - // find index of earliest revision, and report all that were later - final HgChangelog changelog = localRepo.getRepo().getChangelog(); - int earliestRevision = Integer.MAX_VALUE; - List commonKnown = getCommon(); - for (Nodeid n : commonKnown) { - if (!localRepo.hasChildren(n)) { - // there might be (old) nodes, known both locally and remotely, with no children - // hence, we don't need to consider their local revision number - continue; - } - int lr = changelog.getLocalRevision(n); - if (lr < earliestRevision) { - earliestRevision = lr; - } - } - if (earliestRevision == Integer.MAX_VALUE) { - // either there are no common nodes (known locally and at remote) - // or no local children found (local is up to date). In former case, perhaps I shall bit return silently, - // but check for possible wrong repo comparison (hs says 'repository is unrelated' if I try to - // check in/out for a repo that has no common nodes. - 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())); - } - changelog.range(earliestRevision+1, changelog.getLastRevision(), inspector); - } - - private List findCommonWithRemote() throws HgException { - List remoteHeads = remoteRepo.heads(); - LinkedList resultCommon = new LinkedList(); // these remotes are known in local - LinkedList toQuery = new LinkedList(); // these need further queries to find common - for (Nodeid rh : remoteHeads) { - if (localRepo.knownNode(rh)) { - resultCommon.add(rh); - } else { - toQuery.add(rh); - } - } - if (toQuery.isEmpty()) { - return resultCommon; - } - LinkedList checkUp2Head = new LinkedList(); // branch.root and branch.head are of interest only. - // these are branches with unknown head but known root, which might not be the last common known, - // i.e. there might be children changeset that are also available at remote, [..?..common-head..remote-head] - need to - // scroll up to common head. - while (!toQuery.isEmpty()) { - List remoteBranches = remoteRepo.branches(toQuery); //head, root, first parent, second parent - toQuery.clear(); - while(!remoteBranches.isEmpty()) { - RemoteBranch rb = remoteBranches.remove(0); - // I assume branches remote call gives branches with head equal to what I pass there, i.e. - // that I don't need to check whether rb.head is unknown. - if (localRepo.knownNode(rb.root)) { - // we known branch start, common head is somewhere in its descendants line - checkUp2Head.add(rb); - } else { - // dig deeper in the history, if necessary - if (!NULL.equals(rb.p1) && !localRepo.knownNode(rb.p1)) { - toQuery.add(rb.p1); - } - if (!NULL.equals(rb.p2) && !localRepo.knownNode(rb.p2)) { - toQuery.add(rb.p2); - } - } - } - } - // can't check nodes between checkUp2Head element and local heads, remote might have distinct descendants sequence - for (RemoteBranch rb : checkUp2Head) { - // rb.root is known locally - List remoteRevisions = remoteRepo.between(rb.head, rb.root); - if (remoteRevisions.isEmpty()) { - // head is immediate child - resultCommon.add(rb.root); - } else { - // between gives result from head to root, I'd like to go in reverse direction - Collections.reverse(remoteRevisions); - Nodeid root = rb.root; - while(!remoteRevisions.isEmpty()) { - Nodeid n = remoteRevisions.remove(0); - if (localRepo.knownNode(n)) { - if (remoteRevisions.isEmpty()) { - // this is the last known node before an unknown - resultCommon.add(n); - break; - } - if (remoteRevisions.size() == 1) { - // there's only one left between known n and unknown head - // this check is to save extra between query, not really essential - Nodeid last = remoteRevisions.remove(0); - resultCommon.add(localRepo.knownNode(last) ? last : n); - break; - } - // might get handy for next between query, to narrow search down - root = n; - } else { - remoteRevisions = remoteRepo.between(n, root); - Collections.reverse(remoteRevisions); - if (remoteRevisions.isEmpty()) { - resultCommon.add(root); - } - } - } - } - } - // TODO ensure unique elements in the list - return resultCommon; - } - - // somewhat similar to Outgoing.findCommonWithRemote() - public List calculateMissingBranches() throws HgException { - List remoteHeads = remoteRepo.heads(); - LinkedList common = new LinkedList(); // these remotes are known in local - LinkedList toQuery = new LinkedList(); // these need further queries to find common - for (Nodeid rh : remoteHeads) { - if (localRepo.knownNode(rh)) { - common.add(rh); - } else { - toQuery.add(rh); - } - } - if (toQuery.isEmpty()) { - return Collections.emptyList(); // no incoming changes - } - LinkedList branches2load = new LinkedList(); // return value - // detailed comments are in Outgoing.findCommonWithRemote - LinkedList checkUp2Head = new LinkedList(); - // records relation between branch head and its parent branch, if any - HashMap head2chain = new HashMap(); - while (!toQuery.isEmpty()) { - List remoteBranches = remoteRepo.branches(toQuery); //head, root, first parent, second parent - toQuery.clear(); - while(!remoteBranches.isEmpty()) { - RemoteBranch rb = remoteBranches.remove(0); - BranchChain chainElement = head2chain.get(rb.head); - if (chainElement == null) { - chainElement = new BranchChain(rb.head); - // record this unknown branch to download later - branches2load.add(chainElement); - // the only chance we'll need chainElement in the head2chain is when we know this branch's root - head2chain.put(rb.head, chainElement); - } - if (localRepo.knownNode(rb.root)) { - // we known branch start, common head is somewhere in its descendants line - checkUp2Head.add(rb); - } else { - chainElement.branchRoot = rb.root; - // dig deeper in the history, if necessary - boolean hasP1 = !NULL.equals(rb.p1), hasP2 = !NULL.equals(rb.p2); - if (hasP1 && !localRepo.knownNode(rb.p1)) { - toQuery.add(rb.p1); - // we might have seen parent node already, and recorded it as a branch chain - // we shall reuse existing BC to get it completely initializer (head2chain map - // on second put with the same key would leave first BC uninitialized. - - // It seems there's no reason to be affraid (XXX although shall double-check) - // that BC's chain would get corrupt (its p1 and p2 fields assigned twice with different values) - // as parents are always the same (and likely, BC that is common would be the last unknown) - BranchChain bc = head2chain.get(rb.p1); - if (bc == null) { - head2chain.put(rb.p1, bc = new BranchChain(rb.p1)); - } - chainElement.p1 = bc; - } - if (hasP2 && !localRepo.knownNode(rb.p2)) { - toQuery.add(rb.p2); - BranchChain bc = head2chain.get(rb.p2); - if (bc == null) { - head2chain.put(rb.p2, bc = new BranchChain(rb.p2)); - } - chainElement.p2 = bc; - } - if (!hasP1 && !hasP2) { - // special case, when we do incoming against blank repository, chainElement.branchRoot - // is first unknown element (revision 0). We need to add another fake BranchChain - // to fill the promise that terminal BranchChain has branchRoot that is known both locally and remotely - BranchChain fake = new BranchChain(NULL); - fake.branchRoot = NULL; - chainElement.p1 = chainElement.p2 = fake; - } - } - } - } - for (RemoteBranch rb : checkUp2Head) { - Nodeid h = rb.head; - Nodeid r = rb.root; - int watchdog = 1000; - assert head2chain.containsKey(h); - BranchChain bc = head2chain.get(h); - assert bc != null : h.toString(); - // if we know branch root locally, there could be no parent branch chain elements. - assert bc.p1 == null; - assert bc.p2 == null; - do { - List between = remoteRepo.between(h, r); - if (between.isEmpty()) { - bc.branchRoot = r; - break; - } else { - Collections.reverse(between); - for (Nodeid n : between) { - if (localRepo.knownNode(n)) { - r = n; - } else { - h = n; - break; - } - } - Nodeid lastInBetween = between.get(between.size() - 1); - if (r.equals(lastInBetween)) { - bc.branchRoot = r; - break; - } else if (h.equals(lastInBetween)) { // the only chance for current head pointer to point to the sequence tail - // is when r is second from the between list end (iow, head,1,[2],4,8...,root) - bc.branchRoot = r; - break; - } - } - } while(--watchdog > 0); - if (watchdog == 0) { - throw new HgBadStateException(String.format("Can't narrow down branch [%s, %s]", rb.head.shortNotation(), rb.root.shortNotation())); - } - } - if (debug) { - System.out.println("calculateMissingBranches:"); - for (BranchChain bc : branches2load) { - bc.dump(); - } - } - return branches2load; - } - - // root and head (and all between) are unknown for each chain element but last (terminal), which has known root (revision - // known to be locally and at remote server - // alternative would be to keep only unknown elements (so that promise of calculateMissingBranches would be 100% true), but that - // seems to complicate the method, while being useful only for the case when we ask incoming for an empty repository (i.e. - // where branch chain return all nodes, -1..tip. - public static final class BranchChain { - // when we construct a chain, we know head which is missing locally, hence init it right away. - // as for root (branch unknown start), we might happen to have one locally, and need further digging to find out right branch start - public final Nodeid branchHead; - public Nodeid branchRoot; - // either of these can be null, or both. - // although RemoteBranch has either both parents null, or both non-null, when we construct a chain - // we might encounter that we locally know one of branch's parent, hence in the chain corresponding field will be blank. - public BranchChain p1; - public BranchChain p2; - - public BranchChain(Nodeid head) { - assert head != null; - branchHead = head; - } - public boolean isTerminal() { - return p1 == null && p2 == null; // either can be null, see comment above. Terminal is only when no way to descent - } - - // true when this BranchChain is a branch that spans up to very start of the repository - // Thus, the only common revision is NULL, recorded in a fake BranchChain object shared between p1 and p2 - /*package-local*/ boolean isRepoStart() { - return p1 == p2 && p1 != null && p1.branchHead == p1.branchRoot && NULL.equals(p1.branchHead); - } - - @Override - public String toString() { - return String.format("BranchChain [%s, %s]", branchRoot, branchHead); - } - - void dump() { - System.out.println(toString()); - internalDump(" "); - } - - private void internalDump(String prefix) { - if (p1 != null) { - System.out.println(prefix + p1.toString()); - } else if (p2 != null) { - System.out.println(prefix + "NONE?!"); - } - if (p2 != null) { - System.out.println(prefix + p2.toString()); - } else if (p1 != null) { - System.out.println(prefix + "NONE?!"); - } - prefix += " "; - if (p1 != null) { - p1.internalDump(prefix); - } - if (p2 != null) { - p2.internalDump(prefix); - } - } - } - - /** - * @return list of nodeids from branchRoot to branchHead, inclusive. IOW, first element of the list is always root of the branch - */ - public List completeBranch(final Nodeid branchRoot, final Nodeid branchHead) throws HgException { - class DataEntry { - public final Nodeid queryHead; - public final int headIndex; - public List entries; - - public DataEntry(Nodeid head, int index, List data) { - queryHead = head; - headIndex = index; - entries = data; - } - }; - - List initial = remoteRepo.between(branchHead, branchRoot); - Nodeid[] result = new Nodeid[1 + (1 << initial.size())]; - result[0] = branchHead; - int rootIndex = -1; // index in the result, where to place branche's root. - if (initial.isEmpty()) { - rootIndex = 1; - } else if (initial.size() == 1) { - rootIndex = 2; - } - LinkedList datas = new LinkedList(); - // DataEntry in datas has entries list filled with 'between' data, whereas - // DataEntry in toQuery keeps only nodeid and its index, with entries to be initialized before - // moving to datas. - LinkedList toQuery = new LinkedList(); - // - datas.add(new DataEntry(branchHead, 0, initial)); - int totalQueries = 1; - HashSet queried = new HashSet(); - while(!datas.isEmpty()) { - // keep record of those planned to be queried next time we call between() - // although may keep these in queried, if really don't want separate collection - HashSet scheduled = new HashSet(); - do { - DataEntry de = datas.removeFirst(); - // populate result with discovered elements between de.qiueryRoot and branch's head - for (int i = 1, j = 0; j < de.entries.size(); i = i << 1, j++) { - int idx = de.headIndex + i; - result[idx] = de.entries.get(j); - } - // form next query entries from new unknown elements - if (de.entries.size() > 1) { - /* when entries has only one element, it means de.queryRoot was at head-2 position, and thus - * no new information can be obtained. E.g. when it's 2, it might be case of [0..4] query with - * [1,2] result, and we need one more query to get element 3. - */ - for (int i =1, j = 0; j < de.entries.size(); i = i<<1, j++) { - int idx = de.headIndex + i; - Nodeid x = de.entries.get(j); - if (!queried.contains(x) && !scheduled.contains(x) && (rootIndex == -1 || rootIndex - de.headIndex > 1)) { - /*queries for elements right before head is senseless, but unless we know head's index, do it anyway*/ - toQuery.add(new DataEntry(x, idx, null)); - scheduled.add(x); - } - } - } - } while (!datas.isEmpty()); - if (!toQuery.isEmpty()) { - totalQueries++; - } - // for each query, create an between request range, keep record Range->DataEntry to know range's start index - LinkedList betweenBatch = new LinkedList(); - HashMap rangeToEntry = new HashMap(); - for (DataEntry de : toQuery) { - queried.add(de.queryHead); - HgRemoteRepository.Range r = new HgRemoteRepository.Range(branchRoot, de.queryHead); - betweenBatch.add(r); - rangeToEntry.put(r, de); - } - if (!betweenBatch.isEmpty()) { - Map> between = remoteRepo.between(betweenBatch); - for (Entry> e : between.entrySet()) { - DataEntry de = rangeToEntry.get(e.getKey()); - assert de != null; - de.entries = e.getValue(); - if (rootIndex == -1 && de.entries.size() == 1) { - // returned sequence of length 1 means we used element from [head-2] as root - int numberOfElementsExcludingRootAndHead = de.headIndex + 1; - rootIndex = numberOfElementsExcludingRootAndHead + 1; - if (debug) { - System.out.printf("On query %d found out exact number of missing elements: %d\n", totalQueries, numberOfElementsExcludingRootAndHead); - } - } - datas.add(de); // queue up to record result and construct further requests - } - betweenBatch.clear(); - rangeToEntry.clear(); - } - toQuery.clear(); - } - if (rootIndex == -1) { - throw new HgBadStateException("Shall not happen, provided between output is correct"); // FIXME - } - result[rootIndex] = branchRoot; - boolean resultOk = true; - LinkedList fromRootToHead = new LinkedList(); - 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); - resultOk = false; - } - fromRootToHead.addFirst(n); // reverse order - } - if (debug) { - System.out.println("Total queries:" + totalQueries); - } - if (!resultOk) { - throw new HgBadStateException("See console for details"); // FIXME - } - return fromRootToHead; - } - - /** - * returns in order from branch root to head - * for a non-empty BranchChain, shall return modifiable list - */ - public List visitBranches(BranchChain bc) throws HgException { - if (bc == null) { - return Collections.emptyList(); - } - List mine = completeBranch(bc.branchRoot, bc.branchHead); - if (bc.isTerminal() || bc.isRepoStart()) { - return mine; - } - List parentBranch1 = visitBranches(bc.p1); - List parentBranch2 = visitBranches(bc.p2); - // merge - LinkedList merged = new LinkedList(); - ListIterator i1 = parentBranch1.listIterator(), i2 = parentBranch2.listIterator(); - while (i1.hasNext() && i2.hasNext()) { - Nodeid n1 = i1.next(); - Nodeid n2 = i2.next(); - if (n1.equals(n2)) { - merged.addLast(n1); - } else { - // first different => add both, and continue adding both tails sequentially - merged.add(n2); - merged.add(n1); - break; - } - } - // copy rest of second parent branch - while (i2.hasNext()) { - merged.add(i2.next()); - } - // copy rest of first parent branch - while (i1.hasNext()) { - merged.add(i1.next()); - } - // - ArrayList rv = new ArrayList(mine.size() + merged.size()); - rv.addAll(merged); - rv.addAll(mine); - return rv; - } - - public void collectKnownRoots(BranchChain bc, Set result) { - if (bc == null) { - return; - } - if (bc.isTerminal()) { - result.add(bc.branchRoot); - return; - } - if (bc.isRepoStart()) { - return; - } - collectKnownRoots(bc.p1, result); - collectKnownRoots(bc.p2, result); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/RequiresFile.java --- a/src/org/tmatesoft/hg/internal/RequiresFile.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,64 +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 java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStreamReader; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class RequiresFile { - public static final int STORE = 1; - public static final int FNCACHE = 2; - public static final int DOTENCODE = 4; - - public RequiresFile() { - } - - public void parse(Internals repoImpl, File requiresFile) { - if (!requiresFile.exists()) { - return; - } - try { - boolean revlogv1 = false; - boolean store = false; - boolean fncache = false; - boolean dotencode = false; - BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(requiresFile))); - String line; - while ((line = br.readLine()) != null) { - revlogv1 |= "revlogv1".equals(line); - store |= "store".equals(line); - fncache |= "fncache".equals(line); - dotencode |= "dotencode".equals(line); - } - int flags = 0; - flags += store ? STORE : 0; - flags += fncache ? FNCACHE : 0; - flags += dotencode ? DOTENCODE : 0; - repoImpl.setStorageConfig(revlogv1 ? 1 : 0, flags); - } catch (IOException ex) { - ex.printStackTrace(); // FIXME log - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/RevlogDump.java --- a/src/org/tmatesoft/hg/internal/RevlogDump.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2010-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 java.io.BufferedInputStream; -import java.io.DataInput; -import java.io.DataInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.math.BigInteger; -import java.util.zip.Inflater; - -/** - * Utility to test/debug/troubleshoot - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class RevlogDump { - - /** - * Takes 3 command line arguments - - * repository path, - * path to index file (i.e. store/data/hello.c.i) in the repository (relative) - * and "dumpData" whether to print actual content or just revlog headers - */ - public static void main(String[] args) throws Exception { - String repo = "/temp/hg/hello/.hg/"; - String filename = "store/00changelog.i"; -// String filename = "store/data/hello.c.i"; -// String filename = "store/data/docs/readme.i"; - boolean dumpData = true; - if (args.length > 1) { - repo = args[0]; - filename = args[1]; - dumpData = args.length > 2 ? "dumpData".equals(args[2]) : false; - } - // - DataInputStream dis = new DataInputStream(new BufferedInputStream(new FileInputStream(new File(repo + filename)))); - DataInput di = dis; - dis.mark(10); - int versionField = di.readInt(); - dis.reset(); - final int INLINEDATA = 1 << 16; - - boolean inlineData = (versionField & INLINEDATA) != 0; - System.out.printf("%#8x, inline: %b\n", versionField, inlineData); - System.out.println("Index Offset Flags Packed Actual Base Rev Link Rev Parent1 Parent2 nodeid"); - int entryCount = 0; - while (dis.available() > 0) { - long l = di.readLong(); - long offset = l >>> 16; - int flags = (int) (l & 0X0FFFF); - int compressedLen = di.readInt(); - int actualLen = di.readInt(); - int baseRevision = di.readInt(); - int linkRevision = di.readInt(); - int parent1Revision = di.readInt(); - int parent2Revision = di.readInt(); - byte[] buf = new byte[32]; - di.readFully(buf, 12, 20); - dis.skipBytes(12); - // CAN'T USE skip() here without extra precautions. E.g. I ran into situation when - // buffer was 8192 and BufferedInputStream was at position 8182 before attempt to skip(12). - // BIS silently skips available bytes and leaves me two extra bytes that ruin the rest of the code. - System.out.printf("%4d:%14d %6X %10d %10d %10d %10d %8d %8d %040x\n", entryCount, offset, flags, compressedLen, actualLen, baseRevision, linkRevision, parent1Revision, parent2Revision, new BigInteger(buf)); - if (inlineData) { - String resultString; - byte[] data = new byte[compressedLen]; - di.readFully(data); - if (data[0] == 0x78 /* 'x' */) { - Inflater zlib = new Inflater(); - zlib.setInput(data, 0, compressedLen); - byte[] result = new byte[actualLen*2]; - int resultLen = zlib.inflate(result); - zlib.end(); - resultString = new String(result, 0, resultLen, "UTF-8"); - } else if (data[0] == 0x75 /* 'u' */) { - resultString = new String(data, 1, data.length - 1, "UTF-8"); - } else { - resultString = new String(data); - } - if (dumpData) { - System.out.println(resultString); - } - } - entryCount++; - } - dis.close(); - // - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/RevlogStream.java --- a/src/org/tmatesoft/hg/internal/RevlogStream.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,460 +0,0 @@ -/* - * Copyright (c) 2010-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 static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; -import static org.tmatesoft.hg.repo.HgRepository.TIP; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; - -import org.tmatesoft.hg.core.HgBadStateException; -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.repo.HgRepository; - - -/** - * ? Single RevlogStream per file per repository with accessor to record access session (e.g. with back/forward operations), - * or numerous RevlogStream with separate representation of the underlying data (cached, lazy ChunkStream)? - * - * @see http://mercurial.selenic.com/wiki/Revlog - * @see http://mercurial.selenic.com/wiki/RevlogNG - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class RevlogStream { - - /* - * makes sense for index with inline data only - actual offset of the record in the .i file (record entry + revision * record size)) - * - * long[] in fact (there are 8-bytes field in the revlog) - * However, (a) DataAccess currently doesn't operate with long seek/length - * and, of greater significance, (b) files with inlined data are designated for smaller files, - * guess, about 130 Kb, and offset there won't ever break int capacity - */ - private int[] indexRecordOffset; - private int[] baseRevisions; - private boolean inline = false; - private final File indexFile; - private final DataAccessProvider dataAccess; - - // if we need anything else from HgRepo, might replace DAP parameter with HgRepo and query it for DAP. - public RevlogStream(DataAccessProvider dap, File indexFile) { - this.dataAccess = dap; - this.indexFile = indexFile; - } - - /*package*/ DataAccess getIndexStream() { - 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); - } - - public int revisionCount() { - initOutline(); - return baseRevisions.length; - } - - public int dataLength(int revision) { - // XXX in fact, use of iterate() instead of this implementation may be quite reasonable. - // - final int indexSize = revisionCount(); - DataAccess daIndex = getIndexStream(); // XXX may supply a hint that I'll need really few bytes of data (although at some offset) - if (revision == TIP) { - revision = indexSize - 1; - } - try { - int recordOffset = getIndexOffsetInt(revision); - daIndex.seek(recordOffset + 12); // 6+2+4 - int actualLen = daIndex.readInt(); - return actualLen; - } catch (IOException ex) { - ex.printStackTrace(); // log error. FIXME better handling - throw new IllegalStateException(ex); - } finally { - daIndex.done(); - } - } - - public byte[] nodeid(int revision) { - final int indexSize = revisionCount(); - if (revision == TIP) { - revision = indexSize - 1; - } - if (revision < 0 || revision >= indexSize) { - throw new IllegalArgumentException(Integer.toString(revision)); - } - DataAccess daIndex = getIndexStream(); - try { - int recordOffset = getIndexOffsetInt(revision); - daIndex.seek(recordOffset + 32); - byte[] rv = new byte[20]; - daIndex.readBytes(rv, 0, 20); - return rv; - } catch (IOException ex) { - ex.printStackTrace(); - throw new IllegalStateException(); - } finally { - daIndex.done(); - } - } - - public int linkRevision(int revision) { - final int last = revisionCount() - 1; - if (revision == TIP) { - revision = last; - } - if (revision < 0 || revision > last) { - throw new IllegalArgumentException(Integer.toString(revision)); - } - DataAccess daIndex = getIndexStream(); - try { - int recordOffset = getIndexOffsetInt(revision); - daIndex.seek(recordOffset + 20); - int linkRev = daIndex.readInt(); - return linkRev; - } catch (IOException ex) { - ex.printStackTrace(); - throw new IllegalStateException(); - } finally { - daIndex.done(); - } - } - - // Perhaps, RevlogStream should be limited to use of plain int revisions for access, - // while Nodeids should be kept on the level up, in Revlog. Guess, Revlog better keep - // map of nodeids, and once this comes true, we may get rid of this method. - // Unlike its counterpart, {@link Revlog#getLocalRevisionNumber()}, doesn't fail with exception if node not found, - /** - * @return integer in [0..revisionCount()) or {@link HgRepository#BAD_REVISION} if not found - */ - public int findLocalRevisionNumber(Nodeid nodeid) { - // XXX this one may be implemented with iterate() once there's mechanism to stop iterations - final int indexSize = revisionCount(); - DataAccess daIndex = getIndexStream(); - try { - byte[] nodeidBuf = new byte[20]; - for (int i = 0; i < indexSize; i++) { - daIndex.skip(8); - int compressedLen = daIndex.readInt(); - daIndex.skip(20); - daIndex.readBytes(nodeidBuf, 0, 20); - if (nodeid.equalsTo(nodeidBuf)) { - return i; - } - daIndex.skip(inline ? 12 + compressedLen : 12); - } - } catch (IOException ex) { - ex.printStackTrace(); // log error. FIXME better handling - throw new IllegalStateException(ex); - } finally { - daIndex.done(); - } - return BAD_REVISION; - } - - - private final int REVLOGV1_RECORD_SIZE = 64; - - // 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) { - initOutline(); - final int indexSize = revisionCount(); - if (indexSize == 0) { - return; - } - if (end == TIP) { - end = indexSize - 1; - } - if (start == TIP) { - start = indexSize - 1; - } - if (start < 0 || start >= indexSize) { - throw new IllegalArgumentException(String.format("Bad left range boundary %d in [0..%d]", start, indexSize-1)); - } - if (end < start || end >= indexSize) { - throw new IllegalArgumentException(String.format("Bad right range boundary %d in [0..%d]", end, indexSize-1)); - } - // XXX may cache [start .. end] from index with a single read (pre-read) - - DataAccess daIndex = null, daData = null; - daIndex = getIndexStream(); - if (needData && !inline) { - daData = getDataStream(); - } - try { - byte[] nodeidBuf = new byte[20]; - DataAccess lastUserData = null; - int i; - boolean extraReadsToBaseRev = false; - if (needData && getBaseRevision(start) < start) { - i = getBaseRevision(start); - extraReadsToBaseRev = true; - } else { - i = start; - } - - daIndex.seek(getIndexOffsetInt(i)); - for (; i <= end; i++ ) { - if (inline && needData) { - // inspector reading data (though FilterDataAccess) may have affected index position - daIndex.seek(getIndexOffsetInt(i)); - } - long l = daIndex.readLong(); // 0 - long offset = i == 0 ? 0 : (l >>> 16); - @SuppressWarnings("unused") - int flags = (int) (l & 0X0FFFF); - int compressedLen = daIndex.readInt(); // +8 - int actualLen = daIndex.readInt(); // +12 - int baseRevision = daIndex.readInt(); // +16 - int linkRevision = daIndex.readInt(); // +20 - int parent1Revision = daIndex.readInt(); - int parent2Revision = daIndex.readInt(); - // Hg has 32 bytes here, uses 20 for nodeid, and keeps 12 last bytes empty - daIndex.readBytes(nodeidBuf, 0, 20); // +32 - daIndex.skip(12); - DataAccess userDataAccess = null; - if (needData) { - final byte firstByte; - int 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; - streamDataAccess = daData; - daData.seek(streamOffset); - } - final boolean patchToPrevious = baseRevision != i; // the only way I found to tell if it's a patch - firstByte = streamDataAccess.readByte(); - if (firstByte == 0x78 /* 'x' */) { - userDataAccess = new InflaterDataAccess(streamDataAccess, streamOffset, compressedLen, patchToPrevious ? -1 : actualLen); - } else if (firstByte == 0x75 /* 'u' */) { - userDataAccess = new FilterDataAccess(streamDataAccess, streamOffset+1, compressedLen-1); - } else { - // XXX Python impl in fact throws exception when there's not 'x', 'u' or '0' - // but I don't see reason not to return data as is - userDataAccess = new FilterDataAccess(streamDataAccess, streamOffset, compressedLen); - } - // XXX - if (patchToPrevious) { - // this is a patch - LinkedList patches = new LinkedList(); - while (!userDataAccess.isEmpty()) { - PatchRecord pr = PatchRecord.read(userDataAccess); -// System.out.printf("PatchRecord:%d %d %d\n", pr.start, pr.end, pr.len); - patches.add(pr); - } - userDataAccess.done(); - // - byte[] userData = apply(lastUserData, actualLen, patches); - userDataAccess = new ByteArrayDataAccess(userData); - } - } else { - if (inline) { - daIndex.skip(compressedLen); - } - } - if (!extraReadsToBaseRev || i >= start) { - inspector.next(i, actualLen, baseRevision, linkRevision, parent1Revision, parent2Revision, nodeidBuf, userDataAccess); - } - if (userDataAccess != null) { - userDataAccess.reset(); - if (lastUserData != null) { - lastUserData.done(); - } - lastUserData = userDataAccess; - } - } - } catch (IOException ex) { - throw new HgBadStateException(ex); // FIXME need better handling - } finally { - daIndex.done(); - if (daData != null) { - daData.done(); - } - } - } - - private int getBaseRevision(int revision) { - return baseRevisions[revision]; - } - - /** - * @return offset of the revision's record in the index (.i) stream - */ - private int getIndexOffsetInt(int revision) { - return inline ? indexRecordOffset[revision] : revision * REVLOGV1_RECORD_SIZE; - } - - private void initOutline() { - if (baseRevisions != null && baseRevisions.length > 0) { - return; - } - ArrayList resBases = new ArrayList(); - ArrayList resOffsets = new ArrayList(); - DataAccess da = getIndexStream(); - try { - if (da.isEmpty()) { - // do not fail with exception if stream is empty, it's likely intentional - baseRevisions = new int[0]; - return; - } - int versionField = da.readInt(); - da.readInt(); // just to skip next 4 bytes of offset + flags - final int INLINEDATA = 1 << 16; - inline = (versionField & INLINEDATA) != 0; - long offset = 0; // first offset is always 0, thus Hg uses it for other purposes - while(true) { - int compressedLen = da.readInt(); - // 8+4 = 12 bytes total read here - @SuppressWarnings("unused") - int actualLen = da.readInt(); - int baseRevision = da.readInt(); - // 12 + 8 = 20 bytes read here -// int linkRevision = di.readInt(); -// int parent1Revision = di.readInt(); -// int parent2Revision = di.readInt(); -// byte[] nodeid = new byte[32]; - resBases.add(baseRevision); - if (inline) { - int o = (int) 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)"); - } - resOffsets.add(o + REVLOGV1_RECORD_SIZE * resOffsets.size()); - da.skip(3*4 + 32 + compressedLen); // Check: 44 (skip) + 20 (read) = 64 (total RevlogNG record size) - } else { - da.skip(3*4 + 32); - } - if (da.isEmpty()) { - // fine, done then - baseRevisions = toArray(resBases); - if (inline) { - indexRecordOffset = toArray(resOffsets); - } - break; - } else { - // start reading next record - long l = da.readLong(); - offset = l >>> 16; - } - } - } catch (IOException ex) { - ex.printStackTrace(); // log error - // too bad, no outline then, but don't fail with NPE - baseRevisions = new int[0]; - } finally { - da.done(); - } - } - - private static int[] toArray(List l) { - int[] rv = new int[l.size()]; - for (int i = 0; i < rv.length; i++) { - rv[i] = l.get(i); - } - return rv; - } - - - // mpatch.c : apply() - // FIXME need to implement patch merge (fold, combine, gather and discard from aforementioned mpatch.[c|py]), also see Revlog and Mercurial PDF - public/*for HgBundle; until moved to better place*/static byte[] apply(DataAccess baseRevisionContent, int outcomeLen, List patch) throws IOException { - int last = 0, destIndex = 0; - if (outcomeLen == -1) { - outcomeLen = baseRevisionContent.length(); - for (PatchRecord pr : patch) { - outcomeLen += pr.start - last + pr.len; - last = pr.end; - } - outcomeLen -= last; - last = 0; - } - byte[] rv = new byte[outcomeLen]; - for (PatchRecord pr : patch) { - baseRevisionContent.seek(last); - baseRevisionContent.readBytes(rv, destIndex, pr.start-last); - destIndex += pr.start - last; - System.arraycopy(pr.data, 0, rv, destIndex, pr.data.length); - destIndex += pr.data.length; - last = pr.end; - } - baseRevisionContent.seek(last); - baseRevisionContent.readBytes(rv, destIndex, (int) (baseRevisionContent.length() - last)); - return rv; - } - - // @see http://mercurial.selenic.com/wiki/BundleFormat, in Changelog group description - public static class PatchRecord { - /* - Given there are pr1 and pr2: - pr1.start to pr1.end will be replaced with pr's data (of pr1.len) - pr1.end to pr2.start gets copied from base - */ - public int start, end, len; - public byte[] data; - - // TODO consider PatchRecord that only records data position (absolute in data source), and acquires data as needed - private PatchRecord(int p1, int p2, int length, byte[] src) { - start = p1; - end = p2; - len = length; - data = src; - } - - /*package-local*/ static PatchRecord read(byte[] data, int offset) { - final int x = offset; // shorthand - int p1 = ((data[x] & 0xFF)<< 24) | ((data[x+1] & 0xFF) << 16) | ((data[x+2] & 0xFF) << 8) | (data[x+3] & 0xFF); - int p2 = ((data[x+4] & 0xFF) << 24) | ((data[x+5] & 0xFF) << 16) | ((data[x+6] & 0xFF) << 8) | (data[x+7] & 0xFF); - int len = ((data[x+8] & 0xFF) << 24) | ((data[x+9] & 0xFF) << 16) | ((data[x+10] & 0xFF) << 8) | (data[x+11] & 0xFF); - byte[] dataCopy = new byte[len]; - System.arraycopy(data, x+12, dataCopy, 0, len); - return new PatchRecord(p1, p2, len, dataCopy); - } - - public /*for HgBundle*/ static PatchRecord read(DataAccess da) throws IOException { - int p1 = da.readInt(); - int p2 = da.readInt(); - int len = da.readInt(); - byte[] src = new byte[len]; - da.readBytes(src, 0, len); - return new PatchRecord(p1, p2, len, src); - } - } - - // FIXME byte[] data might be too expensive, for few usecases it may be better to have intermediate Access object (when we don't need full data - // instantly - e.g. calculate hash, or comparing two revisions - public interface Inspector { - // 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 revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[/*20*/] nodeid, DataAccess data); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/internal/StoragePathHelper.java --- a/src/org/tmatesoft/hg/internal/StoragePathHelper.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,197 +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 java.util.Arrays; -import java.util.TreeSet; - -import org.tmatesoft.hg.util.PathRewrite; - -/** - * @see http://mercurial.selenic.com/wiki/CaseFoldingPlan - * @see http://mercurial.selenic.com/wiki/fncacheRepoFormat - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -class StoragePathHelper implements PathRewrite { - - private final boolean store; - private final boolean fncache; - private final boolean dotencode; - - public StoragePathHelper(boolean isStore, boolean isFncache, boolean isDotencode) { - store = isStore; - fncache = isFncache; - dotencode = isDotencode; - } - - // 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 - public String rewrite(String path) { - final String STR_STORE = "store/"; - final String STR_DATA = "data/"; - final String STR_DH = "dh/"; - final String reservedChars = "\\:*?\"<>|"; - char[] hexByte = new char[2]; - - path = path.replace(".hg/", ".hg.hg/").replace(".i/", ".i.hg/").replace(".d/", ".d.hg/"); - StringBuilder sb = new StringBuilder(path.length() << 1); - if (store || fncache) { - // encodefilename - for (int i = 0; i < path.length(); i++) { - final char ch = path.charAt(i); - if (ch >= 'a' && ch <= 'z') { - sb.append(ch); // POIRAE - } else if (ch >= 'A' && ch <= 'Z') { - sb.append('_'); - sb.append(Character.toLowerCase(ch)); // Perhaps, (char) (((int) ch) + 32)? Even better, |= 0x20? - } else if (reservedChars.indexOf(ch) != -1) { - sb.append('~'); - sb.append(toHexByte(ch, hexByte)); - } else if ((ch >= '~' /*126*/ && ch <= 255) || ch < ' ' /*32*/) { - sb.append('~'); - sb.append(toHexByte(ch, hexByte)); - } else if (ch == '_') { - sb.append('_'); - sb.append('_'); - } else { - sb.append(ch); - } - } - // auxencode - if (fncache) { - encodeWindowsDeviceNames(sb); - } - } - final int MAX_PATH_LEN = 120; - if (fncache && (sb.length() + STR_DATA.length() + ".i".length() > MAX_PATH_LEN)) { - String digest = new DigestHelper().sha1(STR_DATA, path, ".i").asHexString(); - final int DIR_PREFIX_LEN = 8; - // not sure why (-4) is here. 120 - 40 = up to 80 for path with ext. dh/ + ext(.i) = 3+2 - final int MAX_DIR_PREFIX = 8 * (DIR_PREFIX_LEN + 1) - 4; - sb = new StringBuilder(MAX_PATH_LEN); - for (int i = 0; i < path.length(); i++) { - final char ch = path.charAt(i); - if (ch >= 'a' && ch <= 'z') { - sb.append(ch); - } else if (ch >= 'A' && ch <= 'Z') { - sb.append((char) (ch | 0x20)); // lowercase - } else if (reservedChars.indexOf(ch) != -1) { - sb.append('~'); - sb.append(toHexByte(ch, hexByte)); - } else if ((ch >= '~' /*126*/ && ch <= 255) || ch < ' ' /*32*/) { - sb.append('~'); - sb.append(toHexByte(ch, hexByte)); - } else { - sb.append(ch); - } - } - encodeWindowsDeviceNames(sb); - int fnameStart = sb.lastIndexOf("/"); // since we rewrite file names, it never ends with slash (for dirs, I'd pass length-2); - StringBuilder completeHashName = new StringBuilder(MAX_PATH_LEN); - completeHashName.append(STR_STORE); - completeHashName.append(STR_DH); - if (fnameStart == -1) { - // no dirs, just long filename - sb.setLength(MAX_PATH_LEN - 40 /*digest.length()*/ - STR_DH.length() - ".i".length()); - completeHashName.append(sb); - } else { - StringBuilder sb2 = new StringBuilder(MAX_PATH_LEN); - int x = 0; - do { - int i = sb.indexOf("/", x); - final int sb2Len = sb2.length(); - if (i-x <= DIR_PREFIX_LEN) { // a b c d e f g h / - sb2.append(sb, x, i + 1); // with slash - } else { - sb2.append(sb, x, x + DIR_PREFIX_LEN); - // may unexpectedly end with bad character - final int last = sb2.length()-1; - char lastChar = sb2.charAt(last); - assert lastChar == sb.charAt(x + DIR_PREFIX_LEN - 1); - if (lastChar == '.' || lastChar == ' ') { - sb2.setCharAt(last, '_'); - } - sb2.append('/'); - } - if (sb2.length()-1 > MAX_DIR_PREFIX) { - sb2.setLength(sb2Len); // strip off last segment, it's too much - break; - } - x = i+1; - } while (x < fnameStart); - assert sb2.charAt(sb2.length() - 1) == '/'; - int left = MAX_PATH_LEN - sb2.length() - 40 /*digest.length()*/ - STR_DH.length() - ".i".length(); - assert left >= 0; - fnameStart++; // move from / to actual name - sb2.append(sb, fnameStart, fnameStart + left > sb.length() ? sb.length() : fnameStart+left); - completeHashName.append(sb2); - } - completeHashName.append(digest); - sb = completeHashName; - } else if (store) { - sb.insert(0, STR_STORE + STR_DATA); - } - sb.append(".i"); - return sb.toString(); - } - - private void encodeWindowsDeviceNames(StringBuilder sb) { - char[] hexByte = new char[2]; - int x = 0; // last segment start - final TreeSet windowsReservedFilenames = new TreeSet(); - windowsReservedFilenames.addAll(Arrays.asList("con prn aux nul com1 com2 com3 com4 com5 com6 com7 com8 com9 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9".split(" "))); - do { - int i = sb.indexOf("/", x); - if (i == -1) { - i = sb.length(); - } - // windows reserved filenames are at least of length 3 - if (i - x >= 3) { - boolean found = false; - if (i-x == 3 || i-x == 4) { - found = windowsReservedFilenames.contains(sb.subSequence(x, i)); - } else if (sb.charAt(x+3) == '.') { // implicit i-x > 3 - found = windowsReservedFilenames.contains(sb.subSequence(x, x+3)); - } else if (i-x > 4 && sb.charAt(x+4) == '.') { - found = windowsReservedFilenames.contains(sb.subSequence(x, x+4)); - } - if (found) { - sb.insert(x+3, toHexByte(sb.charAt(x+2), hexByte)); - sb.setCharAt(x+2, '~'); - i += 2; - } - } - if (dotencode && (sb.charAt(x) == '.' || sb.charAt(x) == ' ')) { - sb.insert(x+1, toHexByte(sb.charAt(x), hexByte)); - sb.setCharAt(x, '~'); // setChar *after* charAt/insert to get ~2e, not ~7e for '.' - i += 2; - } - x = i+1; - } while (x < sb.length()); - } - - private static char[] toHexByte(int ch, char[] buf) { - assert buf.length > 1; - final String hexDigits = "0123456789abcdef"; - buf[0] = hexDigits.charAt((ch & 0x00F0) >>> 4); - buf[1] = hexDigits.charAt(ch & 0x0F); - return buf; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/HgBundle.java --- a/src/org/tmatesoft/hg/repo/HgBundle.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,429 +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.repo; - -import static org.tmatesoft.hg.core.Nodeid.NULL; - -import java.io.File; -import java.io.IOException; -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.Nodeid; -import org.tmatesoft.hg.internal.ByteArrayChannel; -import org.tmatesoft.hg.internal.ByteArrayDataAccess; -import org.tmatesoft.hg.internal.DataAccess; -import org.tmatesoft.hg.internal.DataAccessProvider; -import org.tmatesoft.hg.internal.DigestHelper; -import org.tmatesoft.hg.internal.InflaterDataAccess; -import org.tmatesoft.hg.internal.RevlogStream; -import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; -import org.tmatesoft.hg.util.CancelledException; - -/** - * @see http://mercurial.selenic.com/wiki/BundleFormat - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgBundle { - - private final File bundleFile; - private final DataAccessProvider accessProvider; - - HgBundle(DataAccessProvider dap, File bundle) { - accessProvider = dap; - bundleFile = bundle; - } - - private DataAccess getDataStream() throws IOException { - DataAccess da = accessProvider.create(bundleFile); - byte[] signature = new byte[6]; - if (da.length() > 6) { - da.readBytes(signature, 0, 6); - if (signature[0] == 'H' && signature[1] == 'G' && signature[2] == '1' && signature[3] == '0') { - if (signature[4] == 'G' && signature[5] == 'Z') { - return new InflaterDataAccess(da, 6, da.length() - 6); - } - if (signature[4] == 'B' && signature[5] == 'Z') { - throw HgRepository.notImplemented(); - } - if (signature[4] != 'U' || signature[5] != 'N') { - throw new HgBadStateException("Bad bundle signature:" + new String(signature)); - } - // "...UN", fall-through - } else { - da.reset(); - } - } - return da; - } - - private int uses = 0; - public HgBundle link() { - uses++; - return this; - } - public void unlink() { - uses--; - if (uses == 0 && bundleFile != null) { - bundleFile.deleteOnExit(); - } - } - public boolean inUse() { - return uses > 0; - } - - /** - * Get changes recorded in the bundle that are missing from the supplied repository. - * @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 HgException, IOException { - Inspector bundleInsp = new Inspector() { - DigestHelper dh = new DigestHelper(); - boolean emptyChangelog = true; - private DataAccess prevRevContent; - private int revisionIndex; - - public void changelogStart() { - emptyChangelog = true; - revisionIndex = 0; - } - - public void changelogEnd() { - if (emptyChangelog) { - throw new IllegalStateException("No changelog group in the bundle"); // XXX perhaps, just be silent and/or log? - } - } - -/* - * Despite that BundleFormat wiki says: "Each Changelog entry patches the result of all previous patches - * (the previous, or parent patch of a given patch p is the patch that has a node equal to p's p1 field)", - * it seems not to hold true. Instead, each entry patches previous one, regardless of whether the one - * before is its parent (i.e. ge.firstParent()) or not. - * -Actual state in the changelog.i -Index Offset Flags Packed Actual Base Rev Link Rev Parent1 Parent2 nodeid - 50: 9212 0 209 329 48 50 49 -1 f1db8610da62a3e0beb8d360556ee1fd6eb9885e - 51: 9421 0 278 688 48 51 50 -1 9429c7bd1920fab164a9d2b621d38d57bcb49ae0 - 52: 9699 0 154 179 52 52 50 -1 30bd389788464287cee22ccff54c330a4b715de5 - 53: 9853 0 133 204 52 53 51 52 a6f39e595b2b54f56304470269a936ead77f5725 - 54: 9986 0 156 182 54 54 52 -1 fd4f2c98995beb051070630c272a9be87bef617d - -Excerpt from bundle (nodeid, p1, p2, cs): - f1db8610da62a3e0beb8d360556ee1fd6eb9885e 26e3eeaa39623de552b45ee1f55c14f36460f220 0000000000000000000000000000000000000000 f1db8610da62a3e0beb8d360556ee1fd6eb9885e; patches:4 - 9429c7bd1920fab164a9d2b621d38d57bcb49ae0 f1db8610da62a3e0beb8d360556ee1fd6eb9885e 0000000000000000000000000000000000000000 9429c7bd1920fab164a9d2b621d38d57bcb49ae0; patches:3 -> 30bd389788464287cee22ccff54c330a4b715de5 f1db8610da62a3e0beb8d360556ee1fd6eb9885e 0000000000000000000000000000000000000000 30bd389788464287cee22ccff54c330a4b715de5; patches:3 - a6f39e595b2b54f56304470269a936ead77f5725 9429c7bd1920fab164a9d2b621d38d57bcb49ae0 30bd389788464287cee22ccff54c330a4b715de5 a6f39e595b2b54f56304470269a936ead77f5725; patches:3 - fd4f2c98995beb051070630c272a9be87bef617d 30bd389788464287cee22ccff54c330a4b715de5 0000000000000000000000000000000000000000 fd4f2c98995beb051070630c272a9be87bef617d; patches:3 - -To recreate 30bd..e5, one have to take content of 9429..e0, not its p1 f1db..5e - */ - public boolean element(GroupElement ge) { - emptyChangelog = false; - HgChangelog changelog = hgRepo.getChangelog(); - try { - if (prevRevContent == null) { - if (NULL.equals(ge.firstParent()) && NULL.equals(ge.secondParent())) { - prevRevContent = new ByteArrayDataAccess(new byte[0]); - } else { - final Nodeid base = ge.firstParent(); - if (!changelog.isKnown(base) /*only first parent, that's Bundle contract*/) { - 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 - // extra byte[] (inside ByteArrayChannel) duplication just for the sake of subsequent ByteArrayDataChannel wrap. - prevRevContent = new ByteArrayDataAccess(bac.toArray()); - } - } - // - 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()); - } - ByteArrayDataAccess csetDataAccess = new ByteArrayDataAccess(csetContent); - RawChangeset cs = RawChangeset.parse(csetDataAccess); - inspector.next(revisionIndex++, ge.node(), cs); - prevRevContent.done(); - prevRevContent = csetDataAccess.reset(); - } catch (CancelledException ex) { - return false; - } catch (Exception ex) { - throw new HgBadStateException(ex); // FIXME - } - return true; - } - - public void manifestStart() {} - public void manifestEnd() {} - public void fileStart(String name) {} - public void fileEnd(String name) {} - - }; - inspectChangelog(bundleInsp); - } - - public void dump() throws IOException { - Dump dump = new Dump(); - inspectAll(dump); - System.out.println("Total files:" + dump.names.size()); - for (String s : dump.names) { - System.out.println(s); - } - } - - // callback to minimize amount of Strings and Nodeids instantiated - public interface Inspector { - void changelogStart(); - - void changelogEnd(); - - void manifestStart(); - - void manifestEnd(); - - void fileStart(String name); - - void fileEnd(String name); - - /** - * XXX desperately need exceptions here - * @param element data element, instance might be reused, don't keep a reference to it or its raw data - * @return true to continue - */ - boolean element(GroupElement element); - } - - public static class Dump implements Inspector { - public final LinkedList names = new LinkedList(); - - public void changelogStart() { - System.out.println("Changelog group"); - } - - public void changelogEnd() { - } - - public void manifestStart() { - System.out.println("Manifest group"); - } - - public void manifestEnd() { - } - - public void fileStart(String name) { - names.add(name); - System.out.println(name); - } - - public void fileEnd(String name) { - } - - public boolean element(GroupElement ge) { - try { - System.out.printf(" %s %s %s %s; patches:%d\n", ge.node(), ge.firstParent(), ge.secondParent(), ge.cset(), ge.patches().size()); - } catch (Exception ex) { - ex.printStackTrace(); // FIXME - } - return true; - } - } - - public void inspectChangelog(Inspector inspector) throws IOException { - if (inspector == null) { - throw new IllegalArgumentException(); - } - DataAccess da = getDataStream(); - try { - internalInspectChangelog(da, inspector); - } finally { - da.done(); - } - } - - public void inspectManifest(Inspector inspector) throws IOException { - if (inspector == null) { - throw new IllegalArgumentException(); - } - DataAccess da = getDataStream(); - try { - if (da.isEmpty()) { - return; - } - skipGroup(da); // changelog - internalInspectManifest(da, inspector); - } finally { - da.done(); - } - } - - public void inspectFiles(Inspector inspector) throws IOException { - if (inspector == null) { - throw new IllegalArgumentException(); - } - DataAccess da = getDataStream(); - try { - if (da.isEmpty()) { - return; - } - skipGroup(da); // changelog - if (da.isEmpty()) { - return; - } - skipGroup(da); // manifest - internalInspectFiles(da, inspector); - } finally { - da.done(); - } - } - - public void inspectAll(Inspector inspector) throws IOException { - if (inspector == null) { - throw new IllegalArgumentException(); - } - DataAccess da = getDataStream(); - try { - internalInspectChangelog(da, inspector); - internalInspectManifest(da, inspector); - internalInspectFiles(da, inspector); - } finally { - da.done(); - } - } - - private void internalInspectChangelog(DataAccess da, Inspector inspector) throws IOException { - if (da.isEmpty()) { - return; - } - inspector.changelogStart(); - readGroup(da, inspector); - inspector.changelogEnd(); - } - - private void internalInspectManifest(DataAccess da, Inspector inspector) throws IOException { - if (da.isEmpty()) { - return; - } - inspector.manifestStart(); - readGroup(da, inspector); - inspector.manifestEnd(); - } - - private void internalInspectFiles(DataAccess da, Inspector inspector) throws IOException { - while (!da.isEmpty()) { - int fnameLen = da.readInt(); - if (fnameLen <= 4) { - break; // null chunk, the last one. - } - byte[] fnameBuf = new byte[fnameLen - 4]; - da.readBytes(fnameBuf, 0, fnameBuf.length); - String name = new String(fnameBuf); - inspector.fileStart(name); - readGroup(da, inspector); - inspector.fileEnd(name); - } - } - - private static void readGroup(DataAccess da, Inspector inspector) throws IOException { - int len = da.readInt(); - boolean good2go = true; - while (len > 4 && !da.isEmpty() && good2go) { - byte[] nb = new byte[80]; - da.readBytes(nb, 0, 80); - int dataLength = len - 84 /* length field + 4 nodeids */; - byte[] data = new byte[dataLength]; - da.readBytes(data, 0, dataLength); - DataAccess slice = new ByteArrayDataAccess(data); // XXX in fact, may pass a slicing DataAccess. - // Just need to make sure that we seek to proper location afterwards (where next GroupElement starts), - // regardless whether that slice has read it or not. - GroupElement ge = new GroupElement(nb, slice); - good2go = inspector.element(ge); - slice.done(); // BADA doesn't implement done(), but it could (e.g. free array) - /// and we'd better tell it we are not going to use it any more. However, it's important to ensure Inspector - // implementations out there do not retain GroupElement.rawData() - len = da.isEmpty() ? 0 : da.readInt(); - } - // need to skip up to group end if inspector told he don't want to continue with the group, - // because outer code may try to read next group immediately as we return back. - while (len > 4 && !da.isEmpty()) { - da.skip(len - 4 /* length field */); - len = da.isEmpty() ? 0 : da.readInt(); - } - } - - private static void skipGroup(DataAccess da) throws IOException { - int len = da.readInt(); - while (len > 4 && !da.isEmpty()) { - da.skip(len - 4); // sizeof(int) - len = da.isEmpty() ? 0 : da.readInt(); - } - } - - public static class GroupElement { - private final byte[] header; // byte[80] takes 120 bytes, 4 Nodeids - 192 - private final DataAccess dataAccess; - private List patches; - - GroupElement(byte[] fourNodeids, DataAccess rawDataAccess) { - assert fourNodeids != null && fourNodeids.length == 80; - header = fourNodeids; - dataAccess = rawDataAccess; - } - - public Nodeid node() { - return Nodeid.fromBinary(header, 0); - } - - public Nodeid firstParent() { - return Nodeid.fromBinary(header, 20); - } - - public Nodeid secondParent() { - return Nodeid.fromBinary(header, 40); - } - - public Nodeid cset() { // cs seems to be changeset - return Nodeid.fromBinary(header, 60); - } - - public DataAccess rawData() { - return dataAccess; - } - - public List patches() throws IOException { - if (patches == null) { - dataAccess.reset(); - LinkedList p = new LinkedList(); - while (!dataAccess.isEmpty()) { - RevlogStream.PatchRecord pr = RevlogStream.PatchRecord.read(dataAccess); - p.add(pr); - } - patches = p; - } - return patches; - } - - public byte[] apply(DataAccess baseContent) throws IOException { - return RevlogStream.apply(baseContent, -1, patches()); - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/HgChangelog.java --- a/src/org/tmatesoft/hg/repo/HgChangelog.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,351 +0,0 @@ -/* - * Copyright (c) 2010-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.repo; - -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.Formatter; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.TimeZone; - -import org.tmatesoft.hg.core.HgBadStateException; -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.internal.DataAccess; -import org.tmatesoft.hg.internal.Pool; -import org.tmatesoft.hg.internal.RevlogStream; - -/** - * Representation of the Mercurial changelog file (list of ChangeSets) - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgChangelog extends Revlog { - - /* package-local */HgChangelog(HgRepository hgRepo, RevlogStream content) { - super(hgRepo, content); - } - - public void all(final HgChangelog.Inspector inspector) { - range(0, getLastRevision(), inspector); - } - - public void range(int start, int end, final HgChangelog.Inspector inspector) { - if (inspector == null) { - throw new IllegalArgumentException(); - } - content.iterate(start, end, true, new RawCsetParser(inspector)); - } - - public List range(int start, int end) { - final RawCsetCollector c = new RawCsetCollector(end - start + 1); - range(start, end, c); - return c.result; - } - - public void range(final HgChangelog.Inspector inspector, final int... revisions) { - if (revisions == null || revisions.length == 0) { - return; - } - RevlogStream.Inspector i = new RevlogStream.Inspector() { - private final RawCsetParser delegate = new RawCsetParser(inspector); - - public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) { - if (Arrays.binarySearch(revisions, revisionNumber) >= 0) { - delegate.next(revisionNumber, actualLen, baseRevision, linkRevision, parent1Revision, parent2Revision, nodeid, da); - } - } - }; - Arrays.sort(revisions); - content.iterate(revisions[0], revisions[revisions.length - 1], true, i); - } - - 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); - } - - /** - * Entry in the Changelog - */ - public static class RawChangeset implements Cloneable /* for those that would like to keep a copy */{ - // TODO immutable - private/* final */Nodeid manifest; - private String user; - private String comment; - private List files; // unmodifiable collection (otherwise #files() and implicit #clone() shall be revised) - private Date time; - private int timezone; - // http://mercurial.selenic.com/wiki/PruningDeadBranches - Closing changesets can be identified by close=1 in the changeset's extra field. - private Map extras; - - /** - * @see mercurial/changelog.py:read() - * - *
-		 *         format used:
-		 *         nodeid\n        : manifest node in ascii
-		 *         user\n          : user, no \n or \r allowed
-		 *         time tz extra\n : date (time is int or float, timezone is int)
-		 *                         : extra is metadatas, encoded and separated by '\0'
-		 *                         : older versions ignore it
-		 *         files\n\n       : files modified by the cset, no \n or \r allowed
-		 *         (.*)            : comment (free text, ideally utf-8)
-		 * 
-		 *         changelog v0 doesn't use extra
-		 * 
- */ - private RawChangeset() { - } - - public Nodeid manifest() { - return manifest; - } - - public String user() { - return user; - } - - public String comment() { - return comment; - } - - public List files() { - return files; - } - - public Date date() { - return time; - } - - /** - * @return time zone value, as is, positive for Western Hemisphere. - */ - public int timezone() { - return timezone; - } - - public String dateString() { - // XXX keep once formatted? Perhaps, there's faster way to set up calendar/time zone? - StringBuilder sb = new StringBuilder(30); - Formatter f = new Formatter(sb, Locale.US); - TimeZone tz = TimeZone.getTimeZone(TimeZone.getAvailableIDs(timezone * 1000)[0]); - // apparently timezone field records number of seconds time differs from UTC, - // i.e. value to substract from time to get UTC time. Calendar seems to add - // timezone offset to UTC, instead, hence sign change. -// tz.setRawOffset(timezone * -1000); - Calendar c = Calendar.getInstance(tz, Locale.US); - c.setTime(time); - f.format("%ta % extras() { - return extras; - } - - public String branch() { - return extras.get("branch"); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("Changeset {"); - sb.append("User: ").append(user).append(", "); - sb.append("Comment: ").append(comment).append(", "); - sb.append("Manifest: ").append(manifest).append(", "); - sb.append("Date: ").append(time).append(", "); - sb.append("Files: ").append(files.size()); - for (String s : files) { - sb.append(", ").append(s); - } - if (extras != null) { - sb.append(", Extra: ").append(extras); - } - sb.append("}"); - return sb.toString(); - } - - @Override - public RawChangeset clone() { - try { - return (RawChangeset) super.clone(); - } catch (CloneNotSupportedException ex) { - throw new InternalError(ex.toString()); - } - } - - public static RawChangeset parse(DataAccess da) { - try { - byte[] data = da.byteArray(); - RawChangeset rv = new RawChangeset(); - rv.init(data, 0, data.length, null); - return rv; - } catch (IOException ex) { - throw new HgBadStateException(ex); // FIXME "Error reading changeset data" - } - } - - // @param usersPool - it's likely user names get repeated again and again throughout repository. can be null - /* package-local */void init(byte[] data, int offset, int length, Pool usersPool) { - final int bufferEndIndex = offset + length; - final byte lineBreak = (byte) '\n'; - int breakIndex1 = indexOf(data, lineBreak, offset, bufferEndIndex); - if (breakIndex1 == -1) { - throw new IllegalArgumentException("Bad Changeset data"); - } - Nodeid _nodeid = Nodeid.fromAscii(data, 0, breakIndex1); - int breakIndex2 = indexOf(data, lineBreak, breakIndex1 + 1, bufferEndIndex); - if (breakIndex2 == -1) { - throw new IllegalArgumentException("Bad Changeset data"); - } - String _user = new String(data, breakIndex1 + 1, breakIndex2 - breakIndex1 - 1); - if (usersPool != null) { - _user = usersPool.unify(_user); - } - int breakIndex3 = indexOf(data, lineBreak, breakIndex2 + 1, bufferEndIndex); - if (breakIndex3 == -1) { - throw new IllegalArgumentException("Bad Changeset data"); - } - String _timeString = new String(data, breakIndex2 + 1, breakIndex3 - breakIndex2 - 1); - int space1 = _timeString.indexOf(' '); - if (space1 == -1) { - throw new IllegalArgumentException("Bad Changeset data"); - } - int space2 = _timeString.indexOf(' ', space1 + 1); - if (space2 == -1) { - space2 = _timeString.length(); - } - long unixTime = Long.parseLong(_timeString.substring(0, space1)); // XXX Float, perhaps - int _timezone = Integer.parseInt(_timeString.substring(space1 + 1, space2)); - // XXX not sure need to add timezone here - I can't figure out whether Hg keeps GMT time, and records timezone just for info, or unixTime is taken local - // on commit and timezone is recorded to adjust it to UTC. - Date _time = new Date(unixTime * 1000); - String _extras = space2 < _timeString.length() ? _timeString.substring(space2 + 1) : null; - Map _extrasMap; - if (_extras == null) { - _extrasMap = Collections.singletonMap("branch", "default"); - } else { - _extrasMap = new HashMap(); - for (String pair : _extras.split("\00")) { - int eq = pair.indexOf(':'); - // FIXME need to decode key/value, @see changelog.py:decodeextra - _extrasMap.put(pair.substring(0, eq), pair.substring(eq + 1)); - } - if (!_extrasMap.containsKey("branch")) { - _extrasMap.put("branch", "default"); - } - _extrasMap = Collections.unmodifiableMap(_extrasMap); - } - - // - int lastStart = breakIndex3 + 1; - int breakIndex4 = indexOf(data, lineBreak, lastStart, bufferEndIndex); - ArrayList _files = null; - if (breakIndex4 > lastStart) { - // if breakIndex4 == lastStart, we already found \n\n and hence there are no files (e.g. merge revision) - _files = new ArrayList(5); - while (breakIndex4 != -1 && breakIndex4 + 1 < bufferEndIndex) { - _files.add(new String(data, lastStart, breakIndex4 - lastStart)); - lastStart = breakIndex4 + 1; - if (data[breakIndex4 + 1] == lineBreak) { - // found \n\n - break; - } else { - breakIndex4 = indexOf(data, lineBreak, lastStart, bufferEndIndex); - } - } - if (breakIndex4 == -1 || breakIndex4 >= bufferEndIndex) { - throw new IllegalArgumentException("Bad Changeset data"); - } - } else { - breakIndex4--; - } - String _comment; - try { - _comment = new String(data, breakIndex4 + 2, bufferEndIndex - breakIndex4 - 2, "UTF-8"); - // FIXME respect ui.fallbackencoding and try to decode if set - } catch (UnsupportedEncodingException ex) { - _comment = ""; - throw new IllegalStateException("Could hardly happen"); - } - // change this instance at once, don't leave it partially changes in case of error - this.manifest = _nodeid; - this.user = _user; - this.time = _time; - this.timezone = _timezone; - this.files = _files == null ? Collections. emptyList() : Collections.unmodifiableList(_files); - this.comment = _comment; - this.extras = _extrasMap; - } - - private static int indexOf(byte[] src, byte what, int startOffset, int endIndex) { - for (int i = startOffset; i < endIndex; i++) { - if (src[i] == what) { - return i; - } - } - return -1; - } - } - - private static class RawCsetCollector implements Inspector { - final ArrayList result; - - public RawCsetCollector(int count) { - result = new ArrayList(count > 0 ? count : 5); - } - - public void next(int revisionNumber, Nodeid nodeid, RawChangeset cset) { - result.add(cset.clone()); - } - } - - private static class RawCsetParser implements RevlogStream.Inspector { - - private final Inspector inspector; - private final Pool usersPool; - private final RawChangeset cset = new RawChangeset(); - - public RawCsetParser(HgChangelog.Inspector delegate) { - assert delegate != null; - inspector = delegate; - usersPool = new Pool(); - } - - 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); - } catch (Exception ex) { - throw new HgBadStateException(ex); // FIXME exception handling - } - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/HgDataFile.java --- a/src/org/tmatesoft/hg/repo/HgDataFile.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,390 +0,0 @@ -/* - * Copyright (c) 2010-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.repo; - -import static org.tmatesoft.hg.repo.HgInternals.wrongLocalRevision; -import static org.tmatesoft.hg.repo.HgRepository.*; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Collection; -import java.util.TreeMap; - -import org.tmatesoft.hg.core.HgDataStreamException; -import org.tmatesoft.hg.core.HgException; -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.internal.DataAccess; -import org.tmatesoft.hg.internal.FilterByteChannel; -import org.tmatesoft.hg.internal.RevlogStream; -import org.tmatesoft.hg.util.ByteChannel; -import org.tmatesoft.hg.util.CancelledException; -import org.tmatesoft.hg.util.Path; - - - -/** - * ? name:HgFileNode? - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgDataFile extends Revlog { - - // absolute from repo root? - // slashes, unix-style? - // repo location agnostic, just to give info to user, not to access real storage - private final Path path; - private Metadata metadata; // get initialized on first access to file content. - - /*package-local*/HgDataFile(HgRepository hgRepo, Path filePath, RevlogStream content) { - super(hgRepo, content); - path = filePath; - } - - /*package-local*/HgDataFile(HgRepository hgRepo, Path filePath) { - super(hgRepo); - path = filePath; - } - - // 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. - 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") - public Path getPath() { - return path; // hgRepo.backresolve(this) -> name? In this case, what about hashed long names? - } - - public int length(Nodeid nodeid) { - return content.dataLength(getLocalRevision(nodeid)); - } - - public void workingCopy(ByteChannel sink) throws IOException, CancelledException { - throw HgRepository.notImplemented(); - } - -// 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, IOException, CancelledException { - content(revision, new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath()))); - } - - // for data files need to check heading of the file content for possible metadata - // @see http://mercurial.selenic.com/wiki/FileFormats#data.2BAC8- - public void content(int revision, ByteChannel sink) throws HgDataStreamException, IOException, CancelledException { - if (revision == TIP) { - revision = getLastRevision(); - } - if (revision == WORKING_COPY) { - workingCopy(sink); - return; - } - if (wrongLocalRevision(revision) || revision == BAD_REVISION) { - throw new IllegalArgumentException(String.valueOf(revision)); - } - if (sink == null) { - throw new IllegalArgumentException(); - } - if (metadata == null) { - metadata = new Metadata(); - } - ContentPipe insp; - if (metadata.none(revision)) { - insp = new ContentPipe(sink, 0); - } else if (metadata.known(revision)) { - insp = new ContentPipe(sink, metadata.dataOffset(revision)); - } else { - // do not know if there's metadata - insp = new MetadataContentPipe(sink, metadata); - } - insp.checkCancelled(); - super.content.iterate(revision, revision, true, insp); - try { - insp.checkFailed(); - } catch (HgDataStreamException ex) { - throw ex; - } catch (HgException ex) { - // shall not happen, unless we changed ContentPipe or its subclass - throw new HgDataStreamException(ex.getClass().getName(), ex); - } - } - - public void history(HgChangelog.Inspector inspector) { - history(0, getLastRevision(), inspector); - } - - public void history(int start, int end, HgChangelog.Inspector inspector) { - if (!exists()) { - throw new IllegalStateException("Can't get history of invalid repository file node"); - } - final int last = getLastRevision(); - if (start < 0 || start > last) { - throw new IllegalArgumentException(); - } - if (end == TIP) { - end = last; - } else if (end < start || end > last) { - throw new IllegalArgumentException(); - } - final int[] commitRevisions = new int[end - start + 1]; - RevlogStream.Inspector insp = new RevlogStream.Inspector() { - int count = 0; - - public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) { - commitRevisions[count++] = linkRevision; - } - }; - content.iterate(start, end, false, insp); - getRepo().getChangelog().range(inspector, commitRevisions); - } - - // for a given local revision of the file, find out local revision in the changelog - public int getChangesetLocalRevision(int revision) { - return content.linkRevision(revision); - } - - public Nodeid getChangesetRevision(Nodeid nid) { - int changelogRevision = getChangesetLocalRevision(getLocalRevision(nid)); - return getRepo().getChangelog().getRevision(changelogRevision); - } - - public boolean isCopy() throws HgDataStreamException { - if (metadata == null || !metadata.checked(0)) { - // content() always initializes metadata. - // FIXME 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 - // (compared to command-line counterpart of 190ms) - try { - content(0, new ByteChannel() { // No-op channel - public int write(ByteBuffer buffer) throws IOException, CancelledException { - // pretend we consumed whole buffer -// int rv = buffer.remaining(); -// buffer.position(buffer.limit()); -// return rv; - throw new CancelledException(); - } - }); - } catch (CancelledException ex) { - // it's ok, we did that - } catch (Exception ex) { - throw new HgDataStreamException("Can't initialize metadata", ex); - } - } - if (!metadata.known(0)) { - return false; - } - return metadata.find(0, "copy") != null; - } - - public Path getCopySourceName() throws HgDataStreamException { - 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 { - if (isCopy()) { - return Nodeid.fromAscii(metadata.find(0, "copyrev")); // XXX reuse/cache Nodeid - } - throw new UnsupportedOperationException(); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(getClass().getSimpleName()); - sb.append('('); - sb.append(getPath()); - sb.append(')'); - return sb.toString(); - } - - private static final class MetadataEntry { - private final String entry; - private final int valueStart; - /*package-local*/MetadataEntry(String key, String value) { - entry = key + value; - valueStart = key.length(); - } - /*package-local*/boolean matchKey(String key) { - return key.length() == valueStart && entry.startsWith(key); - } -// uncomment once/if needed -// public String key() { -// return entry.substring(0, valueStart); -// } - public String value() { - return entry.substring(valueStart); - } - } - - private static class Metadata { - // XXX sparse array needed - private final TreeMap offsets = new TreeMap(); - private final TreeMap entries = new TreeMap(); - - private final Integer NONE = new Integer(-1); // do not duplicate -1 integers at least within single file (don't want statics) - - // true when there's metadata for given revision - boolean known(int revision) { - Integer i = offsets.get(revision); - return i != null && NONE != i; - } - - // true when revision has been checked for metadata presence. - public boolean checked(int revision) { - return offsets.containsKey(revision); - } - - // true when revision has been checked and found not having any metadata - boolean none(int revision) { - Integer i = offsets.get(revision); - return i == NONE; - } - - // mark revision as having no metadata. - void recordNone(int revision) { - Integer i = offsets.get(revision); - if (i == NONE) { - return; // already there - } - if (i != null) { - throw new IllegalStateException(String.format("Trying to override Metadata state for revision %d (known offset: %d)", revision, i)); - } - offsets.put(revision, NONE); - } - - // since this is internal class, callers are supposed to ensure arg correctness (i.e. ask known() before) - int dataOffset(int revision) { - return offsets.get(revision); - } - void add(int revision, int dataOffset, Collection e) { - assert !offsets.containsKey(revision); - offsets.put(revision, dataOffset); - entries.put(revision, e.toArray(new MetadataEntry[e.size()])); - } - String find(int revision, String key) { - for (MetadataEntry me : entries.get(revision)) { - if (me.matchKey(key)) { - return me.value(); - } - } - return null; - } - } - - private static class MetadataContentPipe extends ContentPipe { - - private final Metadata metadata; - - public MetadataContentPipe(ByteChannel sink, Metadata _metadata) { - super(sink, 0); - metadata = _metadata; - } - - @Override - protected void prepare(int revisionNumber, DataAccess da) throws HgException, IOException { - final int daLength = da.length(); - if (daLength < 4 || da.readByte() != 1 || da.readByte() != 10) { - metadata.recordNone(revisionNumber); - da.reset(); - return; - } - int lastEntryStart = 2; - int lastColon = -1; - ArrayList _metadata = new ArrayList(); - // XXX in fact, need smth like ByteArrayBuilder, similar to StringBuilder, - // which can't be used here because we can't convert bytes to chars as we read them - // (there might be multi-byte encoding), and we need to collect all bytes before converting to string - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - String key = null, value = null; - boolean byteOne = false; - for (int i = 2; i < daLength; i++) { - byte b = da.readByte(); - if (b == '\n') { - if (byteOne) { // i.e. \n follows 1 - lastEntryStart = i+1; - // XXX is it possible to have here incomplete key/value (i.e. if last pair didn't end with \n) - break; - } - if (key == null || lastColon == -1 || i <= lastColon) { - throw new IllegalStateException(); // FIXME log instead and record null key in the metadata. Ex just to fail fast during dev - } - value = new String(bos.toByteArray()).trim(); - bos.reset(); - _metadata.add(new MetadataEntry(key, value)); - key = value = null; - lastColon = -1; - lastEntryStart = i+1; - continue; - } - // byteOne has to be consumed up to this line, if not jet, consume it - if (byteOne) { - // insert 1 we've read on previous step into the byte builder - bos.write(1); - // fall-through to consume current byte - byteOne = false; - } - if (b == (int) ':') { - assert value == null; - key = new String(bos.toByteArray()); - bos.reset(); - lastColon = i; - } else if (b == 1) { - byteOne = true; - } else { - bos.write(b); - } - } - _metadata.trimToSize(); - metadata.add(revisionNumber, lastEntryStart, _metadata); - if (da.isEmpty() || !byteOne) { - throw new HgDataStreamException(String.format("Metadata for revision %d is not closed properly", revisionNumber), null); - } - // da is in prepared state (i.e. we consumed all bytes up to metadata end). - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/HgDirstate.java --- a/src/org/tmatesoft/hg/repo/HgDirstate.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,184 +0,0 @@ -/* - * Copyright (c) 2010-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.repo; - -import java.io.File; -import java.io.IOException; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.TreeSet; - -import org.tmatesoft.hg.internal.DataAccess; -import org.tmatesoft.hg.internal.DataAccessProvider; -import org.tmatesoft.hg.util.Path; - - -/** - * @see http://mercurial.selenic.com/wiki/DirState - * @see http://mercurial.selenic.com/wiki/FileFormats#dirstate - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -class HgDirstate { - - private final DataAccessProvider accessProvider; - private final File dirstateFile; - // deliberate String, not Path as it seems useless to keep Path here - private Map normal; - private Map added; - private Map removed; - private Map merged; - - /*package-local*/ HgDirstate() { - // empty instance - accessProvider = null; - dirstateFile = null; - } - - public HgDirstate(DataAccessProvider dap, File dirstate) { - accessProvider = dap; - dirstateFile = dirstate; - } - - private void read() { - normal = added = removed = merged = Collections.emptyMap(); - if (dirstateFile == null || !dirstateFile.exists()) { - return; - } - DataAccess da = accessProvider.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 { - // XXX skip(40) if we don't need these? - byte[] parents = new byte[40]; - da.readBytes(parents, 0, 40); - parents = null; - do { - final byte state = da.readByte(); - final int fmode = da.readInt(); - final int size = da.readInt(); - final int time = da.readInt(); - final int nameLen = da.readInt(); - String fn1 = null, fn2 = null; - byte[] name = new byte[nameLen]; - da.readBytes(name, 0, nameLen); - for (int i = 0; i < nameLen; i++) { - if (name[i] == 0) { - fn1 = new String(name, 0, i, "UTF-8"); // XXX unclear from documentation what encoding is used there - fn2 = new String(name, i+1, nameLen - i - 1, "UTF-8"); // need to check with different system codepages - break; - } - } - if (fn1 == null) { - fn1 = new String(name); - } - Record r = new Record(fmode, size, time, fn1, fn2); - if (state == 'n') { - normal.put(r.name1, r); - } else if (state == 'a') { - added.put(r.name1, r); - } else if (state == 'r') { - removed.put(r.name1, r); - } else if (state == 'm') { - merged.put(r.name1, r); - } else { - // FIXME log error? - } - } while (!da.isEmpty()); - } catch (IOException ex) { - ex.printStackTrace(); // FIXME log error, clean dirstate? - } finally { - da.done(); - } - } - - // new, modifiable collection - /*package-local*/ TreeSet all() { - read(); - TreeSet rv = new TreeSet(); - @SuppressWarnings("unchecked") - Map[] all = new Map[] { normal, added, removed, merged }; - for (int i = 0; i < all.length; i++) { - for (Record r : all[i].values()) { - rv.add(r.name1); - } - } - return rv; - } - - /*package-local*/ Record checkNormal(Path fname) { - return normal.get(fname.toString()); - } - - /*package-local*/ Record checkAdded(Path fname) { - return added.get(fname.toString()); - } - /*package-local*/ Record checkRemoved(Path fname) { - return removed.get(fname.toString()); - } - /*package-local*/ Record checkRemoved(String fname) { - return removed.get(fname); - } - /*package-local*/ Record checkMerged(Path fname) { - return merged.get(fname.toString()); - } - - - - - /*package-local*/ void dump() { - read(); - @SuppressWarnings("unchecked") - Map[] all = new Map[] { normal, added, removed, merged }; - char[] x = new char[] {'n', 'a', 'r', 'm' }; - for (int i = 0; i < all.length; i++) { - for (Record r : all[i].values()) { - System.out.printf("%c %3o%6d %30tc\t\t%s", x[i], r.mode, r.size, (long) r.time * 1000, r.name1); - if (r.name2 != null) { - System.out.printf(" --> %s", r.name2); - } - System.out.println(); - } - System.out.println(); - } - } - - /*package-local*/ static class Record { - final int mode; - final int size; - final int time; - final String name1; - final String name2; - - public Record(int fmode, int fsize, int ftime, String name1, String name2) { - mode = fmode; - size = fsize; - time = ftime; - this.name1 = name1; - this.name2 = name2; - - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/HgIgnore.java --- a/src/org/tmatesoft/hg/repo/HgIgnore.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,134 +0,0 @@ -/* - * Copyright (c) 2010-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.repo; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.regex.Pattern; - -import org.tmatesoft.hg.util.Path; - -/** - * Handling of ignored paths according to .hgignore configuration - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgIgnore { - - private List entries; - - HgIgnore() { - entries = Collections.emptyList(); - } - - /* package-local */void read(File hgignoreFile) throws IOException { - if (!hgignoreFile.exists()) { - return; - } - ArrayList result = new ArrayList(entries); // start with existing - String syntax = "regex"; // or "glob" - BufferedReader fr = new BufferedReader(new FileReader(hgignoreFile)); - String line; - while ((line = fr.readLine()) != null) { - line = line.trim(); - if (line.startsWith("syntax:")) { - syntax = line.substring("syntax:".length()).trim(); - if (!"regex".equals(syntax) && !"glob".equals(syntax)) { - throw new IllegalStateException(line); - } - } else if (line.length() > 0) { - // shall I account for local paths in the file (i.e. - // back-slashed on windows)? - int x; - if ((x = line.indexOf('#')) >= 0) { - line = line.substring(0, x).trim(); - if (line.length() == 0) { - continue; - } - } - if ("glob".equals(syntax)) { - // hgignore(5) - // (http://www.selenic.com/mercurial/hgignore.5.html) says slashes '\' are escape characters, - // hence no special treatment of Windows path - // however, own attempts make me think '\' on Windows are not treated as escapes - line = glob2regex(line); - } - result.add(Pattern.compile(line)); // case-sensitive - } - } - result.trimToSize(); - entries = result; - } - - // note, #isIgnored(), even if queried for directories and returned positive reply, may still get - // a file from that ignored folder to get examined. Thus, patterns like "bin" shall match not only a folder, - // but any file under that folder as well - // Alternatively, file walker may memorize folder is ignored and uses this information for all nested files. However, - // this approach would require walker (a) return directories (b) provide nesting information. This may become - // troublesome when one walks not over io.File, but Eclipse's IResource or any other custom VFS. - // - // - // might be interesting, although looks like of no direct use in my case - // @see http://stackoverflow.com/questions/1247772/is-there-an-equivalent-of-java-util-regex-for-glob-type-patterns - private String glob2regex(String line) { - assert line.length() > 0; - StringBuilder sb = new StringBuilder(line.length() + 10); - sb.append('^'); // help avoid matcher.find() to match 'bin' pattern in the middle of the filename - int start = 0, end = line.length() - 1; - // '*' at the beginning and end of a line are useless for Pattern - // XXX although how about **.txt - such globs can be seen in a config, are they valid for HgIgnore? - while (start <= end && line.charAt(start) == '*') start++; - while (end > start && line.charAt(end) == '*') end--; - - for (int i = start; i <= end; i++) { - char ch = line.charAt(i); - if (ch == '.' || ch == '\\') { - sb.append('\\'); - } else if (ch == '?') { - // simple '.' substitution might work out, however, more formally - // a char class seems more appropriate to avoid accidentally - // matching a subdirectory with ? char (i.e. /a/b?d against /a/bad, /a/bed and /a/b/d) - // @see http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_13_03 - // quote: "The slash character in a pathname shall be explicitly matched by using one or more slashes in the pattern; - // it shall neither be matched by the asterisk or question-mark special characters nor by a bracket expression" - sb.append("[^/]"); - continue; - } else if (ch == '*') { - sb.append("[^/]*?"); - continue; - } - sb.append(ch); - } - return sb.toString(); - } - - // TODO use PathGlobMatcher - public boolean isIgnored(Path path) { - for (Pattern p : entries) { - if (p.matcher(path).find()) { - return true; - } - } - return false; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/HgInternals.java --- a/src/org/tmatesoft/hg/repo/HgInternals.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,94 +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.repo; - -import static org.tmatesoft.hg.repo.HgRepository.*; - -import java.io.File; -import java.net.InetAddress; -import java.net.UnknownHostException; - -import org.tmatesoft.hg.internal.ConfigFile; -import org.tmatesoft.hg.util.Path; - - -/** - * DO NOT USE THIS CLASS, INTENDED FOR TESTING PURPOSES. - * - * Debug helper, to access otherwise restricted (package-local) methods - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - - */ -public class HgInternals { - - private final HgRepository repo; - - public HgInternals(HgRepository hgRepo) { - repo = hgRepo; - } - - public void dumpDirstate() { - repo.loadDirstate().dump(); - } - - public boolean[] checkIgnored(String... toCheck) { - HgIgnore ignore = repo.getIgnore(); - boolean[] rv = new boolean[toCheck.length]; - for (int i = 0; i < toCheck.length; i++) { - rv[i] = ignore.isIgnored(Path.create(toCheck[i])); - } - return rv; - } - - public File getRepositoryDir() { - return repo.getRepositoryRoot(); - } - - public ConfigFile getRepoConfig() { - return repo.getConfigFile(); - } - - // in fact, need a setter for this anyway, shall move to internal.Internals perhaps? - public String getNextCommitUsername() { - String hgUser = System.getenv("HGUSER"); - if (hgUser != null && hgUser.trim().length() > 0) { - return hgUser.trim(); - } - String configValue = getRepoConfig().getString("ui", "username", null); - if (configValue != null) { - return configValue; - } - String email = System.getenv("EMAIL"); - if (email != null && email.trim().length() > 0) { - return email; - } - String username = System.getProperty("user.name"); - try { - String hostname = InetAddress.getLocalHost().getHostName(); - return username + '@' + hostname; - } catch (UnknownHostException ex) { - return username; - } - } - - // Convenient check of local revision number for validity (not all negative values are wrong as long as we use negative constants) - public static boolean wrongLocalRevision(int rev) { - return rev < 0 && rev != TIP && rev != WORKING_COPY && rev != BAD_REVISION; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/HgLookup.java --- a/src/org/tmatesoft/hg/repo/HgLookup.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2010-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.repo; - -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.HgException; -import org.tmatesoft.hg.internal.ConfigFile; -import org.tmatesoft.hg.internal.DataAccessProvider; -import org.tmatesoft.hg.internal.Internals; - -/** - * Utility methods to find Mercurial repository at a given location - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgLookup { - - private ConfigFile globalCfg; - - public HgRepository detectFromWorkingDir() throws HgException { - return detect(System.getProperty("user.dir")); - } - - public HgRepository detect(String location) throws HgException { - return detect(new File(location)); - } - - // look up in specified location and above - public HgRepository detect(File location) throws HgException { - 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) { - // return invalid repository - return new HgRepository(location.getPath()); - } - try { - String repoPath = repository.getParentFile().getCanonicalPath(); - return new HgRepository(repoPath, repository); - } catch (IOException ex) { - throw new HgException(location.toString(), ex); - } - } - - public HgBundle loadBundle(File location) throws HgException { - if (location == null || !location.canRead()) { - throw new IllegalArgumentException(); - } - return new HgBundle(new DataAccessProvider(), location).link(); - } - - /** - * Try to instantiate remote server. - * @param key either URL or a key from configuration file that points to remote server - * @param hgRepo NOT USED YET local repository that may have extra config, or default remote location - * @return an instance featuring access to remote repository, check {@link HgRemoteRepository#isInvalid()} before actually using it - * @throws HgBadArgumentException if anything is wrong with the remote server's URL - */ - public HgRemoteRepository detectRemote(String key, HgRepository hgRepo) throws HgBadArgumentException { - URL url; - Exception toReport; - try { - url = new URL(key); - toReport = null; - } catch (MalformedURLException ex) { - url = null; - toReport = ex; - } - if (url == null) { - String server = getGlobalConfig().getSection("paths").get(key); - if (server == null) { - throw new HgBadArgumentException(String.format("Can't find server %s specification in the config", key), toReport); - } - try { - url = new URL(server); - } catch (MalformedURLException ex) { - throw new HgBadArgumentException(String.format("Found %s server spec in the config, but failed to initialize with it", key), ex); - } - } - return new HgRemoteRepository(url); - } - - public HgRemoteRepository detect(URL url) throws HgException { - if (url == null) { - throw new IllegalArgumentException(); - } - if (Boolean.FALSE.booleanValue()) { - throw HgRepository.notImplemented(); - } - return new HgRemoteRepository(url); - } - - private ConfigFile getGlobalConfig() { - if (globalCfg == null) { - globalCfg = new Internals().newConfigFile(); - globalCfg.addLocation(new File(System.getProperty("user.home"), ".hgrc")); - } - return globalCfg; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/HgManifest.java --- a/src/org/tmatesoft/hg/repo/HgManifest.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2010-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.repo; - -import java.io.IOException; - -import org.tmatesoft.hg.core.HgBadStateException; -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.internal.DataAccess; -import org.tmatesoft.hg.internal.Pool; -import org.tmatesoft.hg.internal.RevlogStream; - - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgManifest extends Revlog { - - /*package-local*/ HgManifest(HgRepository hgRepo, RevlogStream content) { - super(hgRepo, content); - } - - public void walk(int start, int end, final Inspector inspector) { - if (inspector == null) { - throw new IllegalArgumentException(); - } - content.iterate(start, end, true, new ManifestParser(inspector)); - } - - public interface Inspector { - boolean begin(int revision, Nodeid nid); - boolean next(Nodeid nid, String fname, String flags); - boolean end(int revision); - } - - private static class ManifestParser implements RevlogStream.Inspector { - private boolean gtg = true; // good to go - private final Inspector inspector; - private final Pool nodeidPool; - private final Pool fnamePool; - private final Pool flagsPool; - - public ManifestParser(Inspector delegate) { - assert delegate != null; - inspector = delegate; - nodeidPool = new Pool(); - fnamePool = new Pool(); - flagsPool = new Pool(); - } - - public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) { - if (!gtg) { - return; - } - try { - gtg = gtg && inspector.begin(revisionNumber, new Nodeid(nodeid, true)); - int i; - String fname = null; - String flags = null; - Nodeid nid = null; - byte[] data = da.byteArray(); - for (i = 0; gtg && i < actualLen; i++) { - int x = i; - for( ; data[i] != '\n' && i < actualLen; i++) { - if (fname == null && data[i] == 0) { - fname = fnamePool.unify(new String(data, x, i - x)); - x = i+1; - } - } - if (i < actualLen) { - assert data[i] == '\n'; - int nodeidLen = i - x < 40 ? i-x : 40; - nid = nodeidPool.unify(Nodeid.fromAscii(data, x, nodeidLen)); - if (nodeidLen + x < i) { - // 'x' and 'l' for executable bits and symlinks? - // hg --debug manifest shows 644 for each regular file in my repo - flags = flagsPool.unify(new String(data, x + nodeidLen, i-x-nodeidLen)); - } - gtg = gtg && inspector.next(nid, fname, flags); - } - nid = null; - fname = flags = null; - } - gtg = gtg && inspector.end(revisionNumber); - } catch (IOException ex) { - throw new HgBadStateException(ex); - } - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/HgRemoteRepository.java --- a/src/org/tmatesoft/hg/repo/HgRemoteRepository.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,447 +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.repo; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.StreamTokenizer; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLConnection; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.prefs.BackingStoreException; -import java.util.prefs.Preferences; -import java.util.zip.InflaterInputStream; - -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.X509TrustManager; - -import org.tmatesoft.hg.core.HgBadArgumentException; -import org.tmatesoft.hg.core.HgBadStateException; -import org.tmatesoft.hg.core.HgException; -import org.tmatesoft.hg.core.Nodeid; - -/** - * WORK IN PROGRESS, DO NOT USE - * - * @see http://mercurial.selenic.com/wiki/WireProtocol - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgRemoteRepository { - - private final URL url; - private final SSLContext sslContext; - private final String authInfo; - private final boolean debug = Boolean.parseBoolean(System.getProperty("hg4j.remote.debug")); - private HgLookup lookupHelper; - - HgRemoteRepository(URL url) throws HgBadArgumentException { - if (url == null) { - throw new IllegalArgumentException(); - } - this.url = url; - if ("https".equals(url.getProtocol())) { - try { - sslContext = SSLContext.getInstance("SSL"); - class TrustEveryone implements X509TrustManager { - public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { - if (debug) { - System.out.println("checkClientTrusted:" + authType); - } - } - public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - if (debug) { - System.out.println("checkServerTrusted:" + authType); - } - } - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - }; - sslContext.init(null, new TrustManager[] { new TrustEveryone() }, null); - } catch (Exception ex) { - throw new HgBadArgumentException("Can't initialize secure connection", ex); - } - } else { - sslContext = null; - } - if (url.getUserInfo() != null) { - String ai = null; - try { - // Hack to get Base64-encoded credentials - Preferences tempNode = Preferences.userRoot().node("xxx"); - tempNode.putByteArray("xxx", url.getUserInfo().getBytes()); - ai = tempNode.get("xxx", null); - tempNode.removeNode(); - } catch (BackingStoreException ex) { - ex.printStackTrace(); - // IGNORE - } - authInfo = ai; - } else { - authInfo = null; - } - } - - public boolean isInvalid() throws HgException { - // say hello to server, check response - if (Boolean.FALSE.booleanValue()) { - throw HgRepository.notImplemented(); - } - return false; // FIXME - } - - /** - * @return human-readable address of the server, without user credentials or any other security information - */ - public String getLocation() { - if (url.getUserInfo() == null) { - return url.toExternalForm(); - } - if (url.getPort() != -1) { - return String.format("%s://%s:%d%s", url.getProtocol(), url.getHost(), url.getPort(), url.getPath()); - } else { - return String.format("%s://%s%s", url.getProtocol(), url.getHost(), url.getPath()); - } - } - - public List heads() throws HgException { - try { - URL u = new URL(url, url.getPath() + "?cmd=heads"); - HttpURLConnection c = setupConnection(u.openConnection()); - c.connect(); - if (debug) { - dumpResponseHeader(u, c); - } - InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); - StreamTokenizer st = new StreamTokenizer(is); - st.ordinaryChars('0', '9'); - st.wordChars('0', '9'); - st.eolIsSignificant(false); - LinkedList parseResult = new LinkedList(); - while (st.nextToken() != StreamTokenizer.TT_EOF) { - parseResult.add(Nodeid.fromAscii(st.sval)); - } - return parseResult; - } catch (MalformedURLException ex) { - throw new HgException(ex); - } catch (IOException ex) { - throw new HgException(ex); - } - } - - public List between(Nodeid tip, Nodeid base) throws HgException { - Range r = new Range(base, tip); - // XXX shall handle errors like no range key in the returned map, not sure how. - return between(Collections.singletonList(r)).get(r); - } - - /** - * @param ranges - * @return map, where keys are input instances, values are corresponding server reply - * @throws HgException - */ - public Map> between(Collection ranges) throws HgException { - if (ranges.isEmpty()) { - return Collections.emptyMap(); - } - // if fact, shall do other way round, this method shall send - LinkedHashMap> rv = new LinkedHashMap>(ranges.size() * 4 / 3); - StringBuilder sb = new StringBuilder(20 + ranges.size() * 82); - sb.append("pairs="); - for (Range r : ranges) { - sb.append(r.end.toString()); - sb.append('-'); - sb.append(r.start.toString()); - sb.append('+'); - } - if (sb.charAt(sb.length() - 1) == '+') { - // strip last space - sb.setLength(sb.length() - 1); - } - try { - boolean usePOST = ranges.size() > 3; - URL u = new URL(url, url.getPath() + "?cmd=between" + (usePOST ? "" : '&' + sb.toString())); - HttpURLConnection c = setupConnection(u.openConnection()); - if (usePOST) { - c.setRequestMethod("POST"); - c.setRequestProperty("Content-Length", String.valueOf(sb.length()/*nodeids are ASCII, bytes == characters */)); - c.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - c.setDoOutput(true); - c.connect(); - OutputStream os = c.getOutputStream(); - os.write(sb.toString().getBytes()); - os.flush(); - os.close(); - } else { - c.connect(); - } - if (debug) { - System.out.printf("%d ranges, method:%s \n", ranges.size(), c.getRequestMethod()); - dumpResponseHeader(u, c); - } - InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); - StreamTokenizer st = new StreamTokenizer(is); - st.ordinaryChars('0', '9'); - st.wordChars('0', '9'); - st.eolIsSignificant(true); - Iterator rangeItr = ranges.iterator(); - LinkedList currRangeList = null; - Range currRange = null; - boolean possiblyEmptyNextLine = true; - while (st.nextToken() != StreamTokenizer.TT_EOF) { - if (st.ttype == StreamTokenizer.TT_EOL) { - if (possiblyEmptyNextLine) { - // newline follows newline; - assert currRange == null; - assert currRangeList == null; - if (!rangeItr.hasNext()) { - throw new HgBadStateException(); - } - rv.put(rangeItr.next(), Collections.emptyList()); - } else { - if (currRange == null || currRangeList == null) { - throw new HgBadStateException(); - } - // indicate next range value is needed - currRange = null; - currRangeList = null; - possiblyEmptyNextLine = true; - } - } else { - possiblyEmptyNextLine = false; - if (currRange == null) { - if (!rangeItr.hasNext()) { - throw new HgBadStateException(); - } - currRange = rangeItr.next(); - currRangeList = new LinkedList(); - rv.put(currRange, currRangeList); - } - Nodeid nid = Nodeid.fromAscii(st.sval); - currRangeList.addLast(nid); - } - } - is.close(); - return rv; - } catch (MalformedURLException ex) { - throw new HgException(ex); - } catch (IOException ex) { - throw new HgException(ex); - } - } - - public List branches(List nodes) throws HgException { - StringBuilder sb = new StringBuilder(20 + nodes.size() * 41); - sb.append("nodes="); - for (Nodeid n : nodes) { - sb.append(n.toString()); - sb.append('+'); - } - if (sb.charAt(sb.length() - 1) == '+') { - // strip last space - sb.setLength(sb.length() - 1); - } - try { - URL u = new URL(url, url.getPath() + "?cmd=branches&" + sb.toString()); - HttpURLConnection c = setupConnection(u.openConnection()); - c.connect(); - if (debug) { - dumpResponseHeader(u, c); - } - InputStreamReader is = new InputStreamReader(c.getInputStream(), "US-ASCII"); - StreamTokenizer st = new StreamTokenizer(is); - st.ordinaryChars('0', '9'); - st.wordChars('0', '9'); - st.eolIsSignificant(false); - ArrayList parseResult = new ArrayList(nodes.size() * 4); - while (st.nextToken() != StreamTokenizer.TT_EOF) { - parseResult.add(Nodeid.fromAscii(st.sval)); - } - if (parseResult.size() != nodes.size() * 4) { - throw new HgException(String.format("Bad number of nodeids in result (shall be factor 4), expected %d, got %d", nodes.size()*4, parseResult.size())); - } - ArrayList rv = new ArrayList(nodes.size()); - for (int i = 0; i < nodes.size(); i++) { - RemoteBranch rb = new RemoteBranch(parseResult.get(i*4), parseResult.get(i*4 + 1), parseResult.get(i*4 + 2), parseResult.get(i*4 + 3)); - rv.add(rb); - } - return rv; - } catch (MalformedURLException ex) { - throw new HgException(ex); - } catch (IOException ex) { - throw new HgException(ex); - } - } - - /* - * XXX need to describe behavior when roots arg is empty; our RepositoryComparator code currently returns empty lists when - * no common elements found, which in turn means we need to query changes starting with NULL nodeid. - * - * WireProtocol wiki: roots = a list of the latest nodes on every service side changeset branch that both the client and server know about. - * - * Perhaps, shall be named 'changegroup' - - * Changegroup: - * http://mercurial.selenic.com/wiki/Merge - * http://mercurial.selenic.com/wiki/WireProtocol - * - * according to latter, bundleformat data is sent through zlib - * (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 HgException { - List _roots = roots.isEmpty() ? Collections.singletonList(Nodeid.NULL) : roots; - StringBuilder sb = new StringBuilder(20 + _roots.size() * 41); - sb.append("roots="); - for (Nodeid n : _roots) { - sb.append(n.toString()); - sb.append('+'); - } - if (sb.charAt(sb.length() - 1) == '+') { - // strip last space - sb.setLength(sb.length() - 1); - } - try { - URL u = new URL(url, url.getPath() + "?cmd=changegroup&" + sb.toString()); - HttpURLConnection c = setupConnection(u.openConnection()); - c.connect(); - if (debug) { - dumpResponseHeader(u, c); - } - File tf = writeBundle(c.getInputStream(), false, "HG10GZ" /*didn't see any other that zip*/); - if (debug) { - System.out.printf("Wrote bundle %s for roots %s\n", tf, sb); - } - return getLookupHelper().loadBundle(tf); - } catch (MalformedURLException ex) { - throw new HgException(ex); - } catch (IOException ex) { - throw new HgException(ex); - } - } - - @Override - public String toString() { - return getClass().getSimpleName() + '[' + getLocation() + ']'; - } - - private HgLookup getLookupHelper() { - if (lookupHelper == null) { - lookupHelper = new HgLookup(); - } - return lookupHelper; - } - - private HttpURLConnection setupConnection(URLConnection urlConnection) { - urlConnection.setRequestProperty("User-Agent", "hg4j/0.5.0"); - urlConnection.addRequestProperty("Accept", "application/mercurial-0.1"); - if (authInfo != null) { - urlConnection.addRequestProperty("Authorization", "Basic " + authInfo); - } - if (sslContext != null) { - ((HttpsURLConnection) urlConnection).setSSLSocketFactory(sslContext.getSocketFactory()); - } - return (HttpURLConnection) urlConnection; - } - - private void dumpResponseHeader(URL u, HttpURLConnection c) { - System.out.printf("Query (%d bytes):%s\n", u.getQuery().length(), u.getQuery()); - System.out.println("Response headers:"); - final Map> headerFields = c.getHeaderFields(); - for (String s : headerFields.keySet()) { - System.out.printf("%s: %s\n", s, c.getHeaderField(s)); - } - } - - private static File writeBundle(InputStream is, boolean decompress, String header) throws IOException { - InputStream zipStream = decompress ? new InflaterInputStream(is) : is; - File tf = File.createTempFile("hg-bundle-", null); - FileOutputStream fos = new FileOutputStream(tf); - fos.write(header.getBytes()); - int r; - byte[] buf = new byte[8*1024]; - while ((r = zipStream.read(buf)) != -1) { - fos.write(buf, 0, r); - } - fos.close(); - zipStream.close(); - return tf; - } - - - public static final class Range { - /** - * Root of the range, earlier revision - */ - public final Nodeid start; - /** - * Head of the range, later revision. - */ - public final Nodeid end; - - /** - * @param from - root/base revision - * @param to - head/tip revision - */ - public Range(Nodeid from, Nodeid to) { - start = from; - end = to; - } - } - - public static final class RemoteBranch { - public final Nodeid head, root, p1, p2; - - public RemoteBranch(Nodeid h, Nodeid r, Nodeid parent1, Nodeid parent2) { - head = h; - root = r; - p1 = parent1; - p2 = parent2; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (false == obj instanceof RemoteBranch) { - return false; - } - RemoteBranch o = (RemoteBranch) obj; - return head.equals(o.head) && root.equals(o.root) && (p1 == null && o.p1 == null || p1.equals(o.p1)) && (p2 == null && o.p2 == null || p2.equals(o.p2)); - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/HgRepository.java --- a/src/org/tmatesoft/hg/repo/HgRepository.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,287 +0,0 @@ -/* - * Copyright (c) 2010-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.repo; - -import java.io.File; -import java.io.IOException; -import java.lang.ref.SoftReference; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; - -import org.tmatesoft.hg.internal.ConfigFile; -import org.tmatesoft.hg.internal.DataAccessProvider; -import org.tmatesoft.hg.internal.Filter; -import org.tmatesoft.hg.internal.RelativePathRewrite; -import org.tmatesoft.hg.internal.RequiresFile; -import org.tmatesoft.hg.internal.RevlogStream; -import org.tmatesoft.hg.util.FileIterator; -import org.tmatesoft.hg.util.FileWalker; -import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.PathRewrite; - - - -/** - * Shall be as state-less as possible, all the caching happens outside the repo, in commands/walkers - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public final class HgRepository { - - // if new constants added, consider fixing HgInternals#wrongLocalRevision - public static final int TIP = -3; - public static final int BAD_REVISION = Integer.MIN_VALUE; - public static final int WORKING_COPY = -2; - - // temp aux marker method - public static IllegalStateException notImplemented() { - return new IllegalStateException("Not implemented"); - } - - private final File repoDir; // .hg folder - private final String repoLocation; - private final DataAccessProvider dataAccess; - private final PathRewrite normalizePath; - private final PathRewrite dataPathHelper; - private final PathRewrite repoPathHelper; - - private HgChangelog changelog; - private HgManifest manifest; - private HgTags tags; - // XXX perhaps, shall enable caching explicitly - private final HashMap> streamsCache = new HashMap>(); - - private final org.tmatesoft.hg.internal.Internals impl = new org.tmatesoft.hg.internal.Internals(); - private HgIgnore ignore; - private ConfigFile configFile; - - HgRepository(String repositoryPath) { - repoDir = null; - repoLocation = repositoryPath; - dataAccess = null; - dataPathHelper = repoPathHelper = null; - normalizePath = null; - } - - HgRepository(String repositoryPath, File repositoryRoot) { - assert ".hg".equals(repositoryRoot.getName()) && repositoryRoot.isDirectory(); - assert repositoryPath != null; - assert repositoryRoot != null; - repoDir = repositoryRoot; - repoLocation = repositoryPath; - dataAccess = new DataAccessProvider(); - final boolean runningOnWindows = System.getProperty("os.name").indexOf("Windows") != -1; - if (runningOnWindows) { - normalizePath = new PathRewrite() { - - public String rewrite(String path) { - // TODO handle . and .. (although unlikely to face them from GUI client) - path = path.replace('\\', '/').replace("//", "/"); - if (path.startsWith("/")) { - path = path.substring(1); - } - return path; - } - }; - } else { - normalizePath = new PathRewrite.Empty(); // or strip leading slash, perhaps? - } - parseRequires(); - dataPathHelper = impl.buildDataFilesHelper(); - repoPathHelper = impl.buildRepositoryFilesHelper(); - } - - @Override - public String toString() { - return getClass().getSimpleName() + "[" + getLocation() + (isInvalid() ? "(BAD)" : "") + "]"; - } - - public String getLocation() { - return repoLocation; - } - - public boolean isInvalid() { - return repoDir == null || !repoDir.exists() || !repoDir.isDirectory(); - } - - public HgChangelog getChangelog() { - if (this.changelog == null) { - String storagePath = repoPathHelper.rewrite("00changelog.i"); - RevlogStream content = resolve(Path.create(storagePath), true); - this.changelog = new HgChangelog(this, content); - } - return this.changelog; - } - - public HgManifest getManifest() { - if (this.manifest == null) { - RevlogStream content = resolve(Path.create(repoPathHelper.rewrite("00manifest.i")), true); - this.manifest = new HgManifest(this, content); - } - return this.manifest; - } - - public final HgTags getTags() { - if (tags == null) { - tags = new HgTags(); - try { - tags.readGlobal(new File(repoDir.getParentFile(), ".hgtags")); - tags.readLocal(new File(repoDir, "localtags")); - } catch (IOException ex) { - ex.printStackTrace(); // FIXME log or othewise report - } - } - return tags; - } - - public HgDataFile getFileNode(String path) { - String nPath = normalizePath.rewrite(path); - String storagePath = dataPathHelper.rewrite(nPath); - RevlogStream content = resolve(Path.create(storagePath), false); - Path p = Path.create(nPath); - if (content == null) { - return new HgDataFile(this, p); - } - return new HgDataFile(this, p, content); - } - - public HgDataFile getFileNode(Path path) { - String storagePath = dataPathHelper.rewrite(path.toString()); - RevlogStream content = resolve(Path.create(storagePath), false); - // XXX no content when no file? or HgDataFile.exists() to detect that? - if (content == null) { - return new HgDataFile(this, path); - } - return new HgDataFile(this, path, content); - } - - /* clients need to rewrite path from their FS to a repository-friendly paths, and, perhaps, vice versa*/ - public PathRewrite getToRepoPathHelper() { - return normalizePath; - } - - // local to hide use of io.File. - /*package-local*/ File getRepositoryRoot() { - return repoDir; - } - - // 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) - /*package-local*/ final HgDirstate loadDirstate() { - return new HgDirstate(getDataAccess(), new File(repoDir, "dirstate")); - } - - // package-local, see comment for loadDirstate - /*package-local*/ final HgIgnore getIgnore() { - // TODO read config for additional locations - if (ignore == null) { - ignore = new HgIgnore(); - try { - File ignoreFile = new File(repoDir.getParentFile(), ".hgignore"); - ignore.read(ignoreFile); - } catch (IOException ex) { - ex.printStackTrace(); // log warn - } - } - return ignore; - } - - /*package-local*/ DataAccessProvider getDataAccess() { - return dataAccess; - } - - // FIXME not sure repository shall create walkers - /*package-local*/ FileIterator createWorkingDirWalker() { - File repoRoot = repoDir.getParentFile(); - Path.Source pathSrc = new Path.SimpleSource(new PathRewrite.Composite(new RelativePathRewrite(repoRoot), 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); - } - - /** - * Perhaps, should be separate interface, like ContentLookup - * path - repository storage path (i.e. one usually with .i or .d) - */ - /*package-local*/ RevlogStream resolve(Path path, boolean shallFakeNonExistent) { - final SoftReference ref = streamsCache.get(path); - RevlogStream cached = ref == null ? null : ref.get(); - if (cached != null) { - return cached; - } - File f = new File(repoDir, path.toString()); - if (f.exists()) { - RevlogStream s = new RevlogStream(dataAccess, f); - streamsCache.put(path, new SoftReference(s)); - return s; - } else { - if (shallFakeNonExistent) { - try { - File fake = File.createTempFile(f.getName(), null); - fake.deleteOnExit(); - return new RevlogStream(dataAccess, fake); - } catch (IOException ex) { - ex.printStackTrace(); // FIXME report in debug - } - } - } - return null; // XXX empty stream instead? - } - - // can't expose internal class, otherwise seems reasonable to have it in API - /*package-local*/ ConfigFile getConfigFile() { - if (configFile == null) { - configFile = impl.newConfigFile(); - configFile.addLocation(new File(System.getProperty("user.home"), ".hgrc")); - // last one, overrides anything else - // /.hg/hgrc - configFile.addLocation(new File(getRepositoryRoot(), "hgrc")); - } - return configFile; - } - - /*package-local*/ List getFiltersFromRepoToWorkingDir(Path p) { - return instantiateFilters(p, new Filter.Options(Filter.Direction.FromRepo)); - } - - /*package-local*/ List getFiltersFromWorkingDirToRepo(Path p) { - return instantiateFilters(p, new Filter.Options(Filter.Direction.ToRepo)); - } - - private List instantiateFilters(Path p, Filter.Options opts) { - List factories = impl.getFilters(this, getConfigFile()); - if (factories.isEmpty()) { - return Collections.emptyList(); - } - ArrayList rv = new ArrayList(factories.size()); - for (Filter.Factory ff : factories) { - Filter f = ff.create(p, opts); - if (f != null) { - rv.add(f); - } - } - return rv; - } - - private void parseRequires() { - new RequiresFile().parse(impl, new File(repoDir, "requires")); - } - -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/HgStatusCollector.java --- a/src/org/tmatesoft/hg/repo/HgStatusCollector.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,466 +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.repo; - -import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; -import static org.tmatesoft.hg.repo.HgRepository.TIP; - -import java.util.Collection; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.TreeSet; - -import org.tmatesoft.hg.core.HgDataStreamException; -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.internal.Pool; -import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.PathPool; -import org.tmatesoft.hg.util.PathRewrite; - - -/** - * RevisionWalker? - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgStatusCollector { - - private final HgRepository repo; - private final SortedMap cache; // sparse array, in fact - // with cpython repository, ~70 000 changes, complete Log (direct out, no reverse) output - // no cache limit, no nodeids and fname caching - OOME on changeset 1035 - // 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 final Pool cacheNodes; - private final Pool cacheFilenames; // XXX in fact, need to think if use of PathPool directly instead is better solution - private final ManifestRevisionInspector emptyFakeState; - - - public HgStatusCollector(HgRepository hgRepo) { - this.repo = hgRepo; - cache = new TreeMap(); - cacheNodes = new Pool(); - cacheFilenames = new Pool(); - - emptyFakeState = new ManifestRevisionInspector(null, null); - emptyFakeState.begin(-1, null); - emptyFakeState.end(-1); - } - - public HgRepository getRepo() { - return repo; - } - - private ManifestRevisionInspector get(int rev) { - ManifestRevisionInspector i = cache.get(rev); - if (i == null) { - if (rev == -1) { - return emptyFakeState; - } - while (cache.size() > cacheMaxSize) { - // assume usually we go from oldest to newest, hence remove oldest as most likely to be no longer necessary - cache.remove(cache.firstKey()); - } - i = new ManifestRevisionInspector(cacheNodes, cacheFilenames); - cache.put(rev, i); - repo.getManifest().walk(rev, rev, i); - } - return i; - } - - private boolean cached(int revision) { - return cache.containsKey(revision) || revision == -1; - } - - private void initCacheRange(int minRev, int maxRev) { - while (cache.size() > cacheMaxSize) { - // assume usually we go from oldest to newest, hence remove oldest as most likely to be no longer necessary - cache.remove(cache.firstKey()); - } - repo.getManifest().walk(minRev, maxRev, new HgManifest.Inspector() { - private ManifestRevisionInspector delegate; - private boolean cacheHit; // range may include revisions we already know about, do not re-create them - - public boolean begin(int revision, Nodeid nid) { - assert delegate == null; - if (cache.containsKey(revision)) { // don't need to check emptyFakeState hit as revision never -1 here - cacheHit = true; - } else { - cache.put(revision, delegate = new ManifestRevisionInspector(cacheNodes, cacheFilenames)); - // cache may grow bigger than max size here, but it's ok as present simplistic cache clearing mechanism may - // otherwise remove entries we just added - delegate.begin(revision, nid); - cacheHit = false; - } - return true; - } - - public boolean next(Nodeid nid, String fname, String flags) { - if (!cacheHit) { - delegate.next(nid, fname, flags); - } - return true; - } - - public boolean end(int revision) { - if (!cacheHit) { - delegate.end(revision); - } - cacheHit = false; - delegate = null; - return true; - } - }); - } - - /*package-local*/ ManifestRevisionInspector raw(int rev) { - return get(rev); - } - /*package-local*/ PathPool getPathPool() { - if (pathPool == null) { - pathPool = new PathPool(new PathRewrite.Empty()); - } - return pathPool; - } - - /** - * Allows sharing of a common path cache - */ - public void setPathPool(PathPool pathPool) { - this.pathPool = pathPool; - } - - - // hg status --change - public void change(int rev, HgStatusInspector inspector) { - int[] parents = new int[2]; - repo.getChangelog().parents(rev, parents, null, null); - walk(parents[0], rev, inspector); - } - - // I assume revision numbers are the same for changelog and manifest - here - // user would like to pass changelog revision numbers, and I use them directly to walk manifest. - // if this assumption is wrong, fix this (lookup manifest revisions from changeset). - // rev1 and rev2 may be -1 to indicate comparison to empty repository - // argument order matters - public void walk(int rev1, int rev2, HgStatusInspector inspector) { - if (rev1 == rev2) { - throw new IllegalArgumentException(); - } - if (inspector == null) { - throw new IllegalArgumentException(); - } - if (inspector instanceof Record) { - ((Record) inspector).init(rev1, rev2, this); - } - final int lastManifestRevision = repo.getManifest().getLastRevision(); - if (rev1 == TIP) { - rev1 = lastManifestRevision; - } - if (rev2 == TIP) { - rev2 = lastManifestRevision; - } - // in fact, rev1 and rev2 are often next (or close) to each other, - // thus, we can optimize Manifest reads here (manifest.walk(rev1, rev2)) - ManifestRevisionInspector r1, r2 ; - boolean need1 = !cached(rev1), need2 = !cached(rev2); - if (need1 || need2) { - int minRev, maxRev; - if (need1 && need2 && Math.abs(rev1 - rev2) < 5 /*subjective equivalent of 'close enough'*/) { - minRev = rev1 < rev2 ? rev1 : rev2; - maxRev = minRev == rev1 ? rev2 : rev1; - if (minRev > 0) { - minRev--; // expand range a bit - } - initCacheRange(minRev, maxRev); - need1 = need2 = false; - } - // either both unknown and far from each other, or just one of them. - // read with neighbors to save potential subsequent calls for neighboring elements - // XXX perhaps, if revlog.baseRevision is cheap, shall expand minRev up to baseRevision - // which going to be read anyway - if (need1) { - minRev = rev1; - maxRev = rev1 < lastManifestRevision-5 ? rev1+5 : lastManifestRevision; - initCacheRange(minRev, maxRev); - } - if (need2) { - minRev = rev2; - maxRev = rev2 < lastManifestRevision-5 ? rev2+5 : lastManifestRevision; - initCacheRange(minRev, maxRev); - } - } - r1 = get(rev1); - r2 = get(rev2); - - PathPool pp = getPathPool(); - - TreeSet r1Files = new TreeSet(r1.files()); - for (String fname : r2.files()) { - if (r1Files.remove(fname)) { - Nodeid nidR1 = r1.nodeid(fname); - Nodeid nidR2 = r2.nodeid(fname); - String flagsR1 = r1.flags(fname); - String flagsR2 = r2.flags(fname); - if (nidR1.equals(nidR2) && ((flagsR2 == null && flagsR1 == null) || flagsR2.equals(flagsR1))) { - inspector.clean(pp.path(fname)); - } else { - inspector.modified(pp.path(fname)); - } - } else { - try { - Path copyTarget = pp.path(fname); - Path copyOrigin = getOriginIfCopy(repo, copyTarget, r1Files, rev1); - if (copyOrigin != null) { - inspector.copied(pp.path(copyOrigin) /*pipe through pool, just in case*/, copyTarget); - } else { - inspector.added(copyTarget); - } - } catch (HgDataStreamException ex) { - ex.printStackTrace(); - // FIXME perhaps, shall record this exception to dedicated mediator and continue - // for a single file not to be irresolvable obstacle for a status operation - } - } - } - for (String left : r1Files) { - inspector.removed(pp.path(left)); - } - } - - public Record status(int rev1, int rev2) { - Record rv = new Record(); - walk(rev1, rev2, rv); - return rv; - } - - /*package-local*/static Path getOriginIfCopy(HgRepository hgRepo, Path fname, Collection originals, int originalChangelogRevision) throws HgDataStreamException { - HgDataFile df = hgRepo.getFileNode(fname); - while (df.isCopy()) { - Path original = df.getCopySourceName(); - if (originals.contains(original.toString())) { - df = hgRepo.getFileNode(original); - int changelogRevision = df.getChangesetLocalRevision(0); - if (changelogRevision <= originalChangelogRevision) { - // copy/rename source was known prior to rev1 - // (both r1Files.contains is true and original was created earlier than rev1) - // without r1Files.contains changelogRevision <= rev1 won't suffice as the file - // might get removed somewhere in between (changelogRevision < R < rev1) - return original; - } - break; // copy/rename done later - } - df = hgRepo.getFileNode(original); // try more steps away - } - return null; - } - - // XXX for r1..r2 status, only modified, added, removed (and perhaps, clean) make sense - // XXX Need to specify whether copy targets are in added or not (@see Inspector#copied above) - public static class Record implements HgStatusInspector { - private List modified, added, removed, clean, missing, unknown, ignored; - private Map copied; - - private int startRev, endRev; - private HgStatusCollector statusHelper; - - // XXX StatusCollector may additionally initialize Record instance to speed lookup of changed file revisions - // here I need access to ManifestRevisionInspector via #raw(). Perhaps, non-static class (to get - // implicit reference to StatusCollector) may be better? - // Since users may want to reuse Record instance we've once created (and initialized), we need to - // ensure functionality is correct for each/any call (#walk checks instanceof Record and fixes it up) - // Perhaps, distinct helper (sc.getRevisionHelper().nodeid(fname)) would be better, just not clear - // how to supply [start..end] values there easily - /*package-local*/void init(int startRevision, int endRevision, HgStatusCollector self) { - startRev = startRevision; - endRev = endRevision; - statusHelper = self; - } - - public Nodeid nodeidBeforeChange(Path fname) { - if (statusHelper == null || startRev == BAD_REVISION) { - return null; - } - if ((modified == null || !modified.contains(fname)) && (removed == null || !removed.contains(fname))) { - return null; - } - return statusHelper.raw(startRev).nodeid(fname.toString()); - } - public Nodeid nodeidAfterChange(Path fname) { - if (statusHelper == null || endRev == BAD_REVISION) { - return null; - } - if ((modified == null || !modified.contains(fname)) && (added == null || !added.contains(fname))) { - return null; - } - return statusHelper.raw(endRev).nodeid(fname.toString()); - } - - public List getModified() { - return proper(modified); - } - - public List getAdded() { - return proper(added); - } - - public List getRemoved() { - return proper(removed); - } - - public Map getCopied() { - if (copied == null) { - return Collections.emptyMap(); - } - return Collections.unmodifiableMap(copied); - } - - public List getClean() { - return proper(clean); - } - - public List getMissing() { - return proper(missing); - } - - public List getUnknown() { - return proper(unknown); - } - - public List getIgnored() { - return proper(ignored); - } - - private List proper(List l) { - if (l == null) { - return Collections.emptyList(); - } - return Collections.unmodifiableList(l); - } - - // - // - - public void modified(Path fname) { - modified = doAdd(modified, fname); - } - - public void added(Path fname) { - added = doAdd(added, fname); - } - - public void copied(Path fnameOrigin, Path fnameAdded) { - if (copied == null) { - copied = new LinkedHashMap(); - } - added(fnameAdded); - copied.put(fnameAdded, fnameOrigin); - } - - public void removed(Path fname) { - removed = doAdd(removed, fname); - } - - public void clean(Path fname) { - clean = doAdd(clean, fname); - } - - public void missing(Path fname) { - missing = doAdd(missing, fname); - } - - public void unknown(Path fname) { - unknown = doAdd(unknown, fname); - } - - public void ignored(Path fname) { - ignored = doAdd(ignored, fname); - } - - private static List doAdd(List l, Path p) { - if (l == null) { - l = new LinkedList(); - } - l.add(p); - return l; - } - } - - /*package-local*/ static final class ManifestRevisionInspector implements HgManifest.Inspector { - private final TreeMap idsMap; - private final TreeMap flagsMap; - private final Pool idsPool; - private final Pool namesPool; - - // optional pools for effective management of nodeids and filenames (they are likely - // to be duplicated among different manifest revisions - public ManifestRevisionInspector(Pool nodeidPool, Pool filenamePool) { - idsPool = nodeidPool; - namesPool = filenamePool; - idsMap = new TreeMap(); - flagsMap = new TreeMap(); - } - - public Collection files() { - return idsMap.keySet(); - } - - public Nodeid nodeid(String fname) { - return idsMap.get(fname); - } - - public String flags(String fname) { - return flagsMap.get(fname); - } - - // - - public boolean next(Nodeid nid, String fname, String flags) { - if (namesPool != null) { - fname = namesPool.unify(fname); - } - if (idsPool != null) { - nid = idsPool.unify(nid); - } - idsMap.put(fname, nid); - if (flags != null) { - // TreeMap$Entry takes 32 bytes. No reason to keep null for such price - // Perhaps, Map> might be better solution - flagsMap.put(fname, flags); - } - return true; - } - - public boolean end(int revision) { - // in fact, this class cares about single revision - return false; - } - - public boolean begin(int revision, Nodeid nid) { - return true; - } - } - -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/HgStatusInspector.java --- a/src/org/tmatesoft/hg/repo/HgStatusInspector.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,37 +0,0 @@ -/* - * Copyright (c) 2010-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.repo; - -import org.tmatesoft.hg.util.Path; - -/** - * Callback to get file status information - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public interface HgStatusInspector { - void modified(Path fname); - void added(Path fname); - // XXX need to specify whether StatusCollector invokes added() along with copied or not! - void copied(Path fnameOrigin, Path fnameAdded); // if copied files of no interest, should delegate to self.added(fnameAdded); - void removed(Path fname); - void clean(Path fname); - void missing(Path fname); // aka deleted (tracked by Hg, but not available in FS any more - void unknown(Path fname); // not tracked - void ignored(Path fname); -} \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/HgTags.java --- a/src/org/tmatesoft/hg/repo/HgTags.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,150 +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.repo; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileReader; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -import org.tmatesoft.hg.core.Nodeid; - -/** - * @see http://mercurial.selenic.com/wiki/TagDesign - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgTags { - // global tags come from ".hgtags" - // local come from ".hg/localtags" - - private final Map> globalToName; - private final Map> localToName; - private final Map> globalFromName; - private final Map> localFromName; - - - /*package-local*/ HgTags() { - globalToName = new HashMap>(); - localToName = new HashMap>(); - globalFromName = new TreeMap>(); - localFromName = new TreeMap>(); - } - - /*package-local*/ void readLocal(File localTags) throws IOException { - if (localTags == null || localTags.isDirectory()) { - throw new IllegalArgumentException(String.valueOf(localTags)); - } - read(localTags, localToName, localFromName); - } - - /*package-local*/ void readGlobal(File globalTags) throws IOException { - if (globalTags == null || globalTags.isDirectory()) { - throw new IllegalArgumentException(String.valueOf(globalTags)); - } - read(globalTags, globalToName, globalFromName); - } - - private void read(File f, Map> nid2name, Map> name2nid) throws IOException { - if (!f.canRead()) { - return; - } - BufferedReader r = null; - try { - r = new BufferedReader(new FileReader(f)); - read(r, nid2name, name2nid); - } finally { - if (r != null) { - r.close(); - } - } - } - - private void read(BufferedReader reader, Map> nid2name, Map> name2nid) throws IOException { - String line; - while ((line = reader.readLine()) != null) { - line = line.trim(); - if (line.length() == 0) { - continue; - } - if (line.length() < 40+2 /*nodeid, space and at least single-char tagname*/) { - System.out.println("Bad tags line:" + line); // FIXME log or otherwise report (IStatus analog?) - continue; - } - int spacePos = line.indexOf(' '); - if (spacePos != -1) { - assert spacePos == 40; - final byte[] nodeidBytes = line.substring(0, spacePos).getBytes(); - Nodeid nid = Nodeid.fromAscii(nodeidBytes, 0, nodeidBytes.length); - String tagName = line.substring(spacePos+1); - List nids = name2nid.get(tagName); - if (nids == null) { - nids = new LinkedList(); - // tagName is substring of full line, thus need a copy to let the line be GC'ed - // new String(tagName.toCharArray()) is more expressive, but results in 1 extra arraycopy - tagName = new String(tagName); - name2nid.put(tagName, nids); - } - // XXX repo.getNodeidCache().nodeid(nid); - ((LinkedList) nids).addFirst(nid); - List revTags = nid2name.get(nid); - if (revTags == null) { - revTags = new LinkedList(); - nid2name.put(nid, revTags); - } - revTags.add(tagName); - } else { - System.out.println("Bad tags line:" + line); // FIXME see above - } - } - } - - public List tags(Nodeid nid) { - ArrayList rv = new ArrayList(5); - List l; - if ((l = localToName.get(nid)) != null) { - rv.addAll(l); - } - if ((l = globalToName.get(nid)) != null) { - rv.addAll(l); - } - return rv; - } - - public boolean isTagged(Nodeid nid) { - return localToName.containsKey(nid) || globalToName.containsKey(nid); - } - - public List tagged(String tagName) { - ArrayList rv = new ArrayList(5); - List l; - if ((l = localFromName.get(tagName)) != null) { - rv.addAll(l); - } - if ((l = globalFromName.get(tagName)) != null) { - rv.addAll(l); - } - return rv; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java --- a/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,337 +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.repo; - -import static java.lang.Math.max; -import static java.lang.Math.min; -import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; -import static org.tmatesoft.hg.repo.HgRepository.TIP; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.util.Collections; -import java.util.Set; -import java.util.TreeSet; - -import org.tmatesoft.hg.core.HgDataStreamException; -import org.tmatesoft.hg.core.HgException; -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.internal.ByteArrayChannel; -import org.tmatesoft.hg.internal.FilterByteChannel; -import org.tmatesoft.hg.repo.HgStatusCollector.ManifestRevisionInspector; -import org.tmatesoft.hg.util.ByteChannel; -import org.tmatesoft.hg.util.CancelledException; -import org.tmatesoft.hg.util.FileIterator; -import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.PathPool; -import org.tmatesoft.hg.util.PathRewrite; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class HgWorkingCopyStatusCollector { - - private final HgRepository repo; - private final FileIterator repoWalker; - private HgDirstate dirstate; - private HgStatusCollector baseRevisionCollector; - private PathPool pathPool; - - public HgWorkingCopyStatusCollector(HgRepository hgRepo) { - this(hgRepo, hgRepo.createWorkingDirWalker()); - } - - HgWorkingCopyStatusCollector(HgRepository hgRepo, FileIterator hgRepoWalker) { - this.repo = hgRepo; - this.repoWalker = hgRepoWalker; - } - - /** - * Optionally, supply a collector instance that may cache (or have already cached) base revision - * @param sc may be null - */ - public void setBaseRevisionCollector(HgStatusCollector sc) { - baseRevisionCollector = sc; - } - - /*package-local*/ PathPool getPathPool() { - if (pathPool == null) { - if (baseRevisionCollector == null) { - pathPool = new PathPool(new PathRewrite.Empty()); - } else { - return baseRevisionCollector.getPathPool(); - } - } - return pathPool; - } - - public void setPathPool(PathPool pathPool) { - this.pathPool = pathPool; - } - - - private HgDirstate getDirstate() { - if (dirstate == null) { - dirstate = repo.loadDirstate(); - } - return dirstate; - } - - // may be invoked few times - public void walk(int baseRevision, HgStatusInspector inspector) { - final HgIgnore hgIgnore = repo.getIgnore(); - TreeSet knownEntries = getDirstate().all(); - final boolean isTipBase; - if (baseRevision == TIP) { - baseRevision = repo.getManifest().getRevisionCount() - 1; - isTipBase = true; - } else { - isTipBase = baseRevision == repo.getManifest().getRevisionCount() - 1; - } - HgStatusCollector.ManifestRevisionInspector collect = null; - Set baseRevFiles = Collections.emptySet(); - if (!isTipBase) { - if (baseRevisionCollector != null) { - collect = baseRevisionCollector.raw(baseRevision); - } else { - collect = new HgStatusCollector.ManifestRevisionInspector(null, null); - repo.getManifest().walk(baseRevision, baseRevision, collect); - } - baseRevFiles = new TreeSet(collect.files()); - } - if (inspector instanceof HgStatusCollector.Record) { - HgStatusCollector sc = baseRevisionCollector == null ? new HgStatusCollector(repo) : baseRevisionCollector; - ((HgStatusCollector.Record) inspector).init(baseRevision, BAD_REVISION, sc); - } - repoWalker.reset(); - final PathPool pp = getPathPool(); - while (repoWalker.hasNext()) { - repoWalker.next(); - Path fname = repoWalker.name(); - File f = repoWalker.file(); - if (hgIgnore.isIgnored(fname)) { - inspector.ignored(pp.path(fname)); - } else if (knownEntries.remove(fname.toString())) { - // modified, added, removed, clean - if (collect != null) { // need to check against base revision, not FS file - checkLocalStatusAgainstBaseRevision(baseRevFiles, collect, baseRevision, fname, f, inspector); - baseRevFiles.remove(fname.toString()); - } else { - checkLocalStatusAgainstFile(fname, f, inspector); - } - } else { - inspector.unknown(pp.path(fname)); - } - } - if (collect != null) { - for (String r : baseRevFiles) { - inspector.removed(pp.path(r)); - } - } - for (String m : knownEntries) { - // missing known file from a working dir - if (getDirstate().checkRemoved(m) == null) { - // not removed from the repository = 'deleted' - inspector.missing(pp.path(m)); - } else { - // removed from the repo - // if we check against non-tip revision, do not report files that were added past that revision and now removed. - if (collect == null || baseRevFiles.contains(m)) { - inspector.removed(pp.path(m)); - } - } - } - } - - public HgStatusCollector.Record status(int baseRevision) { - HgStatusCollector.Record rv = new HgStatusCollector.Record(); - walk(baseRevision, rv); - return rv; - } - - //******************************************** - - - private void checkLocalStatusAgainstFile(Path fname, File f, HgStatusInspector inspector) { - HgDirstate.Record r; - if ((r = getDirstate().checkNormal(fname)) != null) { - // either clean or modified - if (f.lastModified() / 1000 == r.time && r.size == f.length()) { - inspector.clean(getPathPool().path(fname)); - } else { - // check actual content to avoid false modified files - HgDataFile df = repo.getFileNode(fname); - if (!areTheSame(f, df, HgRepository.TIP)) { - inspector.modified(df.getPath()); - } else { - inspector.clean(df.getPath()); - } - } - } else if ((r = getDirstate().checkAdded(fname)) != null) { - if (r.name2 == null) { - inspector.added(getPathPool().path(fname)); - } else { - inspector.copied(getPathPool().path(r.name2), getPathPool().path(fname)); - } - } else if ((r = getDirstate().checkRemoved(fname)) != null) { - inspector.removed(getPathPool().path(fname)); - } else if ((r = getDirstate().checkMerged(fname)) != null) { - inspector.modified(getPathPool().path(fname)); - } - } - - // XXX refactor checkLocalStatus methods in more OO way - private void checkLocalStatusAgainstBaseRevision(Set baseRevNames, ManifestRevisionInspector collect, int baseRevision, Path fname, File f, HgStatusInspector inspector) { - // fname is in the dirstate, either Normal, Added, Removed or Merged - Nodeid nid1 = collect.nodeid(fname.toString()); - String flags = collect.flags(fname.toString()); - HgDirstate.Record r; - if (nid1 == null) { - // normal: added? - // added: not known at the time of baseRevision, shall report - // merged: was not known, report as added? - if ((r = getDirstate().checkNormal(fname)) != null) { - try { - Path origin = HgStatusCollector.getOriginIfCopy(repo, fname, baseRevNames, baseRevision); - if (origin != null) { - inspector.copied(getPathPool().path(origin), getPathPool().path(fname)); - return; - } - } catch (HgDataStreamException ex) { - ex.printStackTrace(); - // FIXME report to a mediator, continue status collection - } - } else if ((r = getDirstate().checkAdded(fname)) != null) { - if (r.name2 != null && baseRevNames.contains(r.name2)) { - baseRevNames.remove(r.name2); // XXX surely I shall not report rename source as Removed? - inspector.copied(getPathPool().path(r.name2), getPathPool().path(fname)); - return; - } - // fall-through, report as added - } else if (getDirstate().checkRemoved(fname) != null) { - // removed: removed file was not known at the time of baseRevision, and we should not report it as removed - return; - } - inspector.added(getPathPool().path(fname)); - } else { - // was known; check whether clean or modified - // when added - seems to be the case of a file added once again, hence need to check if content is different - if ((r = getDirstate().checkNormal(fname)) != null || (r = getDirstate().checkMerged(fname)) != null || (r = getDirstate().checkAdded(fname)) != null) { - // either clean or modified - HgDataFile fileNode = repo.getFileNode(fname); - final int lengthAtRevision = fileNode.length(nid1); - if (r.size /* XXX File.length() ?! */ != lengthAtRevision || flags != todoGenerateFlags(fname /*java.io.File*/)) { - inspector.modified(getPathPool().path(fname)); - } else { - // check actual content to see actual changes - if (areTheSame(f, fileNode, fileNode.getLocalRevision(nid1))) { - inspector.clean(getPathPool().path(fname)); - } else { - inspector.modified(getPathPool().path(fname)); - } - } - } - // only those left in idsMap after processing are reported as removed - } - - // TODO think over if content comparison may be done more effectively by e.g. calculating nodeid for a local file and comparing it with nodeid from manifest - // we don't need to tell exact difference, hash should be enough to detect difference, and it doesn't involve reading historical file content, and it's relatively - // cheap to calc hash on a file (no need to keep it completely in memory). OTOH, if I'm right that the next approach is used for nodeids: - // changeset nodeid + hash(actual content) => entry (Nodeid) in the next Manifest - // then it's sufficient to check parents from dirstate, and if they do not match parents from file's baseRevision (non matching parents means different nodeids). - // The question is whether original Hg treats this case (same content, different parents and hence nodeids) as 'modified' or 'clean' - } - - private boolean areTheSame(File f, HgDataFile dataFile, int localRevision) { - // XXX consider adding HgDataDile.compare(File/byte[]/whatever) operation to optimize comparison - ByteArrayChannel bac = new ByteArrayChannel(); - boolean ioFailed = false; - try { - // need content with metadata striped off - although theoretically chances are metadata may be different, - // WC doesn't have it anyway - dataFile.content(localRevision, bac); - } catch (CancelledException ex) { - // silently ignore - can't happen, ByteArrayChannel is not cancellable - } catch (IOException ex) { - ioFailed = true; - } catch (HgException ex) { - ioFailed = true; - } - return !ioFailed && areTheSame(f, bac.toArray(), dataFile.getPath()); - } - - private boolean areTheSame(File f, final byte[] data, Path p) { - FileInputStream fis = null; - try { - try { - fis = new FileInputStream(f); - FileChannel fc = fis.getChannel(); - ByteBuffer fb = ByteBuffer.allocate(min(data.length, 8192)); - final boolean[] checkValue = new boolean[] { true }; - ByteChannel check = new ByteChannel() { - int x = 0; - final boolean debug = false; // XXX may want to add global variable to allow clients to turn - public int write(ByteBuffer buffer) { - for (int i = buffer.remaining(); i > 0; i--, x++) { - if (data[x] != buffer.get()) { - if (debug) { - byte[] xx = new byte[15]; - if (buffer.position() > 5) { - buffer.position(buffer.position() - 5); - } - buffer.get(xx); - System.out.print("expected >>" + new String(data, max(0, x - 4), 20) + "<< but got >>"); - System.out.println(new String(xx) + "<<"); - } - checkValue[0] = false; - break; - } - } - buffer.position(buffer.limit()); // mark as read - return buffer.limit(); - } - }; - FilterByteChannel filters = new FilterByteChannel(check, repo.getFiltersFromWorkingDirToRepo(p)); - while (fc.read(fb) != -1 && checkValue[0]) { - fb.flip(); - filters.write(fb); - fb.compact(); - } - return checkValue[0]; - } catch (IOException ex) { - if (fis != null) { - fis.close(); - } - ex.printStackTrace(); // log warn - } - } catch (/*TODO typed*/Exception ex) { - ex.printStackTrace(); - } - return false; - } - - private static String todoGenerateFlags(Path fname) { - // FIXME implement - return null; - } - -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/Revlog.java --- a/src/org/tmatesoft/hg/repo/Revlog.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,447 +0,0 @@ -/* - * Copyright (c) 2010-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.repo; - -import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; -import static org.tmatesoft.hg.repo.HgRepository.TIP; - -import java.io.IOException; -import java.nio.ByteBuffer; -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.Nodeid; -import org.tmatesoft.hg.internal.DataAccess; -import org.tmatesoft.hg.internal.RevlogStream; -import org.tmatesoft.hg.util.ByteChannel; -import org.tmatesoft.hg.util.CancelSupport; -import org.tmatesoft.hg.util.CancelledException; -import org.tmatesoft.hg.util.ProgressSupport; - - -/** - * Base class for all Mercurial entities that are serialized in a so called revlog format (changelog, manifest, data files). - * - * Implementation note: - * Hides actual actual revlog stream implementation and its access methods (i.e. RevlogStream.Inspector), iow shall not expose anything internal - * in public methods. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -abstract class Revlog { - - private final HgRepository repo; - protected final RevlogStream content; - - protected Revlog(HgRepository hgRepo, RevlogStream contentStream) { - if (hgRepo == null) { - throw new IllegalArgumentException(); - } - if (contentStream == null) { - throw new IllegalArgumentException(); - } - repo = hgRepo; - content = contentStream; - } - - // invalid Revlog - protected Revlog(HgRepository hgRepo) { - repo = hgRepo; - content = null; - } - - public final HgRepository getRepo() { - return repo; - } - - public final int getRevisionCount() { - return content.revisionCount(); - } - - public final int getLastRevision() { - return content.revisionCount() - 1; - } - - public final Nodeid getRevision(int revision) { - // XXX cache nodeids? - return Nodeid.fromBinary(content.nodeid(revision), 0); - } - - public final int getLocalRevision(Nodeid nid) { - int revision = content.findLocalRevisionNumber(nid); - if (revision == BAD_REVISION) { - throw new IllegalArgumentException(String.format("%s doesn't represent a revision of %s", nid.toString(), this /*XXX HgDataFile.getPath might be more suitable here*/)); - } - return revision; - } - - // Till now, i follow approach that NULL nodeid is never part of revlog - public final boolean isKnown(Nodeid nodeid) { - final int rn = content.findLocalRevisionNumber(nodeid); - if (Integer.MIN_VALUE == rn) { - return false; - } - if (rn < 0 || rn >= content.revisionCount()) { - // Sanity check - throw new IllegalStateException(); - } - 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 - */ - protected void rawContent(Nodeid nodeid, ByteChannel sink) throws HgException, IOException, CancelledException { - rawContent(getLocalRevision(nodeid), sink); - } - - /** - * @param revision - repo-local index of this file change (not a changelog revision number!) - */ - protected void rawContent(int revision, ByteChannel sink) throws HgException, IOException, CancelledException { - if (sink == null) { - throw new IllegalArgumentException(); - } - ContentPipe insp = new ContentPipe(sink, 0); - insp.checkCancelled(); - content.iterate(revision, revision, true, insp); - insp.checkFailed(); - } - - /** - * XXX perhaps, return value Nodeid[2] and boolean needNodeids is better (and higher level) API for this query? - * - * @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 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 - * @return - */ - public void parents(int revision, int[] parentRevisions, byte[] parent1, byte[] parent2) { - if (revision != TIP && !(revision >= 0 && revision < content.revisionCount())) { - throw new IllegalArgumentException(String.valueOf(revision)); - } - if (parentRevisions == null || parentRevisions.length < 2) { - throw new IllegalArgumentException(String.valueOf(parentRevisions)); - } - if (parent1 != null && parent1.length < 20) { - throw new IllegalArgumentException(parent1.toString()); - } - if (parent2 != null && parent2.length < 20) { - throw new IllegalArgumentException(parent2.toString()); - } - class ParentCollector implements RevlogStream.Inspector { - public int p1 = -1; - public int p2 = -1; - public byte[] nodeid; - - public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) { - p1 = parent1Revision; - p2 = parent2Revision; - this.nodeid = new byte[20]; - // nodeid arg now comes in 32 byte from (as in file format description), however upper 12 bytes are zeros. - System.arraycopy(nodeid, nodeid.length > 20 ? nodeid.length - 20 : 0, this.nodeid, 0, 20); - } - }; - ParentCollector pc = new ParentCollector(); - content.iterate(revision, revision, false, pc); - parentRevisions[0] = pc.p1; - parentRevisions[1] = pc.p2; - if (parent1 != null) { - if (parentRevisions[0] == -1) { - Arrays.fill(parent1, 0, 20, (byte) 0); - } else { - content.iterate(parentRevisions[0], parentRevisions[0], false, pc); - System.arraycopy(pc.nodeid, 0, parent1, 0, 20); - } - } - if (parent2 != null) { - if (parentRevisions[1] == -1) { - Arrays.fill(parent2, 0, 20, (byte) 0); - } else { - content.iterate(parentRevisions[1], parentRevisions[1], false, pc); - System.arraycopy(pc.nodeid, 0, parent2, 0, 20); - } - } - } - - /* - * 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 { - - - 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 init() { - final RevlogStream stream = Revlog.this.content; - final int revisionCount = stream.revisionCount(); - firstParent = new Nodeid[revisionCount]; - // although branches/merges are less frequent, and most of secondParent would be -1/null, some sort of - // SparseOrderedList might be handy, provided its inner structures do not overweight simplicity of an array - secondParent = new Nodeid[revisionCount]; - // - sequential = new Nodeid[revisionCount]; - sorted = new Nodeid[revisionCount]; - - RevlogStream.Inspector insp = new RevlogStream.Inspector() { - - int ix = 0; - public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) { - if (ix != revisionNumber) { - // XXX temp code, just to make sure I understand what's going on here - throw new IllegalStateException(); - } - if (parent1Revision >= revisionNumber || parent2Revision >= revisionNumber) { - throw new IllegalStateException(); // sanity, revisions are sequential - } - final Nodeid nid = new Nodeid(nodeid, true); - sequential[ix] = sorted[ix] = nid; - if (parent1Revision != -1) { - assert parent1Revision < ix; - firstParent[ix] = sequential[parent1Revision]; - } - if (parent2Revision != -1) { // revlog of DataAccess.java has p2 set when p1 is -1 - assert parent2Revision < ix; - secondParent[ix] = sequential[parent2Revision]; - } - ix++; - } - }; - stream.iterate(0, TIP, false, insp); - 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(); - } - } - - // FIXME need to decide whether Nodeid(00 * 20) is always known or not - // right now Nodeid.NULL is not recognized as known if passed to this method, - // caller is supposed to make explicit check - 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; - } - - /** - * @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++) { - // FIXME 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 static class ContentPipe implements RevlogStream.Inspector, CancelSupport { - private final ByteChannel sink; - private final CancelSupport cancelSupport; - private Exception failure; - private final int offset; - - /** - * @param _sink - cannot be null - * @param seekOffset - when positive, orders to pipe bytes to the sink starting from specified offset, not from the first byte available in DataAccess - */ - public ContentPipe(ByteChannel _sink, int seekOffset) { - assert _sink != null; - sink = _sink; - cancelSupport = CancelSupport.Factory.get(_sink); - offset = seekOffset; - } - - protected void prepare(int revisionNumber, DataAccess da) throws HgException, IOException { - if (offset > 0) { // save few useless reset/rewind operations - da.seek(offset); - } - } - - public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) { - try { - prepare(revisionNumber, da); // XXX perhaps, prepare shall return DA (sliced, if needed) - final ProgressSupport progressSupport = ProgressSupport.Factory.get(sink); - ByteBuffer buf = ByteBuffer.allocate(512); - progressSupport.start(da.length()); - while (!da.isEmpty()) { - cancelSupport.checkCancelled(); - da.readBytes(buf); - buf.flip(); - // XXX I may not rely on returned number of bytes but track change in buf position instead. - int consumed = sink.write(buf); - // FIXME in fact, bad sink implementation (that consumes no bytes) would result in endless loop. Need to account for this - buf.compact(); - progressSupport.worked(consumed); - } - progressSupport.done(); // XXX shall specify whether #done() is invoked always or only if completed successfully. - } catch (IOException ex) { - recordFailure(ex); - } catch (CancelledException ex) { - recordFailure(ex); - } catch (HgException ex) { - recordFailure(ex); - } - } - - public void checkCancelled() throws CancelledException { - cancelSupport.checkCancelled(); - } - - protected void recordFailure(Exception ex) { - assert failure == null; - failure = ex; - } - - public void checkFailed() throws HgException, IOException, CancelledException { - if (failure == null) { - return; - } - if (failure instanceof IOException) { - throw (IOException) failure; - } - if (failure instanceof CancelledException) { - throw (CancelledException) failure; - } - if (failure instanceof HgException) { - throw (HgException) failure; - } - throw new HgBadStateException(failure); - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/repo/package.html --- a/src/org/tmatesoft/hg/repo/package.html Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,5 +0,0 @@ - - -Low-level API operations - - \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/util/Adaptable.java --- a/src/org/tmatesoft/hg/util/Adaptable.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,28 +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; - -/** - * Extension mechanism. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public interface Adaptable { - - T getAdapter(Class adapterClass); -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/util/ByteChannel.java --- a/src/org/tmatesoft/hg/util/ByteChannel.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,35 +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.io.IOException; -import java.nio.ByteBuffer; - -/** - * Much like {@link java.nio.channels.WritableByteChannel} except for thrown exception - * - * XXX Perhaps, we'll add CharChannel in the future to deal with character conversions/encodings - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public interface ByteChannel { - // XXX does int return value makes any sense given buffer keeps its read state - // not clear what retvalue should be in case some filtering happened inside write - i.e. return - // number of bytes consumed in - int write(ByteBuffer buffer) throws IOException, CancelledException; -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/util/CancelSupport.java --- a/src/org/tmatesoft/hg/util/CancelSupport.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,61 +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; - -/** - * Mix-in for objects that support cancellation. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public interface CancelSupport { - - /** - * This method is invoked to check if target had been brought to canceled state. Shall silently return if target is - * in regular state. - * @throws CancelledException when target internal state has been changed to canceled. - */ - void checkCancelled() throws CancelledException; - - - // Yeah, this factory class looks silly now, but perhaps in the future I'll need wrappers for other cancellation sources? - // just don't want to have general Utils class with methods like get() below - static class Factory { - - /** - * Obtain non-null cancel support object. - * - * @param target any object (or null) that might have cancel support - * @return target if it's capable checking cancellation status or no-op implementation that never cancels. - */ - public static CancelSupport get(Object target) { - if (target instanceof CancelSupport) { - return (CancelSupport) target; - } - if (target instanceof Adaptable) { - CancelSupport cs = ((Adaptable) target).getAdapter(CancelSupport.class); - if (cs != null) { - return cs; - } - } - return new CancelSupport() { - public void checkCancelled() { - } - }; - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/util/CancelledException.java --- a/src/org/tmatesoft/hg/util/CancelledException.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,29 +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; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -@SuppressWarnings("serial") -public class CancelledException extends Exception { - - public CancelledException() { - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/util/FileIterator.java --- a/src/org/tmatesoft/hg/util/FileIterator.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,53 +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.io.File; - -/** - * Abstracts iteration over file system. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public interface FileIterator { - - /** - * Brings iterator into initial state to facilitate new use. - */ - void reset(); - - /** - * @return whether can shift to next element - */ - boolean hasNext(); - - /** - * Shift to next element - */ - void next(); - - /** - * @return repository-local path to the current element. - */ - Path name(); - - /** - * @return filesystem element. - */ - File file(); -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/util/FileWalker.java --- a/src/org/tmatesoft/hg/util/FileWalker.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,100 +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.io.File; -import java.util.LinkedList; -import java.util.NoSuchElementException; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class FileWalker implements FileIterator { - - private final File startDir; - private final Path.Source pathHelper; - private final LinkedList dirQueue; - private final LinkedList fileQueue; - private File nextFile; - private Path nextPath; - - public FileWalker(File dir, Path.Source pathFactory) { - startDir = dir; - pathHelper = pathFactory; - dirQueue = new LinkedList(); - fileQueue = new LinkedList(); - reset(); - } - - public void reset() { - fileQueue.clear(); - dirQueue.clear(); - dirQueue.add(startDir); - nextFile = null; - nextPath = null; - } - - public boolean hasNext() { - return fill(); - } - - public void next() { - if (!fill()) { - throw new NoSuchElementException(); - } - nextFile = fileQueue.removeFirst(); - nextPath = pathHelper.path(nextFile.getPath()); - } - - public Path name() { - return nextPath; - } - - public File file() { - return nextFile; - } - - private File[] listFiles(File f) { - // in case we need to solve os-related file issues (mac with some encodings?) - return f.listFiles(); - } - - // return true when fill added any elements to fileQueue. - private boolean fill() { - while (fileQueue.isEmpty()) { - if (dirQueue.isEmpty()) { - return false; - } - while (!dirQueue.isEmpty()) { - File dir = dirQueue.removeFirst(); - for (File f : listFiles(dir)) { - if (f.isDirectory()) { - if (!".hg".equals(f.getName())) { - dirQueue.addLast(f); - } - } else { - fileQueue.addLast(f); - } - } - break; - } - } - return !fileQueue.isEmpty(); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/util/Path.java --- a/src/org/tmatesoft/hg/util/Path.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,121 +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; - -/** - * Identify repository files (not String nor io.File). Convenient for pattern matching. Memory-friendly. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public final class Path implements CharSequence, Comparable/*Cloneable? - although clone for paths make no sense*/{ -// private String[] segments; -// private int flags; // dir, unparsed - private String path; - - /*package-local*/Path(String p) { - path = p; - } - - /** - * Check if this is directory's path. - * Note, this method doesn't perform any file system operation. - * - * @return true when this path points to a directory - */ - public boolean isDirectory() { - // XXX simple logic for now. Later we may decide to have an explicit factory method to create directory paths - return path.charAt(path.length() - 1) == '/'; - } - - public int length() { - return path.length(); - } - - public char charAt(int index) { - return path.charAt(index); - } - - public CharSequence subSequence(int start, int end) { - // new Path if start-end matches boundaries of any subpath - return path.substring(start, end); - } - - @Override - public String toString() { - return path; // CharSequence demands toString() impl - } - - public int compareTo(Path o) { - return path.compareTo(o.path); - } - - @Override - public boolean equals(Object obj) { - if (obj != null && getClass() == obj.getClass()) { - return this == obj || path.equals(((Path) obj).path); - } - return false; - } - @Override - public int hashCode() { - return path.hashCode(); - } - - public static Path create(String path) { - if (path == null) { - throw new IllegalArgumentException(); - } - if (path.indexOf('\\') != -1) { - throw new IllegalArgumentException(); - } - Path rv = new Path(path); - return rv; - } - - /** - * Path filter. - */ - public interface Matcher { - boolean accept(Path path); - } - - /** - * Factory for paths - */ - public interface Source { - Path path(String p); - } - - /** - * Straightforward {@link Source} implementation that creates new Path instance for each supplied string - */ - public static class SimpleSource implements Source { - private final PathRewrite normalizer; - - public SimpleSource(PathRewrite pathRewrite) { - if (pathRewrite == null) { - throw new IllegalArgumentException(); - } - normalizer = pathRewrite; - } - - public Path path(String p) { - return Path.create(normalizer.rewrite(p)); - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/util/PathPool.java --- a/src/org/tmatesoft/hg/util/PathPool.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,81 +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); - return get(p, true); - } - - // pipes path object through cache to reuse instance, if possible - 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); - } - - 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 edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/util/PathRewrite.java --- a/src/org/tmatesoft/hg/util/PathRewrite.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,61 +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.util.LinkedList; -import java.util.List; - -/** - * File names often need transformations, like Windows-style path to Unix or human-readable data file name to storage location. - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public interface PathRewrite { - - // XXX think over CharSequence use instead of String - public String rewrite(String path); - - public static class Empty implements PathRewrite { - public String rewrite(String path) { - return path; - } - } - - public class Composite implements PathRewrite { - private List chain; - - public Composite(PathRewrite... e) { - LinkedList r = new LinkedList(); - for (int i = 0; e != null && i < e.length; i++) { - r.addLast(e[i]); - } - chain = r; - } - public Composite chain(PathRewrite e) { - chain.add(e); - return this; - } - - public String rewrite(String path) { - for (PathRewrite pr : chain) { - path = pr.rewrite(path); - } - return path; - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 src/org/tmatesoft/hg/util/ProgressSupport.java --- a/src/org/tmatesoft/hg/util/ProgressSupport.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,57 +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; - -/** - * Mix-in to report progress of a long-running operation - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public interface ProgressSupport { - - public void start(long totalUnits); - public void worked(int units); - public void done(); - - static class Factory { - - /** - * @param target object that might be capable to report progress. Can be null - * @return support object extracted from target or an empty, no-op implementation - */ - public static ProgressSupport get(Object target) { - if (target instanceof ProgressSupport) { - return (ProgressSupport) target; - } - if (target instanceof Adaptable) { - ProgressSupport ps = ((Adaptable) target).getAdapter(ProgressSupport.class); - if (ps != null) { - return ps; - } - } - return new ProgressSupport() { - public void start(long totalUnits) { - } - public void worked(int units) { - } - public void done() { - } - }; - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 test-data/test-repos.jar Binary file test-data/test-repos.jar has changed diff -r edb2e2829352 -r 6ec4af642ba8 test/org/tmatesoft/hg/test/Configuration.java --- a/test/org/tmatesoft/hg/test/Configuration.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,101 +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.test; - -import static org.junit.Assert.*; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.tmatesoft.hg.repo.HgLookup; -import org.tmatesoft.hg.repo.HgRemoteRepository; -import org.tmatesoft.hg.repo.HgRepository; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class Configuration { - - private static Configuration inst; - private File root; - private final HgLookup lookup; - private File tempDir; - private List remoteServers; - - private Configuration() { - lookup = new HgLookup(); - } - - private File getRoot() { - if (root == null) { - String repo2 = System.getProperty("hg4j.tests.repos"); - assertNotNull("System property hg4j.tests.repos is undefined", repo2); - root = new File(repo2); - assertTrue(root.exists()); - } - return root; - } - - public static Configuration get() { - if (inst == null) { - inst = new Configuration(); - } - return inst; - } - - public HgRepository own() throws Exception { - return lookup.detectFromWorkingDir(); - } - - // fails if repo not found - public HgRepository find(String key) throws Exception { - HgRepository rv = lookup.detect(new File(getRoot(), key)); - assertNotNull(rv); - assertFalse(rv.isInvalid()); - return rv; - } - - // easy override for manual test runs - public void remoteServers(String... keys) { - remoteServers = Arrays.asList(keys); - } - - public List allRemote() throws Exception { - if (remoteServers == null) { - String rr = System.getProperty("hg4j.tests.remote"); - assertNotNull("System property hg4j.tests.remote is undefined", rr); - remoteServers = Arrays.asList(rr.split(" ")); - } - ArrayList rv = new ArrayList(remoteServers.size()); - for (String key : remoteServers) { - rv.add(lookup.detectRemote(key, null)); - } - return rv; - } - - public File getTempDir() { - if (tempDir == null) { - String td = System.getProperty("hg4j.tests.tmpdir", System.getProperty("java.io.tmpdir")); - tempDir = new File(td); - } - return tempDir; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 test/org/tmatesoft/hg/test/ErrorCollectorExt.java --- a/test/org/tmatesoft/hg/test/ErrorCollectorExt.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,45 +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.test; - -import static org.junit.Assert.assertThat; - -import java.util.concurrent.Callable; - -import org.hamcrest.Matcher; -import org.junit.rules.ErrorCollector; - -/** - * Expose verify method for allow not-junit runs to check test outcome - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -final class ErrorCollectorExt extends ErrorCollector { - public void verify() throws Throwable { - super.verify(); - } - - public void checkThat(final String reason, final T value, final Matcher matcher) { - checkSucceeds(new Callable() { - public Object call() throws Exception { - assertThat(reason, value, matcher); - return value; - } - }); - } -} \ No newline at end of file diff -r edb2e2829352 -r 6ec4af642ba8 test/org/tmatesoft/hg/test/ExecHelper.java --- a/test/org/tmatesoft/hg/test/ExecHelper.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,101 +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.test; - -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.StringTokenizer; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class ExecHelper { - - private final OutputParser parser; - private File dir; - private int exitValue; - - public ExecHelper(OutputParser outParser, File workingDir) { - parser = outParser; - dir = workingDir; - } - - public void run(String... cmd) throws IOException, InterruptedException { - ProcessBuilder pb = null; - 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; - } - // PATHEXT controls precedence of .exe, .bat and .cmd files, ususlly .exe wins - if (new File(pe, cmd[0] + ".bat").exists() || new File(pe, cmd[0] + ".cmd").exists()) { - ArrayList command = new ArrayList(); - command.add("cmd.exe"); - command.add("/C"); - command.addAll(Arrays.asList(cmd)); - pb = new ProcessBuilder(command); - break; - } - } - } - 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(); - parser.parse(res); - } - - public int getExitValue() { - return exitValue; - } - - public void cwd(File wd) { - dir = wd; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 test/org/tmatesoft/hg/test/LogOutputParser.java --- a/test/org/tmatesoft/hg/test/LogOutputParser.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,106 +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.test; - -import java.util.LinkedList; -import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.tmatesoft.hg.repo.HgRepository; - - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class LogOutputParser implements OutputParser { - private final List result = new LinkedList(); - private Pattern pattern1; - private Pattern pattern2; - private Pattern pattern3; - private Pattern pattern4; - private Pattern pattern5; - - public LogOutputParser(boolean outputWithDebug) { - if (outputWithDebug) { - pattern1 = Pattern.compile("^changeset:\\s+(\\d+):([a-f0-9]{40})\n(^tag:(.+)$)?", Pattern.MULTILINE); - pattern2 = Pattern.compile("^parent:\\s+(-?\\d+):([a-f0-9]{40})\n", Pattern.MULTILINE); - pattern3 = Pattern.compile("^manifest:\\s+(\\d+):([a-f0-9]{40})\nuser:\\s+(\\S.+)\ndate:\\s+(\\S.+)\n", Pattern.MULTILINE); - pattern4 = Pattern.compile("^description:\\n", Pattern.MULTILINE); - pattern5 = Pattern.compile("\\n\\n"); - //p = "^manifest:\\s+(\\d+):([a-f0-9]{40})\nuser:(.+)$"; - } else { - throw HgRepository.notImplemented(); - } - } - - public void reset() { - result.clear(); - } - - public List getResult() { - return result; - } - - public void parse(CharSequence seq) { - Matcher m = pattern1.matcher(seq); - while (m.find()) { - Record r = new Record(); - r.changesetIndex = Integer.parseInt(m.group(1)); - r.changesetNodeid = m.group(2); - //tags = m.group(4); - m.usePattern(pattern2); - if (m.find()) { - r.parent1Index = Integer.parseInt(m.group(1)); - r.parent1Nodeid = m.group(2); - } - if (m.find()) { - r.parent2Index = Integer.parseInt(m.group(1)); - r.parent2Nodeid = m.group(2); - } - m.usePattern(pattern3); - if (m.find()) { - r.user = m.group(3); - r.date = m.group(4); - } - m.usePattern(pattern4); - if (m.find()) { - int commentStart = m.end(); - m.usePattern(pattern5); - if (m.find()) { - r.description = seq.subSequence(commentStart, m.start()).toString(); - } - } - result.add(r); - m.usePattern(pattern1); - } - } - - public static class Record { - public int changesetIndex; - public String changesetNodeid; - public int parent1Index; - public int parent2Index; - public String parent1Nodeid; - public String parent2Nodeid; - public String user; - public String date; - public String description; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 test/org/tmatesoft/hg/test/ManifestOutputParser.java --- a/test/org/tmatesoft/hg/test/ManifestOutputParser.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,56 +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.test; - -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.util.Path; - - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class ManifestOutputParser implements OutputParser { - - private final Pattern pattern; - private final LinkedHashMap result = new LinkedHashMap(); - - public ManifestOutputParser() { - pattern = Pattern.compile("^([a-f0-9]{40}) (\\d{3}) (.+)$", Pattern.MULTILINE); - } - - public void reset() { - result.clear(); - } - - public Map getResult() { - return result; - } - - public void parse(CharSequence seq) { - Matcher m = pattern.matcher(seq); - while (m.find()) { - result.put(Path.create(m.group(3)), Nodeid.fromAscii(m.group(1).getBytes(), 0, 40)); - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 test/org/tmatesoft/hg/test/OutputParser.java --- a/test/org/tmatesoft/hg/test/OutputParser.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,43 +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.test; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public interface OutputParser { - - public void parse(CharSequence seq); - - public class Stub implements OutputParser { - private boolean shallDump; - public Stub() { - this(false); - } - public Stub(boolean dump) { - shallDump = dump; - } - public void parse(CharSequence seq) { - if (shallDump) { - System.out.println(seq); - } - // else no-op - } - } -} diff -r edb2e2829352 -r 6ec4af642ba8 test/org/tmatesoft/hg/test/StatusOutputParser.java --- a/test/org/tmatesoft/hg/test/StatusOutputParser.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,148 +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.test; - -import java.io.File; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.tmatesoft.hg.repo.HgStatusCollector; -import org.tmatesoft.hg.util.Path; -import org.tmatesoft.hg.util.PathPool; -import org.tmatesoft.hg.util.PathRewrite; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class StatusOutputParser implements OutputParser { - - private final Pattern pattern; - // although using StatusCollector.Record is not really quite honest for testing, - // it's deemed acceptable as long as that class is primitive 'collect all results' - private HgStatusCollector.Record result = new HgStatusCollector.Record(); - private final PathPool pathHelper; - - public StatusOutputParser() { -// pattern = Pattern.compile("^([MAR?IC! ]) ([\\w \\.-/\\\\]+)$", Pattern.MULTILINE); - pattern = Pattern.compile("^([MAR?IC! ]) (.+)$", Pattern.MULTILINE); - pathHelper = new PathPool(new PathRewrite() { - - private final boolean winPathSeparator = File.separatorChar == '\\'; - - public String rewrite(String s) { - if (winPathSeparator) { - // Java impl always give slashed path, while Hg uses local, os-specific convention - s = s.replace('\\', '/'); - } - return s; - } - }); - } - - public void reset() { - result = new HgStatusCollector.Record(); - } - - public void parse(CharSequence seq) { - Matcher m = pattern.matcher(seq); - Path lastEntry = null; - while (m.find()) { - Path fname = pathHelper.path(m.group(2)); - switch ((int) m.group(1).charAt(0)) { - case (int) 'M' : { - result.modified(fname); - lastEntry = fname; // for files modified through merge there's also 'copy' source - break; - } - case (int) 'A' : { - result.added(fname); - lastEntry = fname; - break; - } - case (int) 'R' : { - result.removed(fname); - break; - } - case (int) '?' : { - result.unknown(fname); - break; - } - case (int) 'I' : { - result.ignored(fname); - break; - } - case (int) 'C' : { - result.clean(fname); - break; - } - case (int) '!' : { - result.missing(fname); - break; - } - case (int) ' ' : { - // last added is copy destination - // to get or to remove it - depends on what StatusCollector does in this case - result.copied(fname, lastEntry); - lastEntry = null; - break; - } - } - } - } - - // - public List getModified() { - return result.getModified(); - } - - public List getAdded() { - List rv = new LinkedList(result.getAdded()); - for (Path p : result.getCopied().keySet()) { - rv.remove(p); // remove only one duplicate - } - return rv; - } - - public List getRemoved() { - return result.getRemoved(); - } - - public Map getCopied() { - return result.getCopied(); - } - - public List getClean() { - return result.getClean(); - } - - public List getMissing() { - return result.getMissing(); - } - - public List getUnknown() { - return result.getUnknown(); - } - - public List getIgnored() { - return result.getIgnored(); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 test/org/tmatesoft/hg/test/TestByteChannel.java --- a/test/org/tmatesoft/hg/test/TestByteChannel.java Mon May 09 11:49:23 2011 +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.test; - -import static org.junit.Assert.assertArrayEquals; - -import org.junit.Assert; -import org.junit.Test; -import org.tmatesoft.hg.internal.ByteArrayChannel; -import org.tmatesoft.hg.repo.HgDataFile; -import org.tmatesoft.hg.repo.HgRepository; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class TestByteChannel { - - private HgRepository repo; - - public static void main(String[] args) throws Exception { -// HgRepoFacade rf = new HgRepoFacade(); -// rf.init(); -// HgDataFile file = rf.getRepository().getFileNode("src/org/tmatesoft/hg/internal/KeywordFilter.java"); -// for (int i = file.getLastRevision(); i >= 0; i--) { -// System.out.print("Content for revision:" + i); -// compareContent(file, i); -// System.out.println(" OK"); -// } - //CatCommand cmd = rf.createCatCommand(); - } - -// private static void compareContent(HgDataFile file, int rev) throws Exception { -// byte[] oldAccess = file.content(rev); -// ByteArrayChannel ch = new ByteArrayChannel(); -// file.content(rev, ch); -// byte[] newAccess = ch.toArray(); -// Assert.assertArrayEquals(oldAccess, newAccess); -// // don't trust anyone (even JUnit) -// if (!Arrays.equals(oldAccess, newAccess)) { -// throw new RuntimeException("Failed:" + rev); -// } -// } - - @Test - public void testContent() throws Exception { - repo = Configuration.get().find("log-1"); - final byte[] expectedContent = new byte[] { 'a', ' ', 13, 10 }; - ByteArrayChannel ch = new ByteArrayChannel(); - repo.getFileNode("dir/b").content(0, ch); - assertArrayEquals(expectedContent, ch.toArray()); - repo.getFileNode("d").content(HgRepository.TIP, ch = new ByteArrayChannel() ); - assertArrayEquals(expectedContent, ch.toArray()); - } - - @Test - public void testStripMetadata() throws Exception { - repo = Configuration.get().find("log-1"); - ByteArrayChannel ch = new ByteArrayChannel(); - HgDataFile dir_b = repo.getFileNode("dir/b"); - Assert.assertTrue(dir_b.isCopy()); - Assert.assertEquals("b", dir_b.getCopySourceName().toString()); - Assert.assertEquals("e44751cdc2d14f1eb0146aa64f0895608ad15917", dir_b.getCopySourceRevision().toString()); - dir_b.content(0, ch); - // assert rawContent has 1 10 ... 1 10 - assertArrayEquals("a \r\n".getBytes(), ch.toArray()); - // - // try once again to make sure metadata records/extracts correct offsets - dir_b.content(0, ch = new ByteArrayChannel()); - assertArrayEquals("a \r\n".getBytes(), ch.toArray()); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 test/org/tmatesoft/hg/test/TestClone.java --- a/test/org/tmatesoft/hg/test/TestClone.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,94 +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.test; - -import java.io.File; -import java.io.IOException; -import java.util.Arrays; -import java.util.LinkedList; -import java.util.List; - -import org.hamcrest.CoreMatchers; -import org.junit.Rule; -import org.junit.Test; -import org.tmatesoft.hg.core.HgCloneCommand; -import org.tmatesoft.hg.repo.HgRemoteRepository; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class TestClone { - - @Rule - public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); - - public static void main(String[] args) throws Throwable { - TestClone t = new TestClone(); - t.testSimpleClone(); - t.errorCollector.verify(); - } - - public TestClone() { - } - - @Test - public void testSimpleClone() throws Exception { - int x = 0; - final File tempDir = Configuration.get().getTempDir(); - for (HgRemoteRepository hgRemote : Configuration.get().allRemote()) { - HgCloneCommand cmd = new HgCloneCommand(); - cmd.source(hgRemote); - File dest = new File(tempDir, "test-clone-" + x++); - if (dest.exists()) { - rmdir(dest); - } - cmd.destination(dest); - cmd.execute(); - verify(hgRemote, dest); - } - } - - private void verify(HgRemoteRepository hgRemote, File dest) throws Exception { - ExecHelper eh = new ExecHelper(new OutputParser.Stub(), dest); - eh.run("hg", "verify"); - errorCollector.checkThat("Verify", eh.getExitValue(), CoreMatchers.equalTo(0)); - eh.run("hg", "out", hgRemote.getLocation()); - errorCollector.checkThat("Outgoing", eh.getExitValue(), CoreMatchers.equalTo(1)); - eh.run("hg", "in", hgRemote.getLocation()); - errorCollector.checkThat("Incoming", eh.getExitValue(), CoreMatchers.equalTo(1)); - } - - static void rmdir(File dest) throws IOException { - LinkedList queue = new LinkedList(); - queue.addAll(Arrays.asList(dest.listFiles())); - while (!queue.isEmpty()) { - File next = queue.removeFirst(); - if (next.isDirectory()) { - List files = Arrays.asList(next.listFiles()); - if (!files.isEmpty()) { - queue.addAll(files); - queue.add(next); - } - // fall through - } - next.delete(); - } - dest.delete(); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 test/org/tmatesoft/hg/test/TestHistory.java --- a/test/org/tmatesoft/hg/test/TestHistory.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,240 +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.test; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.is; -import static org.junit.Assert.assertTrue; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; - -import org.junit.Rule; -import org.junit.Test; -import org.tmatesoft.hg.core.HgChangeset; -import org.tmatesoft.hg.core.HgLogCommand; -import org.tmatesoft.hg.core.HgLogCommand.CollectHandler; -import org.tmatesoft.hg.core.HgLogCommand.FileHistoryHandler; -import org.tmatesoft.hg.core.HgLogCommand.FileRevision; -import org.tmatesoft.hg.repo.HgLookup; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.test.LogOutputParser.Record; -import org.tmatesoft.hg.util.Path; - - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class TestHistory { - - @Rule - public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); - - private HgRepository repo; - private final ExecHelper eh; - private LogOutputParser changelogParser; - - public static void main(String[] args) throws Throwable { - TestHistory th = new TestHistory(); - th.testCompleteLog(); - th.testFollowHistory(); - th.errorCollector.verify(); -// th.testPerformance(); - th.testOriginalTestLogRepo(); - th.testUsernames(); - th.testBranches(); - // - th.errorCollector.verify(); - } - - public TestHistory() throws Exception { - this(new HgLookup().detectFromWorkingDir()); - } - - private TestHistory(HgRepository hgRepo) { - repo = hgRepo; - eh = new ExecHelper(changelogParser = new LogOutputParser(true), null); - - } - - @Test - public void testCompleteLog() throws Exception { - changelogParser.reset(); - eh.run("hg", "log", "--debug"); - List r = new HgLogCommand(repo).execute(); - report("hg log - COMPLETE REPO HISTORY", r, true); - } - - @Test - public void testFollowHistory() throws Exception { - final Path f = Path.create("cmdline/org/tmatesoft/hg/console/Remote.java"); - try { - if (repo.getFileNode(f).exists()) { // FIXME getFileNode shall not fail with IAE - changelogParser.reset(); - eh.run("hg", "log", "--debug", "--follow", f.toString()); - - class H extends CollectHandler implements FileHistoryHandler { - boolean copyReported = false; - boolean fromMatched = false; - public void copy(FileRevision from, FileRevision to) { - copyReported = true; - fromMatched = "src/com/tmate/hgkit/console/Remote.java".equals(from.getPath().toString()); - } - }; - H h = new H(); - new HgLogCommand(repo).file(f, true).execute(h); - String what = "hg log - FOLLOW FILE HISTORY"; - errorCollector.checkThat(what + "#copyReported ", h.copyReported, is(true)); - errorCollector.checkThat(what + "#copyFromMatched", h.fromMatched, is(true)); - // - // cmdline always gives in changesets in order from newest (bigger rev number) to oldest. - // LogCommand does other way round, from oldest to newest, follewed by revisions of copy source, if any - // (apparently older than oldest of the copy target). Hence need to sort Java results according to rev numbers - 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; - } - }); - report(what, sorted, false); - } - } catch (IllegalArgumentException ex) { - System.out.println("Can't test file history with follow because need to query specific file with history"); - } - } - - private void report(String what, List r, boolean reverseConsoleResult) { - final List consoleResult = changelogParser.getResult(); - report(what, r, consoleResult, reverseConsoleResult, errorCollector); - } - - static void report(String what, List hg4jResult, List consoleResult, boolean reverseConsoleResult, ErrorCollectorExt errorCollector) { - consoleResult = new ArrayList(consoleResult); // need a copy in case callee would use result again - if (reverseConsoleResult) { - Collections.reverse(consoleResult); - } - errorCollector.checkThat(what + ". Number of changeset reported didn't match", consoleResult.size(), equalTo(hg4jResult.size())); - Iterator consoleResultItr = consoleResult.iterator(); - for (HgChangeset cs : hg4jResult) { - if (!consoleResultItr.hasNext()) { - errorCollector.addError(new AssertionError("Ran out of console results while there are still hg4j results")); - break; - } - Record cr = consoleResultItr.next(); - int x = cs.getRevision() == cr.changesetIndex ? 0x1 : 0; - x |= cs.getDate().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)); - consoleResultItr.remove(); - } - errorCollector.checkThat(what + ". Unprocessed results in console left (insufficient from hg4j)", consoleResultItr.hasNext(), equalTo(false)); - } - - public void testPerformance() throws Exception { - final int runs = 10; - final long start1 = System.currentTimeMillis(); - for (int i = 0; i < runs; i++) { - changelogParser.reset(); - eh.run("hg", "log", "--debug"); - } - final long start2 = System.currentTimeMillis(); - for (int i = 0; i < runs; i++) { - new HgLogCommand(repo).execute(); - } - final long end = System.currentTimeMillis(); - System.out.printf("'hg log --debug', %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); - } - - @Test - public void testOriginalTestLogRepo() throws Exception { - 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 - // - changelogParser.reset(); - eh.run("hg", "log", "--debug", "a", "--cwd", repo.getLocation()); - 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); - // - changelogParser.reset(); - eh.run("hg", "log", "--debug", "-f", "e", "--cwd", repo.getLocation()); - report("log -f e", cmd.file("e", true).execute(), false /*#1, below*/); - // - changelogParser.reset(); - eh.run("hg", "log", "--debug", "dir/b", "--cwd", repo.getLocation()); - 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*/); - /* - * #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. - */ - } - - @Test - public void testUsernames() throws Exception { - repo = Configuration.get().find("log-users"); - final String user1 = "User One "; - // - changelogParser.reset(); - eh.run("hg", "log", "--debug", "-u", user1, "--cwd", repo.getLocation()); - report("log -u " + user1, new HgLogCommand(repo).user(user1).execute(), true); - // - changelogParser.reset(); - eh.run("hg", "log", "--debug", "-u", "user1", "-u", "user2", "--cwd", repo.getLocation()); - report("log -u user1 -u user2", new HgLogCommand(repo).user("user1").user("user2").execute(), true); - // - changelogParser.reset(); - eh.run("hg", "log", "--debug", "-u", "user3", "--cwd", repo.getLocation()); - report("log -u user3", new HgLogCommand(repo).user("user3").execute(), true); - } - - @Test - public void testBranches() throws Exception { - repo = Configuration.get().find("log-branches"); - changelogParser.reset(); - eh.run("hg", "log", "--debug", "-b", "default", "--cwd", repo.getLocation()); - report("log -b default" , new HgLogCommand(repo).branch("default").execute(), true); - // - changelogParser.reset(); - eh.run("hg", "log", "--debug", "-b", "test", "--cwd", repo.getLocation()); - report("log -b test" , new HgLogCommand(repo).branch("test").execute(), true); - // - assertTrue("log -b dummy shall yeild empty result", new HgLogCommand(repo).branch("dummy").execute().isEmpty()); - // - changelogParser.reset(); - eh.run("hg", "log", "--debug", "-b", "default", "-b", "test", "--cwd", repo.getLocation()); - report("log -b default -b test" , new HgLogCommand(repo).branch("default").branch("test").execute(), true); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 test/org/tmatesoft/hg/test/TestIncoming.java --- a/test/org/tmatesoft/hg/test/TestIncoming.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,142 +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.test; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.tmatesoft.hg.internal.RequiresFile.*; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; - -import org.junit.Assert; -import org.junit.Rule; -import org.junit.Test; -import org.tmatesoft.hg.core.HgChangeset; -import org.tmatesoft.hg.core.HgIncomingCommand; -import org.tmatesoft.hg.core.HgLogCommand; -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.internal.Internals; -import org.tmatesoft.hg.repo.HgLookup; -import org.tmatesoft.hg.repo.HgRemoteRepository; -import org.tmatesoft.hg.repo.HgRepository; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class TestIncoming { - - @Rule - public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); - - public static void main(String[] args) throws Throwable { - Configuration.get().remoteServers("http://localhost:8000/"); - TestIncoming t = new TestIncoming(); - t.testSimple(); - t.errorCollector.verify(); - } - - public TestIncoming() { -// Configuration.get().remoteServers("http://localhost:8000/"); - } - - @Test - public void testSimple() throws Exception { - int x = 0; - HgLookup lookup = new HgLookup(); - for (HgRemoteRepository hgRemote : Configuration.get().allRemote()) { - File dest = initEmptyTempRepo("test-incoming-" + x++); - HgRepository localRepo = lookup.detect(dest); - // Idea: - // hg in, hg4j in, compare - // hg pull total/2 - // hg in, hg4j in, compare - List incoming = runAndCompareIncoming(localRepo, hgRemote); - Assert.assertTrue("Need remote repository of reasonable size to test incoming command for partially filled case", incoming.size() >= 5); - // - Nodeid median = incoming.get(incoming.size() / 2); - System.out.println("About to pull up to revision " + median.shortNotation()); - new ExecHelper(new OutputParser.Stub(), dest).run("hg", "pull", "-r", median.toString(), hgRemote.getLocation()); - // - // shall re-read repository to pull up new changes - localRepo = lookup.detect(dest); - runAndCompareIncoming(localRepo, hgRemote); - } - } - - private List runAndCompareIncoming(HgRepository localRepo, HgRemoteRepository hgRemote) throws Exception { - // need new command instance as subsequence exec[Lite|Full] on the same command would yield same result, - // regardless of the pull in between. - HgIncomingCommand cmd = new HgIncomingCommand(localRepo); - cmd.against(hgRemote); - HgLogCommand.CollectHandler collector = new HgLogCommand.CollectHandler(); - LogOutputParser outParser = new LogOutputParser(true); - ExecHelper eh = new ExecHelper(outParser, new File(localRepo.getLocation())); - cmd.executeFull(collector); - eh.run("hg", "incoming", "--debug", hgRemote.getLocation()); - List liteResult = cmd.executeLite(null); - report(collector, outParser, liteResult, errorCollector); - return liteResult; - } - - static void report(HgLogCommand.CollectHandler collector, LogOutputParser outParser, List liteResult, ErrorCollectorExt errorCollector) { - TestHistory.report("hg vs execFull", collector.getChanges(), outParser.getResult(), false, errorCollector); - // - ArrayList expected = new ArrayList(outParser.getResult().size()); - for (LogOutputParser.Record r : outParser.getResult()) { - Nodeid nid = Nodeid.fromAscii(r.changesetNodeid); - expected.add(nid); - } - checkNodeids("hg vs execLite:", liteResult, expected, errorCollector); - // - expected = new ArrayList(outParser.getResult().size()); - for (HgChangeset cs : collector.getChanges()) { - expected.add(cs.getNodeid()); - } - checkNodeids("execFull vs execLite:", liteResult, expected, errorCollector); - } - - static void checkNodeids(String what, List liteResult, List expected, ErrorCollectorExt errorCollector) { - HashSet set = new HashSet(liteResult); - for (Nodeid nid : expected) { - boolean removed = set.remove(nid); - errorCollector.checkThat(what + " Missing " + nid.shortNotation() + " in HgIncomingCommand.execLite result", removed, equalTo(true)); - } - errorCollector.checkThat(what + " Superfluous cset reported by HgIncomingCommand.execLite", set.isEmpty(), equalTo(true)); - } - - static File createEmptyDir(String dirName) throws IOException { - File dest = new File(Configuration.get().getTempDir(), dirName); - if (dest.exists()) { - TestClone.rmdir(dest); - } - dest.mkdirs(); - return dest; - } - - static File initEmptyTempRepo(String dirName) throws IOException { - File dest = createEmptyDir(dirName); - Internals implHelper = new Internals(); - implHelper.setStorageConfig(1, STORE | FNCACHE | DOTENCODE); - implHelper.initEmptyRepository(new File(dest, ".hg")); - return dest; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 test/org/tmatesoft/hg/test/TestManifest.java --- a/test/org/tmatesoft/hg/test/TestManifest.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,120 +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.test; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.junit.Assert.assertTrue; -import static org.tmatesoft.hg.repo.HgRepository.TIP; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.Map; - -import org.junit.Rule; -import org.junit.Test; -import org.tmatesoft.hg.core.HgLogCommand.FileRevision; -import org.tmatesoft.hg.core.HgManifestCommand; -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.repo.HgLookup; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.util.Path; - - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class TestManifest { - - @Rule - public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); - - private final HgRepository repo; - private ManifestOutputParser manifestParser; - private ExecHelper eh; - final LinkedList revisions = new LinkedList(); - private HgManifestCommand.Handler handler = new HgManifestCommand.Handler() { - - public void file(FileRevision fileRevision) { - revisions.add(fileRevision); - } - - public void end(Nodeid manifestRevision) {} - public void dir(Path p) {} - public void begin(Nodeid manifestRevision) {} - }; - - public static void main(String[] args) throws Throwable { - TestManifest tm = new TestManifest(); - tm.testTip(); - tm.testFirstRevision(); - tm.testRevisionInTheMiddle(); - tm.errorCollector.verify(); - } - - public TestManifest() throws Exception { - this(new HgLookup().detectFromWorkingDir()); - } - - private TestManifest(HgRepository hgRepo) { - repo = hgRepo; - assertTrue(!repo.isInvalid()); - eh = new ExecHelper(manifestParser = new ManifestOutputParser(), null); - } - - @Test - public void testTip() throws Exception { - testRevision(TIP); - } - - @Test - public void testFirstRevision() throws Exception { - testRevision(0); - } - - @Test - public void testRevisionInTheMiddle() throws Exception { - int rev = repo.getManifest().getRevisionCount() / 2; - if (rev == 0) { - throw new IllegalStateException("Need manifest with few revisions"); - } - testRevision(rev); - } - - private void testRevision(int rev) throws Exception { - manifestParser.reset(); - eh.run("hg", "manifest", "--debug", "--rev", String.valueOf(rev == TIP ? -1 : rev)); - revisions.clear(); - new HgManifestCommand(repo).revision(rev).execute(handler); - report("manifest " + (rev == TIP ? "TIP:" : "--rev " + rev)); - } - - private void report(String what) throws Exception { - final Map cmdLineResult = new LinkedHashMap(manifestParser.getResult()); - for (FileRevision fr : revisions) { - Nodeid nid = cmdLineResult.remove(fr.getPath()); - errorCollector.checkThat("Extra " + fr.getPath() + " in Java result", nid, notNullValue()); - if (nid != null) { - errorCollector.checkThat("Non-matching nodeid:" + nid, nid, equalTo(fr.getRevision())); - } - } - errorCollector.checkThat("Non-matched entries from command line:", cmdLineResult, equalTo(Collections.emptyMap())); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 test/org/tmatesoft/hg/test/TestOutgoing.java --- a/test/org/tmatesoft/hg/test/TestOutgoing.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,94 +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.test; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; -import java.util.List; - -import org.junit.Assert; -import org.junit.Rule; -import org.junit.Test; -import org.tmatesoft.hg.core.HgLogCommand; -import org.tmatesoft.hg.core.HgOutgoingCommand; -import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.repo.HgLookup; -import org.tmatesoft.hg.repo.HgRemoteRepository; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class TestOutgoing { - - @Rule - public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); - - public static void main(String[] args) throws Throwable { - Configuration.get().remoteServers("http://localhost:8000/"); - TestOutgoing t = new TestOutgoing(); - t.testSimple(); - t.errorCollector.verify(); - } - - public TestOutgoing() { - } - - @Test - public void testSimple() throws Exception { - int x = 0; - HgLookup lookup = new HgLookup(); - for (HgRemoteRepository hgRemote : Configuration.get().allRemote()) { - File dest = TestIncoming.createEmptyDir("test-outgoing-" + x++); - ExecHelper eh0 = new ExecHelper(new OutputParser.Stub(false), null); - eh0.run("hg", "clone", hgRemote.getLocation(), dest.toString()); - eh0.cwd(dest); - Assert.assertEquals("initial clone failed", 0, eh0.getExitValue()); - HgOutgoingCommand cmd = new HgOutgoingCommand(lookup.detect(dest)).against(hgRemote); - LogOutputParser outParser = new LogOutputParser(true); - ExecHelper eh = new ExecHelper(outParser, dest); - HgLogCommand.CollectHandler collector = new HgLogCommand.CollectHandler(); - // - cmd.executeFull(collector); - List liteResult = cmd.executeLite(null); - eh.run("hg", "outgoing", "--debug", hgRemote.getLocation()); - TestIncoming.report(collector, outParser, liteResult, errorCollector); - // - File f = new File(dest, "Test.txt"); - append(f, "1"); - eh0.run("hg", "add"); - eh0.run("hg", "commit", "-m", "1"); - append(f, "2"); - eh0.run("hg", "commit", "-m", "2"); - // - cmd = new HgOutgoingCommand(lookup.detect(dest)).against(hgRemote); - cmd.executeFull(collector = new HgLogCommand.CollectHandler()); - liteResult = cmd.executeLite(null); - outParser.reset(); - eh.run("hg", "outgoing", "--debug", hgRemote.getLocation()); - TestIncoming.report(collector, outParser, liteResult, errorCollector); - } - } - - static void append(File f, String s) throws IOException { - FileWriter fw = new FileWriter(f); - fw.append(s); - fw.close(); - } -} diff -r edb2e2829352 -r 6ec4af642ba8 test/org/tmatesoft/hg/test/TestStatus.java --- a/test/org/tmatesoft/hg/test/TestStatus.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,242 +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.test; - -import static org.hamcrest.CoreMatchers.equalTo; -import static org.tmatesoft.hg.core.HgStatus.*; -import static org.tmatesoft.hg.core.HgStatus.Kind.*; -import static org.tmatesoft.hg.repo.HgRepository.TIP; - -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -import org.junit.Assume; -import org.junit.Rule; -import org.junit.Test; -import org.tmatesoft.hg.core.HgStatus; -import org.tmatesoft.hg.core.HgStatusCommand; -import org.tmatesoft.hg.repo.HgLookup; -import org.tmatesoft.hg.repo.HgRepository; -import org.tmatesoft.hg.repo.HgStatusCollector; -import org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector; -import org.tmatesoft.hg.util.Path; - - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class TestStatus { - - @Rule - public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); - - private final HgRepository repo; - private StatusOutputParser statusParser; - private ExecHelper eh; - - public static void main(String[] args) throws Throwable { - TestStatus test = new TestStatus(); - test.testLowLevel(); - test.testStatusCommand(); - test.testPerformance(); - test.errorCollector.verify(); - } - - public TestStatus() throws Exception { - this(new HgLookup().detectFromWorkingDir()); - } - - private TestStatus(HgRepository hgRepo) { - repo = hgRepo; - Assume.assumeTrue(!repo.isInvalid()); - statusParser = new StatusOutputParser(); - eh = new ExecHelper(statusParser, null); - } - - @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); - // - 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); - // - 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); - // - 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); - } - - @Test - public void testStatusCommand() throws Exception { - final HgStatusCommand sc = new HgStatusCommand(repo).all(); - StatusCollector r; - statusParser.reset(); - eh.run("hg", "status", "-A"); - sc.execute(r = new StatusCollector()); - 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); - // - 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); - - // TODO check not -A, but defaults()/custom set of modifications - } - - private static class StatusCollector implements HgStatusCommand.Handler { - private final Map> map = new TreeMap>(); - - public void handleStatus(HgStatus s) { - List l = map.get(s.getKind()); - if (l == null) { - l = new LinkedList(); - map.put(s.getKind(), l); - } - l.add(s.getPath()); - } - - public List get(Kind k) { - List rv = map.get(k); - if (rv == null) { - return Collections.emptyList(); - } - return rv; - } - } - - public void testRemovedAgainstNonTip() { - /* - status --rev N when a file added past revision N was removed ((both physically and in dirstate), but not yet committed - - Reports extra REMOVED file (the one added and removed in between). Shall not - */ - } - - /* - * 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) - * 'hg status -A --rev 3:80': Native client total 1828 (182 per run), Java client 235 (23) - * '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 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) - */ - public void testPerformance() throws Exception { - final int runs = 10; - final long start1 = System.currentTimeMillis(); - for (int i = 0; i < runs; i++) { - statusParser.reset(); - eh.run("hg", "status", "-A", "--rev", "3:80"); - } - final long start2 = System.currentTimeMillis(); - for (int i = 0; i < runs; i++) { - StatusCollector r = new StatusCollector(); - 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); - } - - private void report(String what, StatusCollector r) { - 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 - } - - 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); - } - } 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())); - } - - private void reportNotEqual(String what, Collection l1, Collection l2) { - List diff = difference(l1, l2); - errorCollector.checkThat(what, diff, equalTo(Collections.emptyList())); - } - - 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); - } - } - return result; - } -} diff -r edb2e2829352 -r 6ec4af642ba8 test/org/tmatesoft/hg/test/TestStorePath.java --- a/test/org/tmatesoft/hg/test/TestStorePath.java Mon May 09 11:49:23 2011 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,73 +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.test; - -import static org.hamcrest.CoreMatchers.equalTo; -import junit.framework.Assert; - -import org.junit.Rule; -import org.junit.Test; -import org.tmatesoft.hg.internal.Internals; -import org.tmatesoft.hg.util.PathRewrite; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class TestStorePath { - - @Rule - public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); - - private PathRewrite storePathHelper; - - public static void main(String[] args) throws Throwable { - final TestStorePath test = new TestStorePath(); - test.testWindowsFilenames(); - test.testHashLongPath(); - test.errorCollector.verify(); - } - - public TestStorePath() { - final Internals i = new Internals(); - i.setStorageConfig(1, 0x7); - storePathHelper = i.buildDataFilesHelper(); - } - - @Test - public void testWindowsFilenames() { - // see http://mercurial.selenic.com/wiki/fncacheRepoFormat#Encoding_of_Windows_reserved_names - String n1 = "aux.bla/bla.aux/prn/PRN/lpt/com3/nul/coma/foo.NUL/normal.c"; - String r1 = "store/data/au~78.bla/bla.aux/pr~6e/_p_r_n/lpt/co~6d3/nu~6c/coma/foo._n_u_l/normal.c.i"; - Assert.assertEquals("Windows filenames are ", r1, storePathHelper.rewrite(n1)); - } - - @Test - public void testHashLongPath() { - String n1 = "AUX/SECOND/X.PRN/FOURTH/FI:FTH/SIXTH/SEVENTH/EIGHTH/NINETH/TENTH/ELEVENTH/LOREMIPSUM.TXT"; - String r1 = "store/dh/au~78/second/x.prn/fourth/fi~3afth/sixth/seventh/eighth/nineth/tenth/loremia20419e358ddff1bf8751e38288aff1d7c32ec05.i"; - String n2 = "enterprise/openesbaddons/contrib-imola/corba-bc/netbeansplugin/wsdlExtension/src/main/java/META-INF/services/org.netbeans.modules.xml.wsdl.bindingsupport.spi.ExtensibilityElementTemplateProvider"; - String r2 = "store/dh/enterpri/openesba/contrib-/corba-bc/netbeans/wsdlexte/src/main/java/org.net7018f27961fdf338a598a40c4683429e7ffb9743.i"; - String n3 = "AUX.THE-QUICK-BROWN-FOX-JU:MPS-OVER-THE-LAZY-DOG-THE-QUICK-BROWN-FOX-JUMPS-OVER-THE-LAZY-DOG.TXT"; - String r3 = "store/dh/au~78.the-quick-brown-fox-ju~3amps-over-the-lazy-dog-the-quick-brown-fox-jud4dcadd033000ab2b26eb66bae1906bcb15d4a70.i"; - // TODO segment[8] == [. ], segment[8] in the middle of windows reserved name or character (to see if ~xx is broken) - errorCollector.checkThat(storePathHelper.rewrite(n1), equalTo(r1)); - errorCollector.checkThat(storePathHelper.rewrite(n2), equalTo(r2)); - errorCollector.checkThat(storePathHelper.rewrite(n3), equalTo(r3)); - } -}