# HG changeset patch # User Artem Tikhomirov # Date 1335553040 -7200 # Node ID 6865eb74288350510c77c7b5ab217d5d910d4ca3 # Parent 2a08466838d370d9ef27408517ed0f8f4a222982 Tests for subrepo API, refactor status tests for reuse, better subrepos API diff -r 2a08466838d3 -r 6865eb742883 build.xml --- 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 @@ - + @@ -92,6 +92,7 @@ + diff -r 2a08466838d3 -r 6865eb742883 cmdline/org/tmatesoft/hg/console/Main.java --- 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()); } } } diff -r 2a08466838d3 -r 6865eb742883 src/org/tmatesoft/hg/internal/SubrepoManager.java --- 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 res = new LinkedList(); + HgInternals hgRepoInternal = new HgInternals(repo); while ((line = br.readLine()) != null) { int sep = line.indexOf('='); if (sep == -1) { @@ -84,7 +90,7 @@ // to have separate String instances (new String(line.substring())) String key = line.substring(0, sep).trim(); String value = line.substring(sep + 1).trim(); - if (value.length() == 0) { + if (key.length() == 0 || value.length() == 0) { // XXX log bad line? continue; } @@ -100,7 +106,12 @@ } } // TODO respect paths mappings in config file - HgSubrepoLocation loc = new HgSubrepoLocation(repo, key, value, kind, substate.get(key)); + // + // apparently, key value can't end with '/', `hg commit` fails if it does: + // abort: path ends in directory separator: fourth/ + Path p = Path.create(key.charAt(key.length()-1) == '/' ? key : key + '/'); + String revValue = substate.get(key); + HgSubrepoLocation loc = hgRepoInternal.newSubrepo(p, value, kind, revValue == null ? null : Nodeid.fromAscii(revValue)); res.add(loc); } return Arrays.asList(res.toArray(new HgSubrepoLocation[res.size()])); @@ -110,6 +121,7 @@ } private Map readState(BufferedReader br) throws IOException { + // TODO reuse for other files with format, like .hgtags HashMap rv = new HashMap(); try { String line; diff -r 2a08466838d3 -r 6865eb742883 src/org/tmatesoft/hg/repo/HgInternals.java --- 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 + *

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

This class gives access to repository internals, and holds methods that I'm not confident have to be widely accessible * Debug helper, to access otherwise restricted (package-local) methods * * @author Artem Tikhomirov @@ -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(); diff -r 2a08466838d3 -r 6865eb742883 src/org/tmatesoft/hg/repo/HgRepositoryFiles.java --- 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; diff -r 2a08466838d3 -r 6865eb742883 src/org/tmatesoft/hg/repo/HgStatusInspector.java --- 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 this.added(fnameAdded) + * method invoked in this case, that is {@link #added(Path)} method is NOT invoked along with it. + * Note, however, {@link #removed(Path)} IS invoked for the removed file in all cases, regardless whether it's a mere rename or not. + *

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

If copied files of no interest, it is implementation responsibility to delegate to this.added(fnameAdded) */ void copied(Path fnameOrigin, Path fnameAdded); void removed(Path fname); diff -r 2a08466838d3 -r 6865eb742883 src/org/tmatesoft/hg/repo/HgSubrepoLocation.java --- 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 null + */ + /*package-local*/ HgSubrepoLocation(HgRepository parentRepo, Path repoLocation, String actualLocation, Kind type, Nodeid revision) { owner = parentRepo; - location = Path.create(repoLocation); + location = repoLocation; source = actualLocation; kind = type; revInfo = revision; } - // as defined in .hgsub, key value + /** + * Sub-repository's location within owning repository, always directory, path/to/nested. + *

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

