changeset 226:26ad7827a62d

Support status query for a single file or a subdirectory of a repository
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Wed, 25 May 2011 12:16:24 +0200
parents fad70a9e6c7f
children 0fd10e5016dd
files cmdline/org/tmatesoft/hg/console/Main.java src/org/tmatesoft/hg/internal/Experimental.java src/org/tmatesoft/hg/repo/HgStatusCollector.java src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java src/org/tmatesoft/hg/util/FileIterator.java src/org/tmatesoft/hg/util/FileWalker.java
diffstat 6 files changed, 237 insertions(+), 21 deletions(-) [+]
line wrap: on
line diff
--- a/cmdline/org/tmatesoft/hg/console/Main.java	Wed May 25 05:13:43 2011 +0200
+++ b/cmdline/org/tmatesoft/hg/console/Main.java	Wed May 25 12:16:24 2011 +0200
@@ -61,7 +61,8 @@
 
 	public static void main(String[] args) throws Exception {
 		Main m = new Main(args);
-		m.dumpBranches();
+		m.testFileStatus();
+//		m.dumpBranches();
 //		m.inflaterLengthException();
 //		m.dumpIgnored();
 //		m.dumpDirstate();
@@ -72,6 +73,13 @@
 //		m.bunchOfTests();
 	}
 	
+	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);
+		wcsc.walk(TIP, new StatusDump());
+	}
+	
 	private void dumpBranches() {
 		HgBranches b = hgRepo.getBranches();
 		for (HgBranches.BranchInfo bi : b.getAllBranches()) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/tmatesoft/hg/internal/Experimental.java	Wed May 25 12:16:24 2011 +0200
@@ -0,0 +1,34 @@
+/*
+ * 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.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Work in progress, provisional, experimental or otherwise unstable code in publicly accessible API 
+ * 
+ * @author Artem Tikhomirov
+ * @author TMate Software Ltd.
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target({ ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR })
+public @interface Experimental {
+	String reason() default "";
+}
--- a/src/org/tmatesoft/hg/repo/HgStatusCollector.java	Wed May 25 05:13:43 2011 +0200
+++ b/src/org/tmatesoft/hg/repo/HgStatusCollector.java	Wed May 25 12:16:24 2011 +0200
@@ -56,6 +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;
 	
 
 	public HgStatusCollector(HgRepository hgRepo) {
@@ -218,20 +219,32 @@
 		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)) {
+				continue;
+			}
 			if (r1Files.remove(fname)) {
 				Nodeid nidR1 = r1.nodeid(fname);
 				Nodeid nidR2 = r2.nodeid(fname);
 				String flagsR1 = r1.flags(fname);
 				String flagsR2 = r2.flags(fname);
 				if (nidR1.equals(nidR2) && ((flagsR2 == null && flagsR1 == null) || (flagsR2 != null && flagsR2.equals(flagsR1)))) {
-					inspector.clean(pp.path(fname));
+					inspector.clean(r2filePath);
 				} else {
-					inspector.modified(pp.path(fname));
+					inspector.modified(r2filePath);
 				}
 			} else {
 				try {
-					Path copyTarget = pp.path(fname);
+					Path copyTarget = r2filePath;
 					Path copyOrigin = getOriginIfCopy(repo, copyTarget, r1Files, rev1);
 					if (copyOrigin != null) {
 						inspector.copied(pp.path(copyOrigin) /*pipe through pool, just in case*/, copyTarget);
@@ -246,7 +259,10 @@
 			}
 		}
 		for (String left : r1Files) {
-			inspector.removed(pp.path(left));
+			final Path r2filePath = pp.path(left);
+			if (scope.accept(r2filePath)) {
+				inspector.removed(r2filePath);
+			}
 		}
 	}
 	
--- a/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java	Wed May 25 05:13:43 2011 +0200
+++ b/src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java	Wed May 25 12:16:24 2011 +0200
@@ -28,6 +28,7 @@
 import java.nio.ByteBuffer;
 import java.nio.channels.FileChannel;
 import java.util.Collections;
+import java.util.NoSuchElementException;
 import java.util.Set;
 import java.util.TreeSet;
 
@@ -35,11 +36,14 @@
 import org.tmatesoft.hg.core.HgException;
 import org.tmatesoft.hg.core.Nodeid;
 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.repo.HgStatusCollector.ManifestRevisionInspector;
 import org.tmatesoft.hg.util.ByteChannel;
 import org.tmatesoft.hg.util.CancelledException;
 import org.tmatesoft.hg.util.FileIterator;
+import org.tmatesoft.hg.util.FileWalker;
 import org.tmatesoft.hg.util.Path;
 import org.tmatesoft.hg.util.PathPool;
 import org.tmatesoft.hg.util.PathRewrite;
@@ -112,7 +116,7 @@
 			isTipBase = baseRevision == repo.getChangelog().getLastRevision();
 		}
 		HgStatusCollector.ManifestRevisionInspector collect = null;
-		Set<String> baseRevFiles = Collections.emptySet();
+		Set<String> baseRevFiles = Collections.emptySet(); // files from base revision not affected by status calculation 
 		if (!isTipBase) {
 			if (baseRevisionCollector != null) {
 				collect = baseRevisionCollector.raw(baseRevision);
@@ -130,28 +134,71 @@
 		final PathPool pp = getPathPool();
 		while (repoWalker.hasNext()) {
 			repoWalker.next();
-			Path fname = repoWalker.name();
+			Path fname = pp.path(repoWalker.name());
 			File f = repoWalker.file();
-			if (hgIgnore.isIgnored(fname)) {
-				inspector.ignored(pp.path(fname));
-			} else if (knownEntries.remove(fname.toString())) {
+			assert f.isFile();
+			if (!f.exists()) {
+				// file coming from iterator doesn't exist.
+				if (knownEntries.remove(fname.toString())) {
+					if (getDirstate().checkRemoved(fname) == null) {
+						inspector.missing(fname);
+					} else {
+						inspector.removed(fname);
+					}
+					// do not report it as removed later
+					if (collect != null) {
+						baseRevFiles.remove(fname.toString());
+					}
+				} else {
+					// chances are it was known in baseRevision. We may rely
+					// that later iteration over baseRevFiles leftovers would yield correct Removed,
+					// but it doesn't hurt to be explicit (provided we know fname *is* inScope of the FileIterator
+					if (collect != null && baseRevFiles.remove(fname.toString())) {
+						inspector.removed(fname);
+					} else {
+						// not sure I shall report such files (i.e. arbitrary name coming from FileIterator)
+						// as unknown. Command-line HG aborts "system can't find the file specified"
+						// in similar case (against wc), or just gives nothing if --change <rev> is specified.
+						// however, as it's unlikely to get unexisting files from FileIterator, and
+						// its better to see erroneous file status rather than not to see any (which is too easy
+						// to overlook), I think unknown() is reasonable approach here
+						inspector.unknown(fname);
+					}
+				}
+				continue;
+			}
+			if (knownEntries.remove(fname.toString())) {
+				// tracked file.
 				// modified, added, removed, clean
 				if (collect != null) { // need to check against base revision, not FS file
 					checkLocalStatusAgainstBaseRevision(baseRevFiles, collect, baseRevision, fname, f, inspector);
-					baseRevFiles.remove(fname.toString());
 				} else {
 					checkLocalStatusAgainstFile(fname, f, inspector);
 				}
 			} else {
-				inspector.unknown(pp.path(fname));
+				if (hgIgnore.isIgnored(fname)) { // hgignore shall be consulted only for non-tracked files
+					inspector.ignored(fname);
+				} else {
+					inspector.unknown(fname);
+				}
+				// the file is not tracked. Even if it's known at baseRevision, we don't need to remove it
+				// from baseRevFiles, it might need to be reported as removed as well (cmdline client does
+				// yield two statuses for the same file)
 			}
 		}
 		if (collect != null) {
 			for (String r : baseRevFiles) {
-				inspector.removed(pp.path(r));
+				final Path fromBase = pp.path(r);
+				if (repoWalker.inScope(fromBase)) {
+					inspector.removed(fromBase);
+				}
 			}
 		}
 		for (String m : knownEntries) {
+			if (!repoWalker.inScope(pp.path(m))) {
+				// do not report as missing/removed those FileIterator doesn't care about.
+				continue;
+			}
 			// missing known file from a working dir  
 			if (getDirstate().checkRemoved(m) == null) {
 				// not removed from the repository = 'deleted'  
@@ -217,7 +264,7 @@
 				try {
 					Path origin = HgStatusCollector.getOriginIfCopy(repo, fname, baseRevNames, baseRevision);
 					if (origin != null) {
-						inspector.copied(getPathPool().path(origin), getPathPool().path(fname));
+						inspector.copied(getPathPool().path(origin), fname);
 						return;
 					}
 				} catch (HgDataStreamException ex) {
@@ -227,7 +274,7 @@
 			} else if ((r = getDirstate().checkAdded(fname)) != null) {
 				if (r.name2 != null && baseRevNames.contains(r.name2)) {
 					baseRevNames.remove(r.name2); // XXX surely I shall not report rename source as Removed?
-					inspector.copied(getPathPool().path(r.name2), getPathPool().path(fname));
+					inspector.copied(getPathPool().path(r.name2), fname);
 					return;
 				}
 				// fall-through, report as added
@@ -235,7 +282,7 @@
 				// removed: removed file was not known at the time of baseRevision, and we should not report it as removed
 				return;
 			}
-			inspector.added(getPathPool().path(fname));
+			inspector.added(fname);
 		} else {
 			// was known; check whether clean or modified
 			// when added - seems to be the case of a file added once again, hence need to check if content is different
@@ -244,17 +291,22 @@
 				HgDataFile fileNode = repo.getFileNode(fname);
 				final int lengthAtRevision = fileNode.length(nid1);
 				if (r.size /* XXX File.length() ?! */ != lengthAtRevision || flags != todoGenerateFlags(fname /*java.io.File*/)) {
-					inspector.modified(getPathPool().path(fname));
+					inspector.modified(fname);
 				} else {
 					// check actual content to see actual changes
 					if (areTheSame(f, fileNode, fileNode.getLocalRevision(nid1))) {
-						inspector.clean(getPathPool().path(fname));
+						inspector.clean(fname);
 					} else {
-						inspector.modified(getPathPool().path(fname));
+						inspector.modified(fname);
 					}
 				}
+				baseRevNames.remove(fname.toString()); // consumed, processed, handled.
+			} else if (getDirstate().checkRemoved(fname) != null) {
+				// was known, and now marked as removed, report it right away, do not rely on baseRevNames processing later
+				inspector.removed(fname);
+				baseRevNames.remove(fname.toString()); // consumed, processed, handled.
 			}
-			// only those left in idsMap after processing are reported as removed 
+			// only those left in baseRevNames after processing are reported as removed 
 		}
 
 		// TODO think over if content comparison may be done more effectively by e.g. calculating nodeid for a local file and comparing it with nodeid from manifest
@@ -289,7 +341,7 @@
 			try {
 				fis = new FileInputStream(f);
 				FileChannel fc = fis.getChannel();
-				ByteBuffer fb = ByteBuffer.allocate(min(data.length * 2 /*to fit couple of lines appended*/, 8192));
+				ByteBuffer fb = ByteBuffer.allocate(min(1 + data.length * 2 /*to fit couple of lines appended; never zero*/, 8192));
 				class Check implements ByteChannel {
 					final boolean debug = false; // XXX may want to add global variable to allow clients to turn 
 					boolean sameSoFar = true;
@@ -347,4 +399,92 @@
 		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);
+		return new HgWorkingCopyStatusCollector(hgRepo, fi);
+	}
+
+	private static class FileListIterator implements FileIterator {
+		private final File dir;
+		private final Path[] paths;
+		private int index;
+		private File nextFile; // cache file() in case it's called more than once
+
+		public FileListIterator(File startDir, Path... files) {
+			dir = startDir;
+			paths = files;
+			reset();
+		}
+
+		public void reset() {
+			index = -1;
+			nextFile = null;
+		}
+
+		public boolean hasNext() {
+			return paths.length > 0 && index < paths.length-1;
+		}
+
+		public void next() {
+			index++;
+			if (index == paths.length) {
+				throw new NoSuchElementException();
+			}
+			nextFile = new File(dir, paths[index].toString());
+		}
+
+		public Path name() {
+			return paths[index];
+		}
+
+		public File file() {
+			return nextFile;
+		}
+
+		public boolean inScope(Path file) {
+			for (int i = 0; i < paths.length; i++) {
+				if (paths[i].equals(file)) {
+					return true;
+				}
+			}
+			return false;
+		}
+	}
+	
+	private static class DirFileIterator implements FileIterator {
+		private final Path dirOfInterest;
+		private final FileWalker walker;
+
+		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 void reset() {
+			walker.reset();
+		}
+
+		public boolean hasNext() {
+			return walker.hasNext();
+		}
+
+		public void next() {
+			walker.next();
+		}
+
+		public Path name() {
+			return walker.name();
+		}
+
+		public File file() {
+			return walker.file();
+		}
+
+		public boolean inScope(Path file) {
+			return file.toString().startsWith(dirOfInterest.toString());
+		}
+	}
 }
--- a/src/org/tmatesoft/hg/util/FileIterator.java	Wed May 25 05:13:43 2011 +0200
+++ b/src/org/tmatesoft/hg/util/FileIterator.java	Wed May 25 12:16:24 2011 +0200
@@ -18,6 +18,8 @@
 
 import java.io.File;
 
+import org.tmatesoft.hg.internal.Experimental;
+
 /**
  * Abstracts iteration over file system.
  * 
@@ -47,7 +49,19 @@
 	Path name();
 
 	/**
+	 * File object to retrieve actual state from. Not necessarily exist, if {@link FileIterator} is used to query status
+	 * of specific files.
 	 * @return filesystem element.
 	 */
 	File file();
+
+	/**
+	 * When {@link FileIterator} represents only fraction of a repository, library might need to figure out if
+	 * specific file (path) belongs to that fraction or not. Paths (and {@link File Files} returned by this {@link FileIterator}
+	 * are always considered as representing the fraction, nonetheless, {@link FileIterator} shall return true for such names if 
+	 * asked.
+	 * @return <code>true</code> if this {@link FileIterator} is responsible for (interested in) specified repository-local path 
+	 */
+	@Experimental(reason="Perhaps, shall not be part of FileIterator, but rather separate Path.Matcher. Approaches in regular StatusCollector (doesn't use FI, but supports scope) and WC collector to look similar, and for HgStatusCommand to use single approach to set the scope")
+	boolean inScope(Path file);
 }
--- a/src/org/tmatesoft/hg/util/FileWalker.java	Wed May 25 05:13:43 2011 +0200
+++ b/src/org/tmatesoft/hg/util/FileWalker.java	Wed May 25 12:16:24 2011 +0200
@@ -70,6 +70,10 @@
 		return nextFile;
 	}
 	
+	public boolean inScope(Path file) {
+		return true; // no limits, all files are of interest
+	}
+	
 	private File[] listFiles(File f) {
 		// in case we need to solve os-related file issues (mac with some encodings?)
 		return f.listFiles();