Mercurial > hg4j
changeset 229:1ec6b327a6ac
Scope for status reworked: explicit files or a general matcher
author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
---|---|
date | Tue, 31 May 2011 05:23:07 +0200 |
parents | fffe4f882248 |
children | 0dd9da7489dc |
files | cmdline/org/tmatesoft/hg/console/Main.java src/org/tmatesoft/hg/core/HgStatusCommand.java src/org/tmatesoft/hg/internal/PathGlobMatcher.java src/org/tmatesoft/hg/internal/PathScope.java src/org/tmatesoft/hg/repo/HgInternals.java src/org/tmatesoft/hg/repo/HgRepository.java src/org/tmatesoft/hg/repo/HgStatusCollector.java src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java src/org/tmatesoft/hg/util/FileWalker.java src/org/tmatesoft/hg/util/Path.java test/org/tmatesoft/hg/test/TestStatus.java |
diffstat | 11 files changed, 418 insertions(+), 89 deletions(-) [+] |
line wrap: on
line diff
--- a/cmdline/org/tmatesoft/hg/console/Main.java Fri May 27 03:01:26 2011 +0200 +++ b/cmdline/org/tmatesoft/hg/console/Main.java Tue May 31 05:23:07 2011 +0200 @@ -28,6 +28,7 @@ import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.internal.ByteArrayChannel; import org.tmatesoft.hg.internal.DigestHelper; +import org.tmatesoft.hg.internal.PathGlobMatcher; import org.tmatesoft.hg.repo.HgBranches; import org.tmatesoft.hg.repo.HgDataFile; import org.tmatesoft.hg.repo.HgInternals; @@ -75,9 +76,26 @@ private void testFileStatus() { // final Path path = Path.create("src/org/tmatesoft/hg/util/"); - final Path path = Path.create("src/org/tmatesoft/hg/internal/Experimental.java"); - HgWorkingCopyStatusCollector wcsc = HgWorkingCopyStatusCollector.create(hgRepo, path); +// final Path path = Path.create("src/org/tmatesoft/hg/internal/Experimental.java"); +// final Path path = Path.create("dir/file3"); +// HgWorkingCopyStatusCollector wcsc = HgWorkingCopyStatusCollector.create(hgRepo, path); + HgWorkingCopyStatusCollector wcsc = HgWorkingCopyStatusCollector.create(hgRepo, new PathGlobMatcher("*")); wcsc.walk(TIP, new StatusDump()); + new HgManifestCommand(hgRepo).dirs(true).revision(TIP).execute(new HgManifestCommand.Handler() { + + public void file(FileRevision fileRevision) { + } + + public void end(Nodeid manifestRevision) { + } + + public void dir(Path p) { + System.out.println(p); + } + + public void begin(Nodeid manifestRevision) { + } + }); } private void dumpBranches() {
--- a/src/org/tmatesoft/hg/core/HgStatusCommand.java Fri May 27 03:01:26 2011 +0200 +++ b/src/org/tmatesoft/hg/core/HgStatusCommand.java Tue May 31 05:23:07 2011 +0200 @@ -29,7 +29,6 @@ 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. @@ -41,7 +40,8 @@ private final HgRepository repo; private int startRevision = TIP; - private int endRevision = WORKING_COPY; + private int endRevision = WORKING_COPY; + private Path.Matcher scope; private final Mediator mediator = new Mediator(); @@ -146,8 +146,8 @@ * @param pathMatcher - matcher to use, pass <code>null/<code> to reset * @return <code>this</code> for convenience */ - public HgStatusCommand match(Path.Matcher pathMatcher) { - mediator.matcher = pathMatcher; + public HgStatusCommand match(Path.Matcher scopeMatcher) { + scope = scopeMatcher; return this; } @@ -176,10 +176,11 @@ // 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); + HgWorkingCopyStatusCollector wcsc = scope != null ? HgWorkingCopyStatusCollector.create(repo, scope) : new HgWorkingCopyStatusCollector(repo); wcsc.setBaseRevisionCollector(sc); wcsc.walk(startRevision, mediator); } else { + sc.setScope(scope); // explicitly set, even if null - would be handy once we reuse StatusCollector if (startRevision == TIP) { sc.change(endRevision, mediator); } else { @@ -204,7 +205,6 @@ boolean needClean; boolean needIgnored; boolean needCopies; - Matcher matcher; Handler handler; private ChangelogHelper logHelper; @@ -227,59 +227,43 @@ public void modified(Path fname) { if (needModified) { - if (matcher == null || matcher.accept(fname)) { - handler.handleStatus(new HgStatus(Modified, fname, logHelper)); - } + 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)); - } + 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)); - } + 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)); - } + // 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)); - } + 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)); - } + 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)); - } + 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)); - } + handler.handleStatus(new HgStatus(Ignored, fname, logHelper)); } } }
--- a/src/org/tmatesoft/hg/internal/PathGlobMatcher.java Fri May 27 03:01:26 2011 +0200 +++ b/src/org/tmatesoft/hg/internal/PathGlobMatcher.java Tue May 31 05:23:07 2011 +0200 @@ -39,7 +39,7 @@ String[] regexp = new String[globPatterns.length]; //deliberately let fail with NPE int i = 0; for (String s : globPatterns) { - regexp[i] = glob2regexp(s); + regexp[i++] = glob2regexp(s); } try { delegate = new PathRegexpMatcher(regexp); @@ -53,21 +53,28 @@ // 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) { + private static String glob2regexp(String glob) { // FIXME TESTS NEEDED!!! 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() + if (glob.length() > 2 && glob.charAt(end) == '*' && glob.charAt(end - 1) == '.') { + end-=2; + } + boolean needLineEndMatch = true;//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) != '*') { +// 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 + if (i < end && glob.charAt(i+1) == '*') { // any char, including path segment separator sb.append(".*?"); i++; + if (i < end && glob.charAt(i+1) == '/') { + sb.append("/?"); + i++; + } } else { // just path segments sb.append("[^/]*?");
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/PathScope.java Tue May 31 05:23:07 2011 +0200 @@ -0,0 +1,84 @@ +/* + * 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.ArrayList; + +import org.tmatesoft.hg.util.Path; + +/** + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class PathScope implements Path.Matcher { + private final Path[] files; + private final Path[] dirs; + private final boolean recursiveDirs; + + public PathScope(boolean recursiveDirs, Path... paths) { + if (paths == null) { + throw new IllegalArgumentException(); + } + this.recursiveDirs = recursiveDirs; + ArrayList<Path> f = new ArrayList<Path>(5); + ArrayList<Path> d = new ArrayList<Path>(5); + for (Path p : paths) { + if (p.isDirectory()) { + d.add(p); + } else { + f.add(p); + } + } + files = f.toArray(new Path[f.size()]); + dirs = d.toArray(new Path[d.size()]); + } + + public boolean accept(Path path) { + if (path.isDirectory()) { + // either equals to or parent of a directory we know about. + // If recursiveDirs, accept also if nested to one of our directories. + // If one of configured files is nested under the path, accept. + for (Path d : dirs) { + switch(d.compareWith(path)) { + case Same : return true; + case Nested : return true; + case Parent : return recursiveDirs; + } + } + for (Path f : files) { + if (f.compareWith(path) == Path.CompareResult.Nested) { + return true; + } + } + } else { + for (Path d : dirs) { + if (d.compareWith(path) == Path.CompareResult.Parent) { + return true; + } + } + for (Path f : files) { + if (f.equals(path)) { + return true; + } + } + // either lives in a directory in out scope + // or there's a file that matches the path + } + // TODO Auto-generated method stub + return false; + } +} \ No newline at end of file
--- a/src/org/tmatesoft/hg/repo/HgInternals.java Fri May 27 03:01:26 2011 +0200 +++ b/src/org/tmatesoft/hg/repo/HgInternals.java Tue May 31 05:23:07 2011 +0200 @@ -23,18 +23,24 @@ import java.net.UnknownHostException; import org.tmatesoft.hg.internal.ConfigFile; +import org.tmatesoft.hg.internal.Experimental; +import org.tmatesoft.hg.internal.RelativePathRewrite; +import org.tmatesoft.hg.util.FileIterator; +import org.tmatesoft.hg.util.FileWalker; import org.tmatesoft.hg.util.Path; +import org.tmatesoft.hg.util.PathRewrite; /** * DO NOT USE THIS CLASS, INTENDED FOR TESTING PURPOSES. * + * This class gives access to repository internals, and holds methods that I'm not confident have to be widely accessible * Debug helper, to access otherwise restricted (package-local) methods * * @author Artem Tikhomirov * @author TMate Software Ltd. - */ +@Experimental(reason="Perhaps, shall split methods with debug purpose from methods that are experimental API") public class HgInternals { private final HgRepository repo; @@ -86,6 +92,17 @@ return username; } } + + @Experimental(reason="Don't want to expose io.File from HgRepository; need to create FileIterator for working dir. Need a place to keep that code") + /*package-local*/ FileIterator createWorkingDirWalker(Path.Matcher workindDirScope) { + File repoRoot = repo.getRepositoryRoot().getParentFile(); + Path.Source pathSrc = new Path.SimpleSource(new PathRewrite.Composite(new RelativePathRewrite(repoRoot), repo.getToRepoPathHelper())); + // Impl note: simple source is enough as files in the working dir are all unique + // even if they might get reused (i.e. after FileIterator#reset() and walking once again), + // path caching is better to be done in the code which knows that path are being reused + return new FileWalker(repoRoot, pathSrc, workindDirScope); + } + // 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) {
--- a/src/org/tmatesoft/hg/repo/HgRepository.java Fri May 27 03:01:26 2011 +0200 +++ b/src/org/tmatesoft/hg/repo/HgRepository.java Tue May 31 05:23:07 2011 +0200 @@ -27,11 +27,8 @@ 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; import org.tmatesoft.hg.util.ProgressSupport; @@ -217,16 +214,6 @@ 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)
--- a/src/org/tmatesoft/hg/repo/HgStatusCollector.java Fri May 27 03:01:26 2011 +0200 +++ b/src/org/tmatesoft/hg/repo/HgStatusCollector.java Tue May 31 05:23:07 2011 +0200 @@ -56,7 +56,7 @@ private final Pool<Nodeid> cacheNodes; private final Pool<String> cacheFilenames; // XXX in fact, need to think if use of PathPool directly instead is better solution private final ManifestRevisionInspector emptyFakeState; - private Path.Matcher scope; + private Path.Matcher scope = new Path.Matcher.Any(); public HgStatusCollector(HgRepository hgRepo) { @@ -152,7 +152,15 @@ public void setPathPool(PathPool pathPool) { this.pathPool = pathPool; } - + + /** + * Limit activity of the collector to certain sub-tree of the repository. + * @param scopeMatcher tells whether collector shall report specific path, can be <code>null</code> + */ + public void setScope(Path.Matcher scopeMatcher) { + // do not assign null, ever + scope = scopeMatcher == null ? new Path.Matcher.Any() : scopeMatcher; + } // hg status --change <rev> public void change(int rev, HgStatusInspector inspector) { @@ -217,16 +225,7 @@ r2 = get(rev2); PathPool pp = getPathPool(); - TreeSet<String> r1Files = new TreeSet<String>(r1.files()); - class MatchAny implements Path.Matcher { - public boolean accept(Path path) { - return true; - } - }; - if (scope == null) { - scope = new MatchAny(); // FIXME configure from outside - } for (String fname : r2.files()) { final Path r2filePath = pp.path(fname); if (!scope.accept(r2filePath)) {
--- a/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java Fri May 27 03:01:26 2011 +0200 +++ b/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java Tue May 31 05:23:07 2011 +0200 @@ -19,14 +19,13 @@ import static java.lang.Math.max; import static java.lang.Math.min; 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.io.File; import java.io.FileInputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; +import java.util.ArrayList; import java.util.Collections; import java.util.NoSuchElementException; import java.util.Set; @@ -38,7 +37,7 @@ import org.tmatesoft.hg.internal.ByteArrayChannel; import org.tmatesoft.hg.internal.Experimental; import org.tmatesoft.hg.internal.FilterByteChannel; -import org.tmatesoft.hg.internal.RelativePathRewrite; +import org.tmatesoft.hg.internal.PathScope; import org.tmatesoft.hg.repo.HgStatusCollector.ManifestRevisionInspector; import org.tmatesoft.hg.util.ByteChannel; import org.tmatesoft.hg.util.CancelledException; @@ -62,10 +61,11 @@ private PathPool pathPool; public HgWorkingCopyStatusCollector(HgRepository hgRepo) { - this(hgRepo, hgRepo.createWorkingDirWalker()); + this(hgRepo, new HgInternals(hgRepo).createWorkingDirWalker(null)); } - HgWorkingCopyStatusCollector(HgRepository hgRepo, FileIterator hgRepoWalker) { + // FIXME document cons + public HgWorkingCopyStatusCollector(HgRepository hgRepo, FileIterator hgRepoWalker) { repo = hgRepo; repoWalker = hgRepoWalker; } @@ -136,7 +136,6 @@ repoWalker.next(); Path fname = pp.path(repoWalker.name()); File f = repoWalker.file(); - assert f.isFile(); if (!f.exists()) { // file coming from iterator doesn't exist. if (knownEntries.remove(fname.toString())) { @@ -167,6 +166,7 @@ } continue; } + assert f.isFile(); if (knownEntries.remove(fname.toString())) { // tracked file. // modified, added, removed, clean @@ -399,11 +399,59 @@ return null; } - @Experimental(reason="There's intention to support status query with multiple files/dirs, API might get changed") - public static HgWorkingCopyStatusCollector create(HgRepository hgRepo, Path file) { - FileIterator fi = file.isDirectory() ? new DirFileIterator(hgRepo, file) : new FileListIterator(hgRepo.getRepositoryRoot().getParentFile(), file); + /** + * Configure status collector to consider only subset of a working copy tree. Tries to be as effective as possible, and to + * traverse only relevant part of working copy on the filesystem. + * + * @param hgRepo repository + * @param paths repository-relative files and/or directories. Directories are processed recursively. + * + * @return new instance of {@link HgWorkingCopyStatusCollector}, ready to {@link #walk(int, HgStatusInspector) walk} associated working copy + */ + @Experimental(reason="Provisional API") + public static HgWorkingCopyStatusCollector create(HgRepository hgRepo, Path... paths) { + ArrayList<Path> f = new ArrayList<Path>(5); + ArrayList<Path> d = new ArrayList<Path>(5); + for (Path p : paths) { + if (p.isDirectory()) { + d.add(p); + } else { + f.add(p); + } + } +// final Path[] dirs = f.toArray(new Path[d.size()]); + if (d.isEmpty()) { + final Path[] files = f.toArray(new Path[f.size()]); + FileIterator fi = new FileListIterator(hgRepo.getRepositoryRoot().getParentFile(), files); + return new HgWorkingCopyStatusCollector(hgRepo, fi); + } + // + + //FileIterator fi = file.isDirectory() ? new DirFileIterator(hgRepo, file) : new FileListIterator(, file); + FileIterator fi = new HgInternals(hgRepo).createWorkingDirWalker(new PathScope(true, paths)); return new HgWorkingCopyStatusCollector(hgRepo, fi); } + + /** + * Configure collector object to calculate status for matching files only. + * This method may be less effective than explicit list of files as it iterates over whole repository + * (thus supplied matcher doesn't need to care if directories to files in question are also in scope, + * see {@link FileWalker#FileWalker(File, Path.Source, Path.Matcher)}) + * + * @return new instance of {@link HgWorkingCopyStatusCollector}, ready to {@link #walk(int, HgStatusInspector) walk} associated working copy + */ + @Experimental(reason="Provisional API. May add boolean strict argument for those who write smart matchers that can be used in FileWalker") + public static HgWorkingCopyStatusCollector create(HgRepository hgRepo, Path.Matcher scope) { + FileIterator w = new HgInternals(hgRepo).createWorkingDirWalker(null); + FileIterator wf = (scope == null || scope instanceof Path.Matcher.Any) ? w : new FileIteratorFilter(w, scope); + // the reason I need to iterate over full repo and apply filter is that I have no idea whatsoever about + // patterns in the scope. I.e. if scope lists a file (PathGlobMatcher("a/b/c.txt")), FileWalker won't get deep + // to the file unless matcher would also explicitly include "a/", "a/b/" in scope. Since I can't rely + // users would write robust matchers, and I don't see a decent way to enforce that (i.e. factory to produce + // correct matcher from Path is much like what PathScope does, and can be accessed directly with #create(repo, Path...) + // method above/ + return new HgWorkingCopyStatusCollector(hgRepo, wf); + } private static class FileListIterator implements FileIterator { private final File dir; @@ -452,15 +500,16 @@ } } - private static class DirFileIterator implements FileIterator { - private final Path dirOfInterest; - private final FileWalker walker; + private static class FileIteratorFilter implements FileIterator { + private final Path.Matcher filter; + private final FileIterator walker; + private boolean didNext = false; - public DirFileIterator(HgRepository hgRepo, Path directory) { - dirOfInterest = directory; - File dir = hgRepo.getRepositoryRoot().getParentFile(); - Path.Source pathSrc = new Path.SimpleSource(new PathRewrite.Composite(new RelativePathRewrite(dir), hgRepo.getToRepoPathHelper())); - walker = new FileWalker(new File(dir, directory.toString()), pathSrc); + public FileIteratorFilter(FileIterator fileWalker, Path.Matcher filterMatcher) { + assert fileWalker != null; + assert filterMatcher != null; + filter = filterMatcher; + walker = fileWalker; } public void reset() { @@ -468,11 +517,24 @@ } public boolean hasNext() { - return walker.hasNext(); + while (walker.hasNext()) { + walker.next(); + if (filter.accept(walker.name())) { + didNext = true; + return true; + } + } + return false; } public void next() { - walker.next(); + if (didNext) { + didNext = false; + } else { + if (!hasNext()) { + throw new NoSuchElementException(); + } + } } public Path name() { @@ -484,7 +546,7 @@ } public boolean inScope(Path file) { - return file.toString().startsWith(dirOfInterest.toString()); + return filter.accept(file); } } }
--- a/src/org/tmatesoft/hg/util/FileWalker.java Fri May 27 03:01:26 2011 +0200 +++ b/src/org/tmatesoft/hg/util/FileWalker.java Tue May 31 05:23:07 2011 +0200 @@ -31,14 +31,28 @@ private final Path.Source pathHelper; private final LinkedList<File> dirQueue; private final LinkedList<File> fileQueue; + private final Path.Matcher scope; private File nextFile; private Path nextPath; public FileWalker(File dir, Path.Source pathFactory) { + this(dir, pathFactory, null); + } + + /** + * + * @param dir + * @param pathFactory + * @param scopeMatcher - this matcher shall be capable to tell not only files of interest, but + * also whether directories shall be traversed or not (Paths it gets in {@link Path.Matcher#accept(Path)} may + * point to directories) + */ + public FileWalker(File dir, Path.Source pathFactory, Path.Matcher scopeMatcher) { startDir = dir; pathHelper = pathFactory; dirQueue = new LinkedList<File>(); fileQueue = new LinkedList<File>(); + scope = scopeMatcher; reset(); } @@ -71,7 +85,8 @@ } public boolean inScope(Path file) { - return true; // no limits, all files are of interest + /* by default, no limits, all files are of interest */ + return scope == null ? true : scope.accept(file); } // returns non-null @@ -91,8 +106,13 @@ while (!dirQueue.isEmpty()) { File dir = dirQueue.removeFirst(); for (File f : listFiles(dir)) { - if (f.isDirectory()) { - if (!".hg".equals(f.getName())) { + final boolean isDir = f.isDirectory(); + Path path = pathHelper.path(isDir ? ensureTrailingSlash(f.getPath()) : f.getPath()); + if (!inScope(path)) { + continue; + } + if (isDir) { + if (!".hg/".equals(path.toString())) { dirQueue.addLast(f); } } else { @@ -104,4 +124,17 @@ } return !fileQueue.isEmpty(); } + + private static String ensureTrailingSlash(String dirName) { + if (dirName.length() > 0) { + char last = dirName.charAt(dirName.length() - 1); + if (last == '/' || last == File.separatorChar) { + return dirName; + } + // if path already has platform-specific separator (which, BTW, it shall, according to File#getPath), + // add similar, otherwise use our default. + return dirName.indexOf(File.separatorChar) != -1 ? dirName.concat(File.separator) : dirName.concat("/"); + } + return dirName; + } }
--- a/src/org/tmatesoft/hg/util/Path.java Fri May 27 03:01:26 2011 +0200 +++ b/src/org/tmatesoft/hg/util/Path.java Tue May 31 05:23:07 2011 +0200 @@ -16,6 +16,8 @@ */ package org.tmatesoft.hg.util; +import java.util.Collection; + /** * Identify repository files (not String nor io.File). Convenient for pattern matching. Memory-friendly. * @@ -75,6 +77,29 @@ public int hashCode() { return path.hashCode(); } + + public enum CompareResult { + Same, Unrelated, Nested, Parent, /* perhaps, also ImmediateParent, DirectChild? */ + } + + /* + * a/file and a/dir ? + */ + public CompareResult compareWith(Path another) { + if (another == null) { + return CompareResult.Unrelated; // XXX perhaps, IAE? + } + if (another == this || (another.length() == length() && equals(another))) { + return CompareResult.Same; + } + if (path.startsWith(another.path)) { + return CompareResult.Nested; + } + if (another.path.startsWith(path)) { + return CompareResult.Parent; + } + return CompareResult.Unrelated; + } public static Path create(String path) { if (path == null) { @@ -92,6 +117,26 @@ */ public interface Matcher { boolean accept(Path path); + + final class Any implements Matcher { + public boolean accept(Path path) { return true; } + } + class Composite implements Matcher { + private final Path.Matcher[] elements; + + public Composite(Collection<Path.Matcher> matchers) { + elements = matchers.toArray(new Path.Matcher[matchers.size()]); + } + + public boolean accept(Path path) { + for (Path.Matcher m : elements) { + if (m.accept(path)) { + return true; + } + } + return false; + } + } } /**
--- a/test/org/tmatesoft/hg/test/TestStatus.java Fri May 27 03:01:26 2011 +0200 +++ b/test/org/tmatesoft/hg/test/TestStatus.java Tue May 31 05:23:07 2011 +0200 @@ -16,8 +16,9 @@ */ package org.tmatesoft.hg.test; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; -import static org.hamcrest.CoreMatchers.equalTo; import static org.tmatesoft.hg.core.HgStatus.Kind.*; import static org.tmatesoft.hg.repo.HgRepository.TIP; @@ -35,6 +36,7 @@ import org.tmatesoft.hg.core.HgStatus; import org.tmatesoft.hg.core.HgStatus.Kind; import org.tmatesoft.hg.core.HgStatusCommand; +import org.tmatesoft.hg.internal.PathGlobMatcher; import org.tmatesoft.hg.repo.HgLookup; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.repo.HgStatusCollector; @@ -268,6 +270,97 @@ assertTrue(sc.get(file1).size() == 1); } + @Test + public void testSubTreeStatus() throws Exception { + repo = Configuration.get().find("status-1"); + HgStatusCommand cmd = new HgStatusCommand(repo); + StatusCollector sc = new StatusCollector(); + cmd.match(new PathGlobMatcher("*")); + cmd.all().execute(sc); + /* + * C .hgignore + * ? file1 + * M file2 + * C readme + */ + final Path file1 = Path.create("file1"); + assertTrue(sc.get(file1).contains(Unknown)); + assertTrue(sc.get(file1).size() == 1); + assertTrue(sc.get(Removed).isEmpty()); + assertTrue(sc.get(Clean).size() == 2); + assertTrue(sc.get(Modified).size() == 1); + // + cmd.match(new PathGlobMatcher("dir/*")).execute(sc = new StatusCollector()); + /* + * I dir/file3 + * R dir/file4 + * R dir/file5 + */ + assertTrue(sc.get(Modified).isEmpty()); + assertTrue(sc.get(Added).isEmpty()); + assertTrue(sc.get(Ignored).size() == 1); + assertTrue(sc.get(Removed).size() == 2); + } + + + @Test + public void testSpecificFileStatus() throws Exception { + repo = Configuration.get().find("status-1"); + // files only + final Path file2 = Path.create("file2"); + final Path file3 = Path.create("dir/file3"); + HgWorkingCopyStatusCollector sc = HgWorkingCopyStatusCollector.create(repo, file2, file3); + HgStatusCollector.Record r = new HgStatusCollector.Record(); + sc.walk(TIP, r); + assertTrue(r.getAdded().isEmpty()); + assertTrue(r.getRemoved().isEmpty()); + assertTrue(r.getUnknown().isEmpty()); + assertTrue(r.getClean().isEmpty()); + assertTrue(r.getMissing().isEmpty()); + assertTrue(r.getCopied().isEmpty()); + assertTrue(r.getIgnored().contains(file3)); + assertTrue(r.getIgnored().size() == 1); + assertTrue(r.getModified().contains(file2)); + assertTrue(r.getModified().size() == 1); + // mix files and directories + final Path readme = Path.create("readme"); + final Path dir = Path.create("dir/"); + sc = HgWorkingCopyStatusCollector.create(repo, readme, dir); + sc.walk(TIP, r = new HgStatusCollector.Record()); + assertTrue(r.getAdded().isEmpty()); + assertTrue(r.getRemoved().size() == 2); + for (Path p : r.getRemoved()) { + assertEquals(p.compareWith(dir), Path.CompareResult.Nested); + } + assertTrue(r.getUnknown().isEmpty()); + assertTrue(r.getClean().size() == 1); + assertTrue(r.getClean().contains(readme)); + assertTrue(r.getMissing().isEmpty()); + assertTrue(r.getCopied().isEmpty()); + assertTrue(r.getIgnored().contains(file3)); + assertTrue(r.getIgnored().size() == 1); + assertTrue(r.getModified().isEmpty()); + } + + @Test + public void testSameResultDirectPathVsMatcher() throws Exception { + repo = Configuration.get().find("status-1"); + final Path file3 = Path.create("dir/file3"); + final Path file5 = Path.create("dir/file5"); + + HgWorkingCopyStatusCollector sc = HgWorkingCopyStatusCollector.create(repo, file3, file5); + HgStatusCollector.Record r; + sc.walk(TIP, r = new HgStatusCollector.Record()); + assertTrue(r.getRemoved().contains(file5)); + assertTrue(r.getIgnored().contains(file3)); + // + // query for the same file, but with + sc = HgWorkingCopyStatusCollector.create(repo, new PathGlobMatcher(file3.toString(), file5.toString())); + sc.walk(TIP, r = new HgStatusCollector.Record()); + assertTrue(r.getRemoved().contains(file5)); + assertTrue(r.getIgnored().contains(file3)); + } + /* * 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)