Note, this revision belongs to the nested repository history, not that of owning repository. + * + * @return revision of the nested repository, or null if not yet committed + */ + public Nodeid getRevision() { return revInfo; } /** - * @return whether this sub repository is known only locally + * Answers whether this sub repository has ever been part of a commit of the owner repository + * + * @return true if owning repository records {@link #getRevision() revision} of this sub-repository */ public boolean isCommitted() { return revInfo != null; } /** - * @return true when there are local changes in the sub repository + * Answers whether there are local changes in the sub-repository, + * @return true if it's dirty */ public boolean hasChanges() { throw HgRepository.notImplemented(); } - -// public boolean isLocal() { -// } - + + /** + * Access repository that owns nested one described by this object + */ public HgRepository getOwner() { return owner; } /** + * Access nested repository as a full-fledged Mercurial repository * * @return object to access sub-repository * @throws HgRepositoryNotFoundException if failed to find repository diff -r 2a08466838d3 -r 6865eb742883 test-data/test-repos.jar Binary file test-data/test-repos.jar has changed diff -r 2a08466838d3 -r 6865eb742883 test/org/tmatesoft/hg/test/ErrorCollectorExt.java --- 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() { public Object call() throws Exception { @@ -59,4 +64,8 @@ } }); } + + public void assertEquals(T expected, T actual) { + checkThat(null, actual, CoreMatchers.equalTo(expected)); + } } \ No newline at end of file diff -r 2a08466838d3 -r 6865eb742883 test/org/tmatesoft/hg/test/TestStatus.java --- 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 ; 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> kind2names = new TreeMap>(); private final Map> name2kinds = new TreeMap>(); private final Map name2error = new LinkedHashMap(); + private final Map new2oldName = new LinkedHashMap(); public void status(HgStatus s) { List l = kind2names.get(s.getKind()); @@ -189,6 +190,9 @@ kind2names.put(s.getKind(), l = new LinkedList()); } l.add(s.getPath()); + if (s.isCopy()) { + new2oldName.put(s.getPath(), s.getOriginalPath()); + } // List k = name2kinds.get(s.getPath()); if (k == null) { @@ -196,24 +200,56 @@ } k.add(s.getKind()); } - + public void error(Path file, Status s) { name2error.put(file, s); } - + public List get(Kind k) { List rv = kind2names.get(k); - return rv == null ? Collections.emptyList() : rv; + return rv == null ? Collections. emptyList() : rv; } - + public List get(Path p) { List rv = name2kinds.get(p); - return rv == null ? Collections.emptyList() : rv; + return rv == null ? Collections. emptyList() : rv; } - + public Map getErrors() { return name2error; } + + public HgStatusCollector.Record asStatusRecord() { + HgStatusCollector.Record rv = new HgStatusCollector.Record(); + for (Path p : get(Modified)) { + rv.modified(p); + } + for (Path p : get(Added)) { + if (!new2oldName.containsKey(p)) { + // new files that are result of a copy get reported separately, below + rv.added(p); + } + } + for (Path p : get(Removed)) { + rv.removed(p); + } + for (Path p : get(Clean)) { + rv.clean(p); + } + for (Path p : get(Ignored)) { + rv.ignored(p); + } + for (Path p : get(Missing)) { + rv.missing(p); + } + for (Path p : get(Unknown)) { + rv.unknown(p); + } + for (Map.Entry e : new2oldName.entrySet()) { + rv.copied(e.getValue(), e.getKey()); + } + return rv; + } } /* @@ -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: + * *
 	 * 		final List filesList = Collections.singletonList("file1");
-	 *	//
-	 *	file1.writeUncompressed(-1, -1, 0, 0, "garbage".getBytes());
-	 *	//
-	 *	ManifestBuilder mb = new ManifestBuilder();
-	 *	mb.reset().add("file1", file1.getRevision(0));
-	 *	manifest.writeUncompressed(-1, -1, 0, 0, mb.build()); // manifest revision 0
-	 *	final byte[] cset1 = buildChangelogEntry(manifest.getRevision(0), Collections.emptyMap(), filesList, "Add a file");
-	 *	changelog.writeUncompressed(-1, -1, 0, 0, cset1);
-	 *	//
-	 *	// pretend we delete all files in a branch 1
-	 *	manifest.writeUncompressed(0, -1, 1, 1, new byte[0]); // manifest revision 1
-	 *	final byte[] cset2 = buildChangelogEntry(manifest.getRevision(1), Collections.singletonMap("branch", "delete-all-1"), filesList, "Delete all files in a first branch");
-	 *	 changelog.writeUncompressed(0, -1, 1, 1, cset2);
-	 *	//
-	 *	// pretend we delete all files in a branch 2 (which is based on revision 0, same as branch 1)
-	 *	manifest.writeUncompressed(1, -1, 1 /*!!! here comes baseRevision != index * /, 2, new byte[0]); // manifest revision 2
-	 *	final byte[] cset3 = buildChangelogEntry(manifest.getRevision(2), Collections.singletonMap("branch", "delete-all-2"), filesList, "Again delete all files but in another branch");
-	 *	changelog.writeUncompressed(0, -1, 2, 2, cset3);
-	 * 
+ * // + * file1.writeUncompressed(-1, -1, 0, 0, "garbage".getBytes()); + * // + * ManifestBuilder mb = new ManifestBuilder(); + * mb.reset().add("file1", file1.getRevision(0)); + * manifest.writeUncompressed(-1, -1, 0, 0, mb.build()); // manifest revision 0 + * final byte[] cset1 = buildChangelogEntry(manifest.getRevision(0), Collections.emptyMap(), filesList, "Add a file"); + * changelog.writeUncompressed(-1, -1, 0, 0, cset1); + * // + * // pretend we delete all files in a branch 1 + * manifest.writeUncompressed(0, -1, 1, 1, new byte[0]); // manifest revision 1 + * final byte[] cset2 = buildChangelogEntry(manifest.getRevision(1), Collections.singletonMap("branch", "delete-all-1"), filesList, "Delete all files in a first branch"); + * changelog.writeUncompressed(0, -1, 1, 1, cset2); + * // + * // pretend we delete all files in a branch 2 (which is based on revision 0, same as branch 1) + * manifest.writeUncompressed(1, -1, 1 /*!!! here comes baseRevision != index * /, 2, new byte[0]); // manifest revision 2 + * final byte[] cset3 = buildChangelogEntry(manifest.getRevision(2), Collections.singletonMap("branch", "delete-all-2"), filesList, "Again delete all files but in another branch"); + * changelog.writeUncompressed(0, -1, 2, 2, cset3); + * */ @Test public void testOnEmptyRepositoryWithAllFilesDeletedInBranch() throws Exception { @@ -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 e : sc.getErrors().entrySet()) { + for (Map.Entry e : sc.getErrors().entrySet()) { System.out.printf("%s : (%s %s)\n", e.getKey(), e.getValue().getKind(), e.getValue().getMessage()); } assertTrue(sc.getErrors().isEmpty()); @@ -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 e : sc.getErrors().entrySet()) { + for (Map.Entry e : sc.getErrors().entrySet()) { System.out.printf("%s : (%s %s)\n", e.getKey(), e.getValue().getKind(), e.getValue().getMessage()); } assertTrue(sc.getErrors().isEmpty()); } - /* * 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 copiedKeyDiff = difference(r.getCopied().keySet(), statusParser.getCopied().keySet()); - HashMap copyDiff = new HashMap(); - if (copiedKeyDiff.isEmpty()) { - for (Path jk : r.getCopied().keySet()) { - Path jv = r.getCopied().get(jk); - if (statusParser.getCopied().containsKey(jk)) { - Path cmdv = statusParser.getCopied().get(jk); - if (!jv.equals(cmdv)) { - copyDiff.put(jk, jv + " instead of " + cmdv); + public StatusReporter(ErrorCollectorExt ec, StatusOutputParser sp) { + errorCollector = ec; + statusParser = sp; + } + + public void report(String what, StatusCollector r) { + errorCollector.assertTrue(what, r.getErrors().isEmpty()); + report(what, r.asStatusRecord()); + } + + public void report(String what, HgStatusCollector.Record r) { + reportNotEqual(what + "#MODIFIED", r.getModified(), statusParser.getModified()); + reportNotEqual(what + "#ADDED", r.getAdded(), statusParser.getAdded()); + reportNotEqual(what + "#REMOVED", r.getRemoved(), statusParser.getRemoved()); + reportNotEqual(what + "#CLEAN", r.getClean(), statusParser.getClean()); + reportNotEqual(what + "#IGNORED", r.getIgnored(), statusParser.getIgnored()); + reportNotEqual(what + "#MISSING", r.getMissing(), statusParser.getMissing()); + reportNotEqual(what + "#UNKNOWN", r.getUnknown(), statusParser.getUnknown()); + List copiedKeyDiff = difference(r.getCopied().keySet(), statusParser.getCopied().keySet()); + HashMap copyDiff = new HashMap(); + if (copiedKeyDiff.isEmpty()) { + for (Path jk : r.getCopied().keySet()) { + Path jv = r.getCopied().get(jk); + if (statusParser.getCopied().containsKey(jk)) { + Path cmdv = statusParser.getCopied().get(jk); + if (!jv.equals(cmdv)) { + copyDiff.put(jk, jv + " instead of " + cmdv); + } + } else { + copyDiff.put(jk, "ERRONEOUSLY REPORTED IN JAVA"); } - } else { - copyDiff.put(jk, "ERRONEOUSLY REPORTED IN JAVA"); } } + errorCollector.checkThat(what + "#Non-matching 'copied' keys: ", copiedKeyDiff, equalTo(Collections. emptyList())); + errorCollector.checkThat(what + "#COPIED", copyDiff, equalTo(Collections. emptyMap())); } - errorCollector.checkThat(what + "#Non-matching 'copied' keys: ", copiedKeyDiff, equalTo(Collections.emptyList())); - errorCollector.checkThat(what + "#COPIED", copyDiff, equalTo(Collections.emptyMap())); - } - - private > void reportNotEqual(String what, Collection l1, Collection l2) { -// List diff = difference(l1, l2); -// errorCollector.checkThat(what, diff, equalTo(Collections.emptyList())); - ArrayList sl1 = new ArrayList(l1); - Collections.sort(sl1); - ArrayList sl2 = new ArrayList(l2); - Collections.sort(sl2); - errorCollector.checkThat(what, sl1, equalTo(sl2)); - } - private static List difference(Collection l1, Collection l2) { - LinkedList result = new LinkedList(l2); - for (T t : l1) { - if (l2.contains(t)) { - result.remove(t); - } else { - result.add(t); + private > void reportNotEqual(String what, Collection l1, Collection l2) { + // List diff = difference(l1, l2); + // errorCollector.checkThat(what, diff, equalTo(Collections.emptyList())); + ArrayList sl1 = new ArrayList(l1); + Collections.sort(sl1); + ArrayList sl2 = new ArrayList(l2); + Collections.sort(sl2); + errorCollector.checkThat(what, sl1, equalTo(sl2)); + } + + public static List difference(Collection l1, Collection l2) { + LinkedList result = new LinkedList(l2); + for (T t : l1) { + if (l2.contains(t)) { + result.remove(t); + } else { + result.add(t); + } } + return result; } - return result; } } diff -r 2a08466838d3 -r 6865eb742883 test/org/tmatesoft/hg/test/TestSubrepo.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/org/tmatesoft/hg/test/TestSubrepo.java 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 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); + + } + +}