comparison src/org/tmatesoft/hg/repo/HgWorkingCopyStatusCollector.java @ 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 d63583b47bfa
children 1ec6b327a6ac
comparison
equal deleted inserted replaced
225:fad70a9e6c7f 226:26ad7827a62d
26 import java.io.FileInputStream; 26 import java.io.FileInputStream;
27 import java.io.IOException; 27 import java.io.IOException;
28 import java.nio.ByteBuffer; 28 import java.nio.ByteBuffer;
29 import java.nio.channels.FileChannel; 29 import java.nio.channels.FileChannel;
30 import java.util.Collections; 30 import java.util.Collections;
31 import java.util.NoSuchElementException;
31 import java.util.Set; 32 import java.util.Set;
32 import java.util.TreeSet; 33 import java.util.TreeSet;
33 34
34 import org.tmatesoft.hg.core.HgDataStreamException; 35 import org.tmatesoft.hg.core.HgDataStreamException;
35 import org.tmatesoft.hg.core.HgException; 36 import org.tmatesoft.hg.core.HgException;
36 import org.tmatesoft.hg.core.Nodeid; 37 import org.tmatesoft.hg.core.Nodeid;
37 import org.tmatesoft.hg.internal.ByteArrayChannel; 38 import org.tmatesoft.hg.internal.ByteArrayChannel;
39 import org.tmatesoft.hg.internal.Experimental;
38 import org.tmatesoft.hg.internal.FilterByteChannel; 40 import org.tmatesoft.hg.internal.FilterByteChannel;
41 import org.tmatesoft.hg.internal.RelativePathRewrite;
39 import org.tmatesoft.hg.repo.HgStatusCollector.ManifestRevisionInspector; 42 import org.tmatesoft.hg.repo.HgStatusCollector.ManifestRevisionInspector;
40 import org.tmatesoft.hg.util.ByteChannel; 43 import org.tmatesoft.hg.util.ByteChannel;
41 import org.tmatesoft.hg.util.CancelledException; 44 import org.tmatesoft.hg.util.CancelledException;
42 import org.tmatesoft.hg.util.FileIterator; 45 import org.tmatesoft.hg.util.FileIterator;
46 import org.tmatesoft.hg.util.FileWalker;
43 import org.tmatesoft.hg.util.Path; 47 import org.tmatesoft.hg.util.Path;
44 import org.tmatesoft.hg.util.PathPool; 48 import org.tmatesoft.hg.util.PathPool;
45 import org.tmatesoft.hg.util.PathRewrite; 49 import org.tmatesoft.hg.util.PathRewrite;
46 50
47 /** 51 /**
110 isTipBase = true; 114 isTipBase = true;
111 } else { 115 } else {
112 isTipBase = baseRevision == repo.getChangelog().getLastRevision(); 116 isTipBase = baseRevision == repo.getChangelog().getLastRevision();
113 } 117 }
114 HgStatusCollector.ManifestRevisionInspector collect = null; 118 HgStatusCollector.ManifestRevisionInspector collect = null;
115 Set<String> baseRevFiles = Collections.emptySet(); 119 Set<String> baseRevFiles = Collections.emptySet(); // files from base revision not affected by status calculation
116 if (!isTipBase) { 120 if (!isTipBase) {
117 if (baseRevisionCollector != null) { 121 if (baseRevisionCollector != null) {
118 collect = baseRevisionCollector.raw(baseRevision); 122 collect = baseRevisionCollector.raw(baseRevision);
119 } else { 123 } else {
120 collect = new HgStatusCollector.ManifestRevisionInspector(null, null); 124 collect = new HgStatusCollector.ManifestRevisionInspector(null, null);
128 } 132 }
129 repoWalker.reset(); 133 repoWalker.reset();
130 final PathPool pp = getPathPool(); 134 final PathPool pp = getPathPool();
131 while (repoWalker.hasNext()) { 135 while (repoWalker.hasNext()) {
132 repoWalker.next(); 136 repoWalker.next();
133 Path fname = repoWalker.name(); 137 Path fname = pp.path(repoWalker.name());
134 File f = repoWalker.file(); 138 File f = repoWalker.file();
135 if (hgIgnore.isIgnored(fname)) { 139 assert f.isFile();
136 inspector.ignored(pp.path(fname)); 140 if (!f.exists()) {
137 } else if (knownEntries.remove(fname.toString())) { 141 // file coming from iterator doesn't exist.
142 if (knownEntries.remove(fname.toString())) {
143 if (getDirstate().checkRemoved(fname) == null) {
144 inspector.missing(fname);
145 } else {
146 inspector.removed(fname);
147 }
148 // do not report it as removed later
149 if (collect != null) {
150 baseRevFiles.remove(fname.toString());
151 }
152 } else {
153 // chances are it was known in baseRevision. We may rely
154 // that later iteration over baseRevFiles leftovers would yield correct Removed,
155 // but it doesn't hurt to be explicit (provided we know fname *is* inScope of the FileIterator
156 if (collect != null && baseRevFiles.remove(fname.toString())) {
157 inspector.removed(fname);
158 } else {
159 // not sure I shall report such files (i.e. arbitrary name coming from FileIterator)
160 // as unknown. Command-line HG aborts "system can't find the file specified"
161 // in similar case (against wc), or just gives nothing if --change <rev> is specified.
162 // however, as it's unlikely to get unexisting files from FileIterator, and
163 // its better to see erroneous file status rather than not to see any (which is too easy
164 // to overlook), I think unknown() is reasonable approach here
165 inspector.unknown(fname);
166 }
167 }
168 continue;
169 }
170 if (knownEntries.remove(fname.toString())) {
171 // tracked file.
138 // modified, added, removed, clean 172 // modified, added, removed, clean
139 if (collect != null) { // need to check against base revision, not FS file 173 if (collect != null) { // need to check against base revision, not FS file
140 checkLocalStatusAgainstBaseRevision(baseRevFiles, collect, baseRevision, fname, f, inspector); 174 checkLocalStatusAgainstBaseRevision(baseRevFiles, collect, baseRevision, fname, f, inspector);
141 baseRevFiles.remove(fname.toString());
142 } else { 175 } else {
143 checkLocalStatusAgainstFile(fname, f, inspector); 176 checkLocalStatusAgainstFile(fname, f, inspector);
144 } 177 }
145 } else { 178 } else {
146 inspector.unknown(pp.path(fname)); 179 if (hgIgnore.isIgnored(fname)) { // hgignore shall be consulted only for non-tracked files
180 inspector.ignored(fname);
181 } else {
182 inspector.unknown(fname);
183 }
184 // the file is not tracked. Even if it's known at baseRevision, we don't need to remove it
185 // from baseRevFiles, it might need to be reported as removed as well (cmdline client does
186 // yield two statuses for the same file)
147 } 187 }
148 } 188 }
149 if (collect != null) { 189 if (collect != null) {
150 for (String r : baseRevFiles) { 190 for (String r : baseRevFiles) {
151 inspector.removed(pp.path(r)); 191 final Path fromBase = pp.path(r);
192 if (repoWalker.inScope(fromBase)) {
193 inspector.removed(fromBase);
194 }
152 } 195 }
153 } 196 }
154 for (String m : knownEntries) { 197 for (String m : knownEntries) {
198 if (!repoWalker.inScope(pp.path(m))) {
199 // do not report as missing/removed those FileIterator doesn't care about.
200 continue;
201 }
155 // missing known file from a working dir 202 // missing known file from a working dir
156 if (getDirstate().checkRemoved(m) == null) { 203 if (getDirstate().checkRemoved(m) == null) {
157 // not removed from the repository = 'deleted' 204 // not removed from the repository = 'deleted'
158 inspector.missing(pp.path(m)); 205 inspector.missing(pp.path(m));
159 } else { 206 } else {
215 // merged: was not known, report as added? 262 // merged: was not known, report as added?
216 if ((r = getDirstate().checkNormal(fname)) != null) { 263 if ((r = getDirstate().checkNormal(fname)) != null) {
217 try { 264 try {
218 Path origin = HgStatusCollector.getOriginIfCopy(repo, fname, baseRevNames, baseRevision); 265 Path origin = HgStatusCollector.getOriginIfCopy(repo, fname, baseRevNames, baseRevision);
219 if (origin != null) { 266 if (origin != null) {
220 inspector.copied(getPathPool().path(origin), getPathPool().path(fname)); 267 inspector.copied(getPathPool().path(origin), fname);
221 return; 268 return;
222 } 269 }
223 } catch (HgDataStreamException ex) { 270 } catch (HgDataStreamException ex) {
224 ex.printStackTrace(); 271 ex.printStackTrace();
225 // FIXME report to a mediator, continue status collection 272 // FIXME report to a mediator, continue status collection
226 } 273 }
227 } else if ((r = getDirstate().checkAdded(fname)) != null) { 274 } else if ((r = getDirstate().checkAdded(fname)) != null) {
228 if (r.name2 != null && baseRevNames.contains(r.name2)) { 275 if (r.name2 != null && baseRevNames.contains(r.name2)) {
229 baseRevNames.remove(r.name2); // XXX surely I shall not report rename source as Removed? 276 baseRevNames.remove(r.name2); // XXX surely I shall not report rename source as Removed?
230 inspector.copied(getPathPool().path(r.name2), getPathPool().path(fname)); 277 inspector.copied(getPathPool().path(r.name2), fname);
231 return; 278 return;
232 } 279 }
233 // fall-through, report as added 280 // fall-through, report as added
234 } else if (getDirstate().checkRemoved(fname) != null) { 281 } else if (getDirstate().checkRemoved(fname) != null) {
235 // removed: removed file was not known at the time of baseRevision, and we should not report it as removed 282 // removed: removed file was not known at the time of baseRevision, and we should not report it as removed
236 return; 283 return;
237 } 284 }
238 inspector.added(getPathPool().path(fname)); 285 inspector.added(fname);
239 } else { 286 } else {
240 // was known; check whether clean or modified 287 // was known; check whether clean or modified
241 // when added - seems to be the case of a file added once again, hence need to check if content is different 288 // when added - seems to be the case of a file added once again, hence need to check if content is different
242 if ((r = getDirstate().checkNormal(fname)) != null || (r = getDirstate().checkMerged(fname)) != null || (r = getDirstate().checkAdded(fname)) != null) { 289 if ((r = getDirstate().checkNormal(fname)) != null || (r = getDirstate().checkMerged(fname)) != null || (r = getDirstate().checkAdded(fname)) != null) {
243 // either clean or modified 290 // either clean or modified
244 HgDataFile fileNode = repo.getFileNode(fname); 291 HgDataFile fileNode = repo.getFileNode(fname);
245 final int lengthAtRevision = fileNode.length(nid1); 292 final int lengthAtRevision = fileNode.length(nid1);
246 if (r.size /* XXX File.length() ?! */ != lengthAtRevision || flags != todoGenerateFlags(fname /*java.io.File*/)) { 293 if (r.size /* XXX File.length() ?! */ != lengthAtRevision || flags != todoGenerateFlags(fname /*java.io.File*/)) {
247 inspector.modified(getPathPool().path(fname)); 294 inspector.modified(fname);
248 } else { 295 } else {
249 // check actual content to see actual changes 296 // check actual content to see actual changes
250 if (areTheSame(f, fileNode, fileNode.getLocalRevision(nid1))) { 297 if (areTheSame(f, fileNode, fileNode.getLocalRevision(nid1))) {
251 inspector.clean(getPathPool().path(fname)); 298 inspector.clean(fname);
252 } else { 299 } else {
253 inspector.modified(getPathPool().path(fname)); 300 inspector.modified(fname);
254 } 301 }
255 } 302 }
256 } 303 baseRevNames.remove(fname.toString()); // consumed, processed, handled.
257 // only those left in idsMap after processing are reported as removed 304 } else if (getDirstate().checkRemoved(fname) != null) {
305 // was known, and now marked as removed, report it right away, do not rely on baseRevNames processing later
306 inspector.removed(fname);
307 baseRevNames.remove(fname.toString()); // consumed, processed, handled.
308 }
309 // only those left in baseRevNames after processing are reported as removed
258 } 310 }
259 311
260 // 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 312 // 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
261 // we don't need to tell exact difference, hash should be enough to detect difference, and it doesn't involve reading historical file content, and it's relatively 313 // we don't need to tell exact difference, hash should be enough to detect difference, and it doesn't involve reading historical file content, and it's relatively
262 // cheap to calc hash on a file (no need to keep it completely in memory). OTOH, if I'm right that the next approach is used for nodeids: 314 // cheap to calc hash on a file (no need to keep it completely in memory). OTOH, if I'm right that the next approach is used for nodeids:
287 FileInputStream fis = null; 339 FileInputStream fis = null;
288 try { 340 try {
289 try { 341 try {
290 fis = new FileInputStream(f); 342 fis = new FileInputStream(f);
291 FileChannel fc = fis.getChannel(); 343 FileChannel fc = fis.getChannel();
292 ByteBuffer fb = ByteBuffer.allocate(min(data.length * 2 /*to fit couple of lines appended*/, 8192)); 344 ByteBuffer fb = ByteBuffer.allocate(min(1 + data.length * 2 /*to fit couple of lines appended; never zero*/, 8192));
293 class Check implements ByteChannel { 345 class Check implements ByteChannel {
294 final boolean debug = false; // XXX may want to add global variable to allow clients to turn 346 final boolean debug = false; // XXX may want to add global variable to allow clients to turn
295 boolean sameSoFar = true; 347 boolean sameSoFar = true;
296 int x = 0; 348 int x = 0;
297 349
345 private static String todoGenerateFlags(Path fname) { 397 private static String todoGenerateFlags(Path fname) {
346 // FIXME implement 398 // FIXME implement
347 return null; 399 return null;
348 } 400 }
349 401
402 @Experimental(reason="There's intention to support status query with multiple files/dirs, API might get changed")
403 public static HgWorkingCopyStatusCollector create(HgRepository hgRepo, Path file) {
404 FileIterator fi = file.isDirectory() ? new DirFileIterator(hgRepo, file) : new FileListIterator(hgRepo.getRepositoryRoot().getParentFile(), file);
405 return new HgWorkingCopyStatusCollector(hgRepo, fi);
406 }
407
408 private static class FileListIterator implements FileIterator {
409 private final File dir;
410 private final Path[] paths;
411 private int index;
412 private File nextFile; // cache file() in case it's called more than once
413
414 public FileListIterator(File startDir, Path... files) {
415 dir = startDir;
416 paths = files;
417 reset();
418 }
419
420 public void reset() {
421 index = -1;
422 nextFile = null;
423 }
424
425 public boolean hasNext() {
426 return paths.length > 0 && index < paths.length-1;
427 }
428
429 public void next() {
430 index++;
431 if (index == paths.length) {
432 throw new NoSuchElementException();
433 }
434 nextFile = new File(dir, paths[index].toString());
435 }
436
437 public Path name() {
438 return paths[index];
439 }
440
441 public File file() {
442 return nextFile;
443 }
444
445 public boolean inScope(Path file) {
446 for (int i = 0; i < paths.length; i++) {
447 if (paths[i].equals(file)) {
448 return true;
449 }
450 }
451 return false;
452 }
453 }
454
455 private static class DirFileIterator implements FileIterator {
456 private final Path dirOfInterest;
457 private final FileWalker walker;
458
459 public DirFileIterator(HgRepository hgRepo, Path directory) {
460 dirOfInterest = directory;
461 File dir = hgRepo.getRepositoryRoot().getParentFile();
462 Path.Source pathSrc = new Path.SimpleSource(new PathRewrite.Composite(new RelativePathRewrite(dir), hgRepo.getToRepoPathHelper()));
463 walker = new FileWalker(new File(dir, directory.toString()), pathSrc);
464 }
465
466 public void reset() {
467 walker.reset();
468 }
469
470 public boolean hasNext() {
471 return walker.hasNext();
472 }
473
474 public void next() {
475 walker.next();
476 }
477
478 public Path name() {
479 return walker.name();
480 }
481
482 public File file() {
483 return walker.file();
484 }
485
486 public boolean inScope(Path file) {
487 return file.toString().startsWith(dirOfInterest.toString());
488 }
489 }
350 } 490 }