Mercurial > jhg
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 } |
