Mercurial > hg4j
changeset 442:6865eb742883
Tests for subrepo API, refactor status tests for reuse, better subrepos API
author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
---|---|
date | Fri, 27 Apr 2012 20:57:20 +0200 |
parents | 2a08466838d3 |
children | 072b5f3ed0c8 |
files | build.xml cmdline/org/tmatesoft/hg/console/Main.java src/org/tmatesoft/hg/internal/SubrepoManager.java src/org/tmatesoft/hg/repo/HgInternals.java src/org/tmatesoft/hg/repo/HgRepositoryFiles.java src/org/tmatesoft/hg/repo/HgStatusInspector.java src/org/tmatesoft/hg/repo/HgSubrepoLocation.java test-data/test-repos.jar test/org/tmatesoft/hg/test/ErrorCollectorExt.java test/org/tmatesoft/hg/test/TestStatus.java test/org/tmatesoft/hg/test/TestSubrepo.java |
diffstat | 11 files changed, 399 insertions(+), 169 deletions(-) [+] |
line wrap: on
line diff
--- a/build.xml Thu Apr 26 12:42:32 2012 +0200 +++ b/build.xml Fri Apr 27 20:57:20 2012 +0200 @@ -27,7 +27,7 @@ <property name="junit.jar" value="lib/junit-4.8.2.jar" /> <property name="ver.qualifier" value="" /> - <property name="version.lib" value="0.9.0" /> + <property name="version.lib" value="0.9.5" /> <property name="version.jar" value="${version.lib}${ver.qualifier}" /> <property name="compile-with-debug" value="yes"/> @@ -92,6 +92,7 @@ <test name="org.tmatesoft.hg.test.TestDirstate" /> <test name="org.tmatesoft.hg.test.TestBranches" /> <test name="org.tmatesoft.hg.test.TestByteChannel" /> + <test name="org.tmatesoft.hg.test.TestSubrepo" /> <test name="org.tmatesoft.hg.test.TestClone" /> <test name="org.tmatesoft.hg.test.TestIncoming" /> <test name="org.tmatesoft.hg.test.TestOutgoing" />
--- a/cmdline/org/tmatesoft/hg/console/Main.java Thu Apr 26 12:42:32 2012 +0200 +++ b/cmdline/org/tmatesoft/hg/console/Main.java Fri Apr 27 20:57:20 2012 +0200 @@ -97,14 +97,14 @@ public static void main(String[] args) throws Exception { Main m = new Main(args); - m.checkWalkFileRevisions(); +// m.checkWalkFileRevisions(); // m.checkSubProgress(); // m.checkFileFlags(); // m.buildFileLog(); // m.testConsoleLog(); // m.testTreeTraversal(); // m.testRevisionMap(); -// m.testSubrepos(); + m.testSubrepos(); // m.testReadWorkingCopy(); // m.testParents(); // m.testEffectiveFileLog(); @@ -318,7 +318,9 @@ } + // any repository with subrepositories private void testSubrepos() throws Exception { + // @see TestSubrepo#testAccessAPI for (HgSubrepoLocation l : hgRepo.getSubrepositories()) { System.out.println(l.getLocation()); System.out.println(l.getSource()); @@ -326,10 +328,10 @@ System.out.println(l.isCommitted() ? l.getRevision() : "not yet committed"); if (l.getType() == Kind.Hg) { HgRepository r = l.getRepo(); - System.out.printf("%s has %d revisions\n", l.getLocation(), r.getChangelog().getLastRevision() + 1); + System.out.printf("%s (%s) has %d revisions\n", l.getLocation(), r.getLocation(), r.getChangelog().getLastRevision() + 1); if (r.getChangelog().getLastRevision() >= 0) { final RawChangeset c = r.getChangelog().range(TIP, TIP).get(0); - System.out.printf("TIP: %s %s %s\n", c.user(), c.dateString(), c.comment()); + System.out.printf("TIP: %s %s '%s'\n", c.user(), c.dateString(), c.comment()); } } }
--- a/src/org/tmatesoft/hg/internal/SubrepoManager.java Thu Apr 26 12:42:32 2012 +0200 +++ b/src/org/tmatesoft/hg/internal/SubrepoManager.java Fri Apr 27 20:57:20 2012 +0200 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 TMate Software Ltd + * Copyright (c) 2011-2012 TMate Software Ltd * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -27,12 +27,17 @@ import java.util.List; import java.util.Map; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.repo.HgInternals; import org.tmatesoft.hg.repo.HgInvalidControlFileException; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.repo.HgSubrepoLocation; +import org.tmatesoft.hg.util.Path; /** * + * @see http://mercurial.selenic.com/wiki/SubrepoWork + * @see http://mercurial.selenic.com/wiki/Subrepository * @author Artem Tikhomirov * @author TMate Software Ltd. */ @@ -75,6 +80,7 @@ try { String line; LinkedList<HgSubrepoLocation> res = new LinkedList<HgSubrepoLocation>(); + HgInternals hgRepoInternal = new HgInternals(repo); while ((line = br.readLine()) != null) { int sep = line.indexOf('='); if (sep == -1) { @@ -84,7 +90,7 @@ // to have separate String instances (new String(line.substring())) String key = line.substring(0, sep).trim(); String value = line.substring(sep + 1).trim(); - if (value.length() == 0) { + if (key.length() == 0 || value.length() == 0) { // XXX log bad line? continue; } @@ -100,7 +106,12 @@ } } // TODO respect paths mappings in config file - HgSubrepoLocation loc = new HgSubrepoLocation(repo, key, value, kind, substate.get(key)); + // + // apparently, key value can't end with '/', `hg commit` fails if it does: + // abort: path ends in directory separator: fourth/ + Path p = Path.create(key.charAt(key.length()-1) == '/' ? key : key + '/'); + String revValue = substate.get(key); + HgSubrepoLocation loc = hgRepoInternal.newSubrepo(p, value, kind, revValue == null ? null : Nodeid.fromAscii(revValue)); res.add(loc); } return Arrays.asList(res.toArray(new HgSubrepoLocation[res.size()])); @@ -110,6 +121,7 @@ } private Map<String, String> readState(BufferedReader br) throws IOException { + // TODO reuse for other files with <revision><space><value> format, like .hgtags HashMap<String, String> rv = new HashMap<String, String>(); try { String line;
--- a/src/org/tmatesoft/hg/repo/HgInternals.java Thu Apr 26 12:42:32 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgInternals.java Fri Apr 27 20:57:20 2012 +0200 @@ -25,11 +25,13 @@ import java.net.InetAddress; import java.net.UnknownHostException; +import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.core.SessionContext; import org.tmatesoft.hg.internal.Experimental; import org.tmatesoft.hg.internal.Internals; import org.tmatesoft.hg.internal.RelativePathRewrite; import org.tmatesoft.hg.internal.WinToNixPathRewrite; +import org.tmatesoft.hg.repo.HgSubrepoLocation.Kind; import org.tmatesoft.hg.util.FileIterator; import org.tmatesoft.hg.util.FileWalker; import org.tmatesoft.hg.util.Path; @@ -39,7 +41,9 @@ /** * DO NOT USE THIS CLASS, INTENDED FOR TESTING PURPOSES. * - * This class gives access to repository internals, and holds methods that I'm not confident have to be widely accessible + * <p>This class is not part of the public API and may change or vanish any moment. + * + * <p>This class gives access to repository internals, and holds methods that I'm not confident have to be widely accessible * Debug helper, to access otherwise restricted (package-local) methods * * @author Artem Tikhomirov @@ -81,6 +85,10 @@ } return rv; } + + public HgSubrepoLocation newSubrepo(Path loc, String src, Kind kind, Nodeid rev) { + return new HgSubrepoLocation(repo, loc, src, kind, rev); + } public static File getRepositoryDir(HgRepository hgRepo) { return hgRepo.getRepositoryRoot();
--- a/src/org/tmatesoft/hg/repo/HgRepositoryFiles.java Thu Apr 26 12:42:32 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgRepositoryFiles.java Fri Apr 27 20:57:20 2012 +0200 @@ -19,7 +19,8 @@ import org.tmatesoft.hg.internal.Experimental; /** - * + * Names of some Mercurial configuration/service files. + * * @author Artem Tikhomirov * @author TMate Software Ltd. */ @@ -27,7 +28,8 @@ public enum HgRepositoryFiles { HgIgnore(".hgignore"), HgTags(".hgtags"), HgEol(".hgeol"), - Dirstate(".hg/dirstate"), HgLocalTags(".hg/localtags"); + Dirstate(".hg/dirstate"), HgLocalTags(".hg/localtags"), + HgSub(".hgsub"), HgSubstate(".hgsubstate"); private String fname;
--- a/src/org/tmatesoft/hg/repo/HgStatusInspector.java Thu Apr 26 12:42:32 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgStatusInspector.java Fri Apr 27 20:57:20 2012 +0200 @@ -31,8 +31,11 @@ void added(Path fname); /** * This method is invoked for files that we added as a result of a copy/move operation, and it's the sole - * method invoked in this case, that is {@link #added(Path)} method is NOT invoked along with it. - * If copied files of no interest, it is implementation responsibility to delegate to <code>this.added(fnameAdded)</code> + * method invoked in this case, that is {@link #added(Path)} method is NOT invoked along with it. + * Note, however, {@link #removed(Path)} IS invoked for the removed file in all cases, regardless whether it's a mere rename or not. + * <p>The reason why it's not symmetrical ({@link #copied(Path, Path)} and {@link #removed(Path)} but not {@link #added(Path)}) is that Mercurial + * does it this way ('copy' is just an extra attribute for Added file), and we try to stay as close as possible here. + * <p>If copied files of no interest, it is implementation responsibility to delegate to <code>this.added(fnameAdded)</code> */ void copied(Path fnameOrigin, Path fnameAdded); void removed(Path fname);
--- a/src/org/tmatesoft/hg/repo/HgSubrepoLocation.java Thu Apr 26 12:42:32 2012 +0200 +++ b/src/org/tmatesoft/hg/repo/HgSubrepoLocation.java Fri Apr 27 20:57:20 2012 +0200 @@ -19,11 +19,14 @@ import java.io.File; import org.tmatesoft.hg.core.HgRepositoryNotFoundException; +import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.internal.Experimental; import org.tmatesoft.hg.util.Path; /** * WORK IN PROGRESS, DO NOT USE + * + * @see http://mercurial.selenic.com/wiki/Subrepository * @author Artem Tikhomirov * @author TMate Software Ltd. */ @@ -34,58 +37,92 @@ private final Kind kind; private final Path location; private final String source; - private final String revInfo; + private final Nodeid revInfo; public enum Kind { Hg, SVN, Git, } - public HgSubrepoLocation(HgRepository parentRepo, String repoLocation, String actualLocation, Kind type, String revision) { + /** + * + * @param parentRepo + * @param repoLocation path, shall be valid directory (i.e. even if .hgsub doesn't specify trailing slash, this one shall) + * @param actualLocation + * @param type + * @param revision may be <code>null</code> + */ + /*package-local*/ HgSubrepoLocation(HgRepository parentRepo, Path repoLocation, String actualLocation, Kind type, Nodeid revision) { owner = parentRepo; - location = Path.create(repoLocation); + location = repoLocation; source = actualLocation; kind = type; revInfo = revision; } - // as defined in .hgsub, key value + /** + * Sub-repository's location within owning repository, always directory, <code>path/to/nested</code>. + * <p> + * May differ from left-hand, key value from <code>.hgsub</code> if the latter doesn't include trailing slash, which is required + * for {@link Path} objects + * + * @return path to nested repository relative to owner's location + */ public Path getLocation() { return location; } - // value from .hgsub + /** + * Right-hand value from <code>.hgsub</code>, with <code>[kind]</code> stripped, if any. + * @return sub-repository's source + */ public String getSource() { return source; } + /** + * Sub-repository kind, either Mercurial, Subversion or Git + * @return one of predefined constants + */ public Kind getType() { return kind; } - public String getRevision() { + /** + * For a nested repository that has been committed at least once, returns + * its revision as known from <code>.hgsubstate</code> + * + * <p>Note, this revision belongs to the nested repository history, not that of owning repository. + * + * @return revision of the nested repository, or <code>null</code> if not yet committed + */ + public Nodeid getRevision() { return revInfo; } /** - * @return whether this sub repository is known only locally + * Answers whether this sub repository has ever been part of a commit of the owner repository + * + * @return <code>true</code> if owning repository records {@link #getRevision() revision} of this sub-repository */ public boolean isCommitted() { return revInfo != null; } /** - * @return <code>true</code> when there are local changes in the sub repository + * Answers whether there are local changes in the sub-repository, + * @return <code>true</code> if it's dirty */ public boolean hasChanges() { throw HgRepository.notImplemented(); } - -// public boolean isLocal() { -// } - + + /** + * Access repository that owns nested one described by this object + */ public HgRepository getOwner() { return owner; } /** + * Access nested repository as a full-fledged Mercurial repository * * @return object to access sub-repository * @throws HgRepositoryNotFoundException if failed to find repository
--- a/test/org/tmatesoft/hg/test/ErrorCollectorExt.java Thu Apr 26 12:42:32 2012 +0200 +++ b/test/org/tmatesoft/hg/test/ErrorCollectorExt.java Fri Apr 27 20:57:20 2012 +0200 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 TMate Software Ltd + * Copyright (c) 2011-2012 TMate Software Ltd * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -20,6 +20,7 @@ import java.util.concurrent.Callable; +import org.hamcrest.CoreMatchers; import org.hamcrest.Matcher; import org.junit.internal.runners.model.MultipleFailureException; import org.junit.rules.ErrorCollector; @@ -51,6 +52,10 @@ }); } + public void assertTrue(final boolean value) { + assertTrue(null, value); + } + public void assertTrue(final String reason, final boolean value) { checkSucceeds(new Callable<Object>() { public Object call() throws Exception { @@ -59,4 +64,8 @@ } }); } + + public <T> void assertEquals(T expected, T actual) { + checkThat(null, actual, CoreMatchers.equalTo(expected)); + } } \ No newline at end of file
--- a/test/org/tmatesoft/hg/test/TestStatus.java Thu Apr 26 12:42:32 2012 +0200 +++ b/test/org/tmatesoft/hg/test/TestStatus.java Fri Apr 27 20:57:20 2012 +0200 @@ -50,7 +50,6 @@ import org.tmatesoft.hg.util.Path; import org.tmatesoft.hg.util.Status; - /** * * @author Artem Tikhomirov @@ -64,6 +63,7 @@ private HgRepository repo; private StatusOutputParser statusParser; private ExecHelper eh; + private StatusReporter sr; public static void main(String[] args) throws Throwable { TestStatus test = new TestStatus(); @@ -79,7 +79,7 @@ t3.testDirstateParentOtherThanTipNoUpdate(); t3.errorCollector.verify(); } - + public TestStatus() throws Exception { this(new HgLookup().detectFromWorkingDir()); } @@ -89,41 +89,42 @@ Assume.assumeTrue(!repo.isInvalid()); statusParser = new StatusOutputParser(); eh = new ExecHelper(statusParser, hgRepo.getWorkingDir()); + sr = new StatusReporter(errorCollector, statusParser); } - + @Test public void testLowLevel() throws Exception { final HgWorkingCopyStatusCollector wcc = new HgWorkingCopyStatusCollector(repo); statusParser.reset(); eh.run("hg", "status", "-A"); HgStatusCollector.Record r = wcc.status(HgRepository.TIP); - report("hg status -A", r, statusParser); + sr.report("hg status -A", r); // statusParser.reset(); int revision = 3; eh.run("hg", "status", "-A", "--rev", String.valueOf(revision)); r = wcc.status(revision); - report("status -A --rev " + revision, r, statusParser); + sr.report("status -A --rev " + revision, r); // statusParser.reset(); eh.run("hg", "status", "-A", "--change", String.valueOf(revision)); r = new HgStatusCollector.Record(); new HgStatusCollector(repo).change(revision, r); - report("status -A --change " + revision, r, statusParser); + sr.report("status -A --change " + revision, r); // statusParser.reset(); int rev2 = 80; final String range = String.valueOf(revision) + ":" + String.valueOf(rev2); eh.run("hg", "status", "-A", "--rev", range); r = new HgStatusCollector(repo).status(revision, rev2); - report("Status -A -rev " + range, r, statusParser); + sr.report("Status -A -rev " + range, r); } /** * hg up --rev <earlier rev>; hg status * * To check if HgWorkingCopyStatusCollector respects actual working copy parent (takes from dirstate) - * and if status is calculated correctly + * and if status is calculated correctly */ @Test @Ignore("modifies test repository, needs careful configuration") @@ -145,16 +146,15 @@ // eh.run("hg", "status", "-A"); HgStatusCollector.Record r = wcc.status(HgRepository.TIP); - report("hg status -A", r, statusParser); + sr.report("hg status -A", r); // statusParser.reset(); int revision = 3; eh.run("hg", "status", "-A", "--rev", String.valueOf(revision)); r = wcc.status(revision); - report("status -A --rev " + revision, r, statusParser); + sr.report("status -A --rev " + revision, r); } - @Test public void testStatusCommand() throws Exception { final HgStatusCommand sc = new HgStatusCommand(repo).all(); @@ -162,26 +162,27 @@ statusParser.reset(); eh.run("hg", "status", "-A"); sc.execute(r = new StatusCollector()); - report("hg status -A", r); + sr.report("hg status -A", r); // statusParser.reset(); int revision = 3; eh.run("hg", "status", "-A", "--rev", String.valueOf(revision)); sc.base(revision).execute(r = new StatusCollector()); - report("status -A --rev " + revision, r); + sr.report("status -A --rev " + revision, r); // statusParser.reset(); eh.run("hg", "status", "-A", "--change", String.valueOf(revision)); sc.base(TIP).revision(revision).execute(r = new StatusCollector()); - report("status -A --change " + revision, r); - + sr.report("status -A --change " + revision, r); + // TODO check not -A, but defaults()/custom set of modifications } - - private static class StatusCollector implements HgStatusHandler { + + static class StatusCollector implements HgStatusHandler { private final Map<Kind, List<Path>> kind2names = new TreeMap<Kind, List<Path>>(); private final Map<Path, List<Kind>> name2kinds = new TreeMap<Path, List<Kind>>(); private final Map<Path, Status> name2error = new LinkedHashMap<Path, Status>(); + private final Map<Path, Path> new2oldName = new LinkedHashMap<Path, Path>(); public void status(HgStatus s) { List<Path> l = kind2names.get(s.getKind()); @@ -189,6 +190,9 @@ kind2names.put(s.getKind(), l = new LinkedList<Path>()); } l.add(s.getPath()); + if (s.isCopy()) { + new2oldName.put(s.getPath(), s.getOriginalPath()); + } // List<Kind> k = name2kinds.get(s.getPath()); if (k == null) { @@ -196,24 +200,56 @@ } k.add(s.getKind()); } - + public void error(Path file, Status s) { name2error.put(file, s); } - + public List<Path> get(Kind k) { List<Path> rv = kind2names.get(k); - return rv == null ? Collections.<Path>emptyList() : rv; + return rv == null ? Collections.<Path> emptyList() : rv; } - + public List<Kind> get(Path p) { List<Kind> rv = name2kinds.get(p); - return rv == null ? Collections.<Kind>emptyList() : rv; + return rv == null ? Collections.<Kind> emptyList() : rv; } - + public Map<Path, Status> getErrors() { return name2error; } + + public HgStatusCollector.Record asStatusRecord() { + HgStatusCollector.Record rv = new HgStatusCollector.Record(); + for (Path p : get(Modified)) { + rv.modified(p); + } + for (Path p : get(Added)) { + if (!new2oldName.containsKey(p)) { + // new files that are result of a copy get reported separately, below + rv.added(p); + } + } + for (Path p : get(Removed)) { + rv.removed(p); + } + for (Path p : get(Clean)) { + rv.clean(p); + } + for (Path p : get(Ignored)) { + rv.ignored(p); + } + for (Path p : get(Missing)) { + rv.missing(p); + } + for (Path p : get(Unknown)) { + rv.unknown(p); + } + for (Map.Entry<Path, Path> e : new2oldName.entrySet()) { + rv.copied(e.getValue(), e.getKey()); + } + return rv; + } } /* @@ -232,7 +268,7 @@ // shall not be listed at all assertTrue(sc.get(file5).isEmpty()); } - + /* * status-1/file2 is tracked, but later .hgignore got entry to ignore it, file2 got modified * HG doesn't respect .hgignore for tracked files. @@ -253,8 +289,8 @@ /* * status/dir/file4, added in rev 3, has been scheduled for removal (hg remove -Af file4), but still there in the WC. - * Shall be reported as Removed, when comparing against rev 3 - * (despite both rev 3 and WC's parent has file4, there are different paths in the code for wc against parent and wc against rev) + * Shall be reported as Removed, when comparing against rev 3 + * (despite both rev 3 and WC's parent has file4, there are different paths in the code for wc against parent and wc against rev) */ @Test public void testMarkedRemovedButStillInWC() throws Exception { @@ -280,10 +316,10 @@ } /* - * status-1/dir/file3 tracked, listed in .hgignore since rev 4, removed (hg remove file3) from repo and WC + * status-1/dir/file3 tracked, listed in .hgignore since rev 4, removed (hg remove file3) from repo and WC * (but entry in .hgignore left) in revision 5, and new file3 got created in WC. * Shall be reported as ignored when comparing against WC's parent, - * and both ignored and removed when comparing against revision 3 + * and both ignored and removed when comparing against revision 3 */ @Test public void testRemovedIgnoredInWC() throws Exception { @@ -317,7 +353,7 @@ /* * status/file1 was removed in cset 2. New file with the same name in the WC. - * Shall report 2 statuses (as cmdline hg does): unknown and removed when comparing against that revision. + * Shall report 2 statuses (as cmdline hg does): unknown and removed when comparing against that revision. */ @Test public void testNewFileWithSameNameAsDeletedOld() throws Exception { @@ -339,7 +375,7 @@ assertTrue(sc.get(file1).contains(Unknown)); assertTrue(sc.get(file1).size() == 1); } - + @Test public void testSubTreeStatus() throws Exception { repo = Configuration.get().find("status-1"); @@ -373,8 +409,7 @@ assertTrue(sc.get(Ignored).size() == 1); assertTrue(sc.get(Removed).size() == 2); } - - + @Test public void testSpecificFileStatus() throws Exception { repo = Configuration.get().find("status-1"); @@ -413,13 +448,13 @@ assertTrue(r.getIgnored().size() == 1); assertTrue(r.getModified().isEmpty()); } - + @Test public void testSameResultDirectPathVsMatcher() throws Exception { repo = Configuration.get().find("status-1"); final Path file3 = Path.create("dir/file3"); final Path file5 = Path.create("dir/file5"); - + HgWorkingCopyStatusCollector sc = HgWorkingCopyStatusCollector.create(repo, file3, file5); HgStatusCollector.Record r; sc.walk(WORKING_COPY, r = new HgStatusCollector.Record()); @@ -432,7 +467,7 @@ assertTrue(r.getRemoved().contains(file5)); assertTrue(r.getIgnored().contains(file3)); } - + @Test public void testScopeInHistoricalStatus() throws Exception { repo = Configuration.get().find("status-1"); @@ -459,7 +494,7 @@ assertTrue(sc.get(Added).size() == 1); } - + /** * Issue 22 */ @@ -473,34 +508,35 @@ // shall pass without exception assertTrue(sc.getErrors().isEmpty()); for (HgStatus.Kind k : HgStatus.Kind.values()) { - assertTrue("Kind " + k.name() + " shall be empty",sc.get(k).isEmpty()); + assertTrue("Kind " + k.name() + " shall be empty", sc.get(k).isEmpty()); } } - + /** * Issue 22, two subsequent commits that remove all repository files, each in a different branch. * Here's excerpt from my RevlogWriter utility: + * * <pre> * final List<String> filesList = Collections.singletonList("file1"); - * // - * file1.writeUncompressed(-1, -1, 0, 0, "garbage".getBytes()); - * // - * ManifestBuilder mb = new ManifestBuilder(); - * mb.reset().add("file1", file1.getRevision(0)); - * manifest.writeUncompressed(-1, -1, 0, 0, mb.build()); // manifest revision 0 - * final byte[] cset1 = buildChangelogEntry(manifest.getRevision(0), Collections.<String, String>emptyMap(), filesList, "Add a file"); - * changelog.writeUncompressed(-1, -1, 0, 0, cset1); - * // - * // pretend we delete all files in a branch 1 - * manifest.writeUncompressed(0, -1, 1, 1, new byte[0]); // manifest revision 1 - * final byte[] cset2 = buildChangelogEntry(manifest.getRevision(1), Collections.singletonMap("branch", "delete-all-1"), filesList, "Delete all files in a first branch"); - * changelog.writeUncompressed(0, -1, 1, 1, cset2); - * // - * // pretend we delete all files in a branch 2 (which is based on revision 0, same as branch 1) - * manifest.writeUncompressed(1, -1, 1 /*!!! here comes baseRevision != index * /, 2, new byte[0]); // manifest revision 2 - * final byte[] cset3 = buildChangelogEntry(manifest.getRevision(2), Collections.singletonMap("branch", "delete-all-2"), filesList, "Again delete all files but in another branch"); - * changelog.writeUncompressed(0, -1, 2, 2, cset3); - * </pre> + * // + * file1.writeUncompressed(-1, -1, 0, 0, "garbage".getBytes()); + * // + * ManifestBuilder mb = new ManifestBuilder(); + * mb.reset().add("file1", file1.getRevision(0)); + * manifest.writeUncompressed(-1, -1, 0, 0, mb.build()); // manifest revision 0 + * final byte[] cset1 = buildChangelogEntry(manifest.getRevision(0), Collections.<String, String>emptyMap(), filesList, "Add a file"); + * changelog.writeUncompressed(-1, -1, 0, 0, cset1); + * // + * // pretend we delete all files in a branch 1 + * manifest.writeUncompressed(0, -1, 1, 1, new byte[0]); // manifest revision 1 + * final byte[] cset2 = buildChangelogEntry(manifest.getRevision(1), Collections.singletonMap("branch", "delete-all-1"), filesList, "Delete all files in a first branch"); + * changelog.writeUncompressed(0, -1, 1, 1, cset2); + * // + * // pretend we delete all files in a branch 2 (which is based on revision 0, same as branch 1) + * manifest.writeUncompressed(1, -1, 1 /*!!! here comes baseRevision != index * /, 2, new byte[0]); // manifest revision 2 + * final byte[] cset3 = buildChangelogEntry(manifest.getRevision(2), Collections.singletonMap("branch", "delete-all-2"), filesList, "Again delete all files but in another branch"); + * changelog.writeUncompressed(0, -1, 2, 2, cset3); + * </pre> */ @Test public void testOnEmptyRepositoryWithAllFilesDeletedInBranch() throws Exception { @@ -512,12 +548,12 @@ // shall pass without exception assertTrue(sc.getErrors().isEmpty()); for (HgStatus.Kind k : HgStatus.Kind.values()) { - assertTrue("Kind " + k.name() + " shall be empty",sc.get(k).isEmpty()); + assertTrue("Kind " + k.name() + " shall be empty", sc.get(k).isEmpty()); } } - + /** - * Issue 23: HgInvalidRevisionException for svn imported repository (changeset 0 references nullid manifest) + * Issue 23: HgInvalidRevisionException for svn imported repository (changeset 0 references nullid manifest) */ @Test public void testImportedRepoWithOddManifestRevisions() throws Exception { @@ -529,32 +565,32 @@ // shall pass without exception assertTrue(sc.getErrors().isEmpty()); } - + /** * Issue 24: IllegalArgumentException in FilterDataAccess * There were two related defects in RevlogStream - * a) for compressedLen == 0, a byte was read and FilterDataAccess (of length 0, but it didn't help too much) was created - first byte happen to be 0. - * Patch was not applied (userDataAccess.isEmpty() check thanks to Issue 22) - * b) That FilterDataAccess (with 0 size represents patch more or less relevantly, but didn't represent actual revision) get successfully - * reassigned as lastUserData for the next iteration. And at the next step attempt to apply patch recorded in the next revision failed - * because baseRevisionData is 0 length FilterDataAccess + * a) for compressedLen == 0, a byte was read and FilterDataAccess (of length 0, but it didn't help too much) was created - first byte happen to be 0. + * Patch was not applied (userDataAccess.isEmpty() check thanks to Issue 22) + * b) That FilterDataAccess (with 0 size represents patch more or less relevantly, but didn't represent actual revision) get successfully + * reassigned as lastUserData for the next iteration. And at the next step attempt to apply patch recorded in the next revision failed + * because baseRevisionData is 0 length FilterDataAccess * - * Same applies for + * Same applies for * Issue 25: IOException: Underflow. Rewind past end of the slice in InflaterDataAccess * with the difference in separate .i and .d (thus not 0 but 'x' first byte was read) - * + * * Sample: - * status-5/file1 has 3 revisions, second is zero-length patch: - * Index Offset Packed Actual Base Rev - * 0: 0 8 7 0 - * DATA - * 1: 8 0 7 0 - * NO DATA - * 2: 8 14 6 0 - * PATCH + * status-5/file1 has 3 revisions, second is zero-length patch: + * Index Offset Packed Actual Base Rev + * 0: 0 8 7 0 + * DATA + * 1: 8 0 7 0 + * NO DATA + * 2: 8 14 6 0 + * PATCH */ @Test - public void testZeroLengthPatchAgainstNonEmptyBaseRev() throws Exception{ + public void testZeroLengthPatchAgainstNonEmptyBaseRev() throws Exception { repo = Configuration.get().find("status-5"); // pretend we modified files in the working copy // for HgWorkingCopyStatusCollector to go and retrieve its content from repository @@ -569,7 +605,7 @@ cmd.execute(sc); // shall pass without exception // - for (Map.Entry<Path,Status> e : sc.getErrors().entrySet()) { + for (Map.Entry<Path, Status> e : sc.getErrors().entrySet()) { System.out.printf("%s : (%s %s)\n", e.getKey(), e.getValue().getKind(), e.getValue().getMessage()); } assertTrue(sc.getErrors().isEmpty()); @@ -579,13 +615,13 @@ * Issue 26: UnsupportedOperationException when patching empty base revision * * Sample: - * status-5/file2 has 3 revisions, second is patch (complete revision content in a form of the patch) for empty base revision: - * Index Offset Packed Actual Base Rev - * 0: 0 0 0 0 - * NO DATA - * 1: 0 20 7 0 - * PATCH: 0..0, 7:garbage - * 2: 20 16 7 0 + * status-5/file2 has 3 revisions, second is patch (complete revision content in a form of the patch) for empty base revision: + * Index Offset Packed Actual Base Rev + * 0: 0 0 0 0 + * NO DATA + * 1: 0 20 7 0 + * PATCH: 0..0, 7:garbage + * 2: 20 16 7 0 */ @Test public void testPatchZeroLengthBaseRevision() throws Exception { @@ -600,13 +636,12 @@ cmd.execute(sc); // shall pass without exception // - for (Map.Entry<Path,Status> e : sc.getErrors().entrySet()) { + for (Map.Entry<Path, Status> e : sc.getErrors().entrySet()) { System.out.printf("%s : (%s %s)\n", e.getKey(), e.getValue().getKind(), e.getValue().getMessage()); } assertTrue(sc.getErrors().isEmpty()); } - /* * With warm-up of previous tests, 10 runs, time in milliseconds * 'hg status -A': Native client total 953 (95 per run), Java client 94 (9) @@ -614,13 +649,13 @@ * 'hg log --debug', 10 runs: Native client total 1766 (176 per run), Java client 78 (7) * * 18.02.2011 - * 'hg status -A --rev 3:80', 10 runs: Native client total 2000 (200 per run), Java client 250 (25) + * 'hg status -A --rev 3:80', 10 runs: Native client total 2000 (200 per run), Java client 250 (25) * 'hg log --debug', 10 runs: Native client total 2297 (229 per run), Java client 125 (12) * * 9.3.2011 (DataAccess instead of byte[] in ReflogStream.Inspector - * 'hg status -A', 10 runs: Native client total 1516 (151 per run), Java client 219 (21) - * 'hg status -A --rev 3:80', 10 runs: Native client total 1875 (187 per run), Java client 3187 (318) (!!! ???) - * 'hg log --debug', 10 runs: Native client total 2484 (248 per run), Java client 344 (34) + * 'hg status -A', 10 runs: Native client total 1516 (151 per run), Java client 219 (21) + * 'hg status -A --rev 3:80', 10 runs: Native client total 1875 (187 per run), Java client 3187 (318) (!!! ???) + * 'hg log --debug', 10 runs: Native client total 2484 (248 per run), Java client 344 (34) */ public void testPerformance() throws Exception { final int runs = 10; @@ -635,67 +670,71 @@ new HgStatusCommand(repo).all().base(3).revision(80).execute(r); } final long end = System.currentTimeMillis(); - System.out.printf("'hg status -A --rev 3:80', %d runs: Native client total %d (%d per run), Java client %d (%d)\n", runs, start2-start1, (start2-start1)/runs, end-start2, (end-start2)/runs); + System.out.printf("'hg status -A --rev 3:80', %d runs: Native client total %d (%d per run), Java client %d (%d)\n", runs, start2 - start1, (start2 - start1) / runs, end - start2, + (end - start2) / runs); } - private void report(String what, StatusCollector r) { - assertTrue(r.getErrors().isEmpty()); - reportNotEqual(what + "#MODIFIED", r.get(Modified), statusParser.getModified()); - reportNotEqual(what + "#ADDED", r.get(Added), statusParser.getAdded()); - reportNotEqual(what + "#REMOVED", r.get(Removed), statusParser.getRemoved()); - reportNotEqual(what + "#CLEAN", r.get(Clean), statusParser.getClean()); - reportNotEqual(what + "#IGNORED", r.get(Ignored), statusParser.getIgnored()); - reportNotEqual(what + "#MISSING", r.get(Missing), statusParser.getMissing()); - reportNotEqual(what + "#UNKNOWN", r.get(Unknown), statusParser.getUnknown()); - // TODO test copies - } + static class StatusReporter { + private final StatusOutputParser statusParser; + private final ErrorCollectorExt errorCollector; - private void report(String what, HgStatusCollector.Record r, StatusOutputParser statusParser) { - reportNotEqual(what + "#MODIFIED", r.getModified(), statusParser.getModified()); - reportNotEqual(what + "#ADDED", r.getAdded(), statusParser.getAdded()); - reportNotEqual(what + "#REMOVED", r.getRemoved(), statusParser.getRemoved()); - reportNotEqual(what + "#CLEAN", r.getClean(), statusParser.getClean()); - reportNotEqual(what + "#IGNORED", r.getIgnored(), statusParser.getIgnored()); - reportNotEqual(what + "#MISSING", r.getMissing(), statusParser.getMissing()); - reportNotEqual(what + "#UNKNOWN", r.getUnknown(), statusParser.getUnknown()); - List<Path> copiedKeyDiff = difference(r.getCopied().keySet(), statusParser.getCopied().keySet()); - HashMap<Path, String> copyDiff = new HashMap<Path,String>(); - if (copiedKeyDiff.isEmpty()) { - for (Path jk : r.getCopied().keySet()) { - Path jv = r.getCopied().get(jk); - if (statusParser.getCopied().containsKey(jk)) { - Path cmdv = statusParser.getCopied().get(jk); - if (!jv.equals(cmdv)) { - copyDiff.put(jk, jv + " instead of " + cmdv); + public StatusReporter(ErrorCollectorExt ec, StatusOutputParser sp) { + errorCollector = ec; + statusParser = sp; + } + + public void report(String what, StatusCollector r) { + errorCollector.assertTrue(what, r.getErrors().isEmpty()); + report(what, r.asStatusRecord()); + } + + public void report(String what, HgStatusCollector.Record r) { + reportNotEqual(what + "#MODIFIED", r.getModified(), statusParser.getModified()); + reportNotEqual(what + "#ADDED", r.getAdded(), statusParser.getAdded()); + reportNotEqual(what + "#REMOVED", r.getRemoved(), statusParser.getRemoved()); + reportNotEqual(what + "#CLEAN", r.getClean(), statusParser.getClean()); + reportNotEqual(what + "#IGNORED", r.getIgnored(), statusParser.getIgnored()); + reportNotEqual(what + "#MISSING", r.getMissing(), statusParser.getMissing()); + reportNotEqual(what + "#UNKNOWN", r.getUnknown(), statusParser.getUnknown()); + List<Path> copiedKeyDiff = difference(r.getCopied().keySet(), statusParser.getCopied().keySet()); + HashMap<Path, String> copyDiff = new HashMap<Path, String>(); + if (copiedKeyDiff.isEmpty()) { + for (Path jk : r.getCopied().keySet()) { + Path jv = r.getCopied().get(jk); + if (statusParser.getCopied().containsKey(jk)) { + Path cmdv = statusParser.getCopied().get(jk); + if (!jv.equals(cmdv)) { + copyDiff.put(jk, jv + " instead of " + cmdv); + } + } else { + copyDiff.put(jk, "ERRONEOUSLY REPORTED IN JAVA"); } - } else { - copyDiff.put(jk, "ERRONEOUSLY REPORTED IN JAVA"); } } + errorCollector.checkThat(what + "#Non-matching 'copied' keys: ", copiedKeyDiff, equalTo(Collections.<Path> emptyList())); + errorCollector.checkThat(what + "#COPIED", copyDiff, equalTo(Collections.<Path, String> emptyMap())); } - errorCollector.checkThat(what + "#Non-matching 'copied' keys: ", copiedKeyDiff, equalTo(Collections.<Path>emptyList())); - errorCollector.checkThat(what + "#COPIED", copyDiff, equalTo(Collections.<Path,String>emptyMap())); - } - - private <T extends Comparable<? super T>> void reportNotEqual(String what, Collection<T> l1, Collection<T> l2) { -// List<T> diff = difference(l1, l2); -// errorCollector.checkThat(what, diff, equalTo(Collections.<T>emptyList())); - ArrayList<T> sl1 = new ArrayList<T>(l1); - Collections.sort(sl1); - ArrayList<T> sl2 = new ArrayList<T>(l2); - Collections.sort(sl2); - errorCollector.checkThat(what, sl1, equalTo(sl2)); - } - private static <T> List<T> difference(Collection<T> l1, Collection<T> l2) { - LinkedList<T> result = new LinkedList<T>(l2); - for (T t : l1) { - if (l2.contains(t)) { - result.remove(t); - } else { - result.add(t); + private <T extends Comparable<? super T>> void reportNotEqual(String what, Collection<T> l1, Collection<T> l2) { + // List<T> diff = difference(l1, l2); + // errorCollector.checkThat(what, diff, equalTo(Collections.<T>emptyList())); + ArrayList<T> sl1 = new ArrayList<T>(l1); + Collections.sort(sl1); + ArrayList<T> sl2 = new ArrayList<T>(l2); + Collections.sort(sl2); + errorCollector.checkThat(what, sl1, equalTo(sl2)); + } + + public static <T> List<T> difference(Collection<T> l1, Collection<T> l2) { + LinkedList<T> result = new LinkedList<T>(l2); + for (T t : l1) { + if (l2.contains(t)) { + result.remove(t); + } else { + result.add(t); + } } + return result; } - return result; } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/org/tmatesoft/hg/test/TestSubrepo.java Fri Apr 27 20:57:20 2012 +0200 @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2012 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.test; + +import static org.junit.Assert.assertEquals; +import static org.tmatesoft.hg.repo.HgRepository.TIP; + +import java.io.File; +import java.util.List; + +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.tmatesoft.hg.core.HgStatusCommand; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgSubrepoLocation; +import org.tmatesoft.hg.repo.HgSubrepoLocation.Kind; +import org.tmatesoft.hg.util.Path; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class TestSubrepo { + + @Rule + public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); + + private HgRepository repo; + private StatusOutputParser statusParser; + private ExecHelper eh; + + /* + * Layout of status-subrepo: + * first/ regular subrepo + * dir/second/ subrepo nested under a tracked folder + * third/ subrepo with another one + * third/fourth 2nd level of subrepo nesting (registered in third/.hgsub) + * third/fourth/file4_1 A, added file + * third/fourth/file4_2 ?, untracked file + * fifth/ nested repository not yet registered in .hgsub + * fifth/file5 untracked file + * + * Curiously, fifth/ shall not be reported (neither 'hg status -AS' nor '-A' don't report + * anything for it, no '?' for the file5 in particular. Once fifth/.hg/ is removed, + * file5 gets its ? as one would expect) + */ + + @Test + public void testAccessAPI() throws Exception { + repo = Configuration.get().find("status-subrepo"); + List<HgSubrepoLocation> subrepositories = repo.getSubrepositories(); + assertEquals(3, subrepositories.size()); + checkHgSubrepo(Path.create("first/"), true, repo, subrepositories.get(0)); + checkHgSubrepo(Path.create("dir/second/"), true, repo, subrepositories.get(1)); + checkHgSubrepo(Path.create("third/"), false, repo, subrepositories.get(2)); + } + + private void checkHgSubrepo(Path expectedLocation, boolean isCommitted, HgRepository topRepo, HgSubrepoLocation l) throws Exception { + errorCollector.assertEquals(expectedLocation, l.getLocation()); + errorCollector.assertEquals(Kind.Hg, l.getType()); + if (isCommitted) { + errorCollector.assertTrue(l.isCommitted()); + errorCollector.assertTrue(l.getRevision() != null); + errorCollector.assertTrue(!l.getRevision().isNull()); + } else { + errorCollector.assertTrue(!l.isCommitted()); + errorCollector.assertTrue(l.getRevision() == null); + } + errorCollector.assertEquals(topRepo, l.getOwner()); + HgRepository r = l.getRepo(); + String expectedSubRepoLoc = new File(topRepo.getLocation(), expectedLocation.toString()).toString(); + errorCollector.assertEquals(expectedSubRepoLoc, r.getLocation()); + errorCollector.assertTrue(r.getChangelog().getRevisionCount() > 0); + if (isCommitted) { + errorCollector.assertEquals(r.getChangelog().getRevision(TIP), l.getRevision()); + } + } + + @Test + @Ignore("StatusCommand doesn't suport subrepositories yet") + public void testStatusCommand() throws Exception { + repo = Configuration.get().find("status-subrepo"); + statusParser = new StatusOutputParser(); + eh = new ExecHelper(statusParser, repo.getWorkingDir()); + TestStatus.StatusReporter sr = new TestStatus.StatusReporter(errorCollector, statusParser); + HgStatusCommand cmd = new HgStatusCommand(repo).all(); + TestStatus.StatusCollector sc; + + eh.run("hg", "status", "-A", "-S"); + cmd.subrepo(true); + cmd.execute(sc = new TestStatus.StatusCollector()); + sr.report("status -A -S", sc); + + eh.run("hg", "status", "-A", "-S"); + cmd.subrepo(false); + cmd.execute(sc = new TestStatus.StatusCollector()); + sr.report("status -A", sc); + + } + +}