tikhomirov@58: /* tikhomirov@397: * Copyright (c) 2011-2012 TMate Software Ltd tikhomirov@74: * tikhomirov@74: * This program is free software; you can redistribute it and/or modify tikhomirov@74: * it under the terms of the GNU General Public License as published by tikhomirov@74: * the Free Software Foundation; version 2 of the License. tikhomirov@74: * tikhomirov@74: * This program is distributed in the hope that it will be useful, tikhomirov@74: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@74: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@74: * GNU General Public License for more details. tikhomirov@74: * tikhomirov@74: * For information on how to redistribute this software under tikhomirov@74: * the terms of a license other than GNU General Public License tikhomirov@102: * contact TMate Software at support@hg4j.com tikhomirov@58: */ tikhomirov@74: package org.tmatesoft.hg.repo; tikhomirov@58: tikhomirov@120: import static java.lang.Math.max; tikhomirov@117: import static java.lang.Math.min; tikhomirov@218: import static org.tmatesoft.hg.repo.HgRepository.*; tikhomirov@58: tikhomirov@58: import java.io.File; tikhomirov@58: import java.io.IOException; tikhomirov@117: import java.nio.ByteBuffer; tikhomirov@287: import java.nio.channels.ReadableByteChannel; tikhomirov@229: import java.util.ArrayList; tikhomirov@58: import java.util.Collections; tikhomirov@226: import java.util.NoSuchElementException; tikhomirov@58: import java.util.Set; tikhomirov@58: import java.util.TreeSet; tikhomirov@58: tikhomirov@74: import org.tmatesoft.hg.core.Nodeid; tikhomirov@425: import org.tmatesoft.hg.core.SessionContext; tikhomirov@157: import org.tmatesoft.hg.internal.ByteArrayChannel; tikhomirov@226: import org.tmatesoft.hg.internal.Experimental; tikhomirov@117: import org.tmatesoft.hg.internal.FilterByteChannel; tikhomirov@413: import org.tmatesoft.hg.internal.Internals; tikhomirov@248: import org.tmatesoft.hg.internal.ManifestRevision; tikhomirov@229: import org.tmatesoft.hg.internal.PathScope; tikhomirov@355: import org.tmatesoft.hg.internal.Preview; tikhomirov@356: import org.tmatesoft.hg.util.Adaptable; tikhomirov@117: import org.tmatesoft.hg.util.ByteChannel; tikhomirov@423: import org.tmatesoft.hg.util.CancelSupport; tikhomirov@157: import org.tmatesoft.hg.util.CancelledException; tikhomirov@287: import org.tmatesoft.hg.util.FileInfo; tikhomirov@141: import org.tmatesoft.hg.util.FileIterator; tikhomirov@226: import org.tmatesoft.hg.util.FileWalker; tikhomirov@133: import org.tmatesoft.hg.util.Path; tikhomirov@93: import org.tmatesoft.hg.util.PathPool; tikhomirov@93: import org.tmatesoft.hg.util.PathRewrite; tikhomirov@287: import org.tmatesoft.hg.util.RegularFileInfo; tikhomirov@58: tikhomirov@58: /** tikhomirov@58: * tikhomirov@74: * @author Artem Tikhomirov tikhomirov@74: * @author TMate Software Ltd. tikhomirov@58: */ tikhomirov@94: public class HgWorkingCopyStatusCollector { tikhomirov@58: tikhomirov@58: private final HgRepository repo; tikhomirov@141: private final FileIterator repoWalker; tikhomirov@59: private HgDirstate dirstate; tikhomirov@94: private HgStatusCollector baseRevisionCollector; tikhomirov@93: private PathPool pathPool; tikhomirov@282: private ManifestRevision dirstateParentManifest; tikhomirov@58: tikhomirov@362: /** tikhomirov@362: * Collector that iterates over complete working copy tikhomirov@362: */ tikhomirov@94: public HgWorkingCopyStatusCollector(HgRepository hgRepo) { tikhomirov@229: this(hgRepo, new HgInternals(hgRepo).createWorkingDirWalker(null)); tikhomirov@74: } tikhomirov@74: tikhomirov@362: /** tikhomirov@362: * Collector may analyze and report status for any arbitrary sub-tree of the working copy. tikhomirov@362: * File iterator shall return names of the files relative to the repository root. tikhomirov@362: * tikhomirov@362: * @param hgRepo status target repository tikhomirov@362: * @param workingCopyWalker iterator over files in the working copy tikhomirov@362: */ tikhomirov@362: public HgWorkingCopyStatusCollector(HgRepository hgRepo, FileIterator workingCopyWalker) { tikhomirov@218: repo = hgRepo; tikhomirov@362: repoWalker = workingCopyWalker; tikhomirov@58: } tikhomirov@59: tikhomirov@59: /** tikhomirov@59: * Optionally, supply a collector instance that may cache (or have already cached) base revision tikhomirov@59: * @param sc may be null tikhomirov@59: */ tikhomirov@94: public void setBaseRevisionCollector(HgStatusCollector sc) { tikhomirov@59: baseRevisionCollector = sc; tikhomirov@59: } tikhomirov@93: tikhomirov@93: /*package-local*/ PathPool getPathPool() { tikhomirov@93: if (pathPool == null) { tikhomirov@93: if (baseRevisionCollector == null) { tikhomirov@93: pathPool = new PathPool(new PathRewrite.Empty()); tikhomirov@93: } else { tikhomirov@93: return baseRevisionCollector.getPathPool(); tikhomirov@93: } tikhomirov@93: } tikhomirov@93: return pathPool; tikhomirov@93: } tikhomirov@93: tikhomirov@93: public void setPathPool(PathPool pathPool) { tikhomirov@93: this.pathPool = pathPool; tikhomirov@93: } tikhomirov@93: tikhomirov@290: /** tikhomirov@290: * Access to directory state information this collector uses. tikhomirov@290: * @return directory state holder, never null tikhomirov@290: */ tikhomirov@348: public HgDirstate getDirstate() throws HgInvalidControlFileException { tikhomirov@59: if (dirstate == null) { tikhomirov@284: dirstate = repo.loadDirstate(getPathPool()); tikhomirov@59: } tikhomirov@59: return dirstate; tikhomirov@59: } tikhomirov@275: tikhomirov@348: private HgDirstate getDirstateImpl() { tikhomirov@348: return dirstate; tikhomirov@348: } tikhomirov@348: tikhomirov@366: private ManifestRevision getManifest(int changelogLocalRev) throws HgInvalidControlFileException { tikhomirov@284: assert changelogLocalRev >= 0; tikhomirov@282: ManifestRevision mr; tikhomirov@282: if (baseRevisionCollector != null) { tikhomirov@282: mr = baseRevisionCollector.raw(changelogLocalRev); tikhomirov@282: } else { tikhomirov@282: mr = new ManifestRevision(null, null); tikhomirov@282: repo.getManifest().walk(changelogLocalRev, changelogLocalRev, mr); tikhomirov@282: } tikhomirov@282: return mr; tikhomirov@282: } tikhomirov@354: tikhomirov@354: private void initDirstateParentManifest() throws HgInvalidControlFileException { tikhomirov@354: Nodeid dirstateParent = getDirstateImpl().parents().first(); tikhomirov@354: if (dirstateParent.isNull()) { tikhomirov@405: dirstateParentManifest = baseRevisionCollector != null ? baseRevisionCollector.raw(NO_REVISION) : HgStatusCollector.createEmptyManifestRevision(); tikhomirov@354: } else { tikhomirov@367: int changeloRevIndex = repo.getChangelog().getRevisionIndex(dirstateParent); tikhomirov@367: dirstateParentManifest = getManifest(changeloRevIndex); tikhomirov@354: } tikhomirov@354: } tikhomirov@354: tikhomirov@354: // WC not necessarily points to TIP, but may be result of update to any previous revision. tikhomirov@354: // In such case, we need to compare local files not to their TIP content, but to specific version at the time of selected revision tikhomirov@282: private ManifestRevision getDirstateParentManifest() { tikhomirov@282: return dirstateParentManifest; tikhomirov@282: } tikhomirov@282: tikhomirov@423: /** tikhomirov@429: * Walk working copy, analyze status for each file found and missing. tikhomirov@429: * May be invoked few times. tikhomirov@423: * tikhomirov@429: *

There's no dedicated constant to for working copy parent, at least now. tikhomirov@429: * Use {@link HgRepository#WORKING_COPY} to indicate comparison tikhomirov@429: * shall be run against working copy parent. Although a bit confusing, single case doesn't tikhomirov@429: * justify a dedicated constant. tikhomirov@429: * tikhomirov@429: * @param baseRevision revision index to check against, or {@link HgRepository#WORKING_COPY}. Note, {@link HgRepository#TIP} is not supported. tikhomirov@429: * @param inspector callback to receive status information tikhomirov@423: * @throws IOException to propagate IO errors from {@link FileIterator} tikhomirov@423: * @throws CancelledException if operation execution was cancelled tikhomirov@423: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception tikhomirov@423: */ tikhomirov@423: public void walk(int baseRevision, HgStatusInspector inspector) throws IOException, CancelledException, HgRuntimeException { tikhomirov@367: if (HgInternals.wrongRevisionIndex(baseRevision) || baseRevision == BAD_REVISION) { tikhomirov@425: throw new HgInvalidRevisionException(baseRevision); tikhomirov@218: } tikhomirov@362: if (getDirstateImpl() == null) { tikhomirov@362: getDirstate(); tikhomirov@362: } tikhomirov@362: if (getDirstateParentManifest() == null) { tikhomirov@362: initDirstateParentManifest(); tikhomirov@348: } tikhomirov@429: // XXX NOTE, use of TIP for working copy parent is questionable, at least. Instead, TIP shall mean latest cset or not allowed at all tikhomirov@282: ManifestRevision collect = null; // non null indicates we compare against base revision tikhomirov@285: Set baseRevFiles = Collections.emptySet(); // files from base revision not affected by status calculation tikhomirov@282: if (baseRevision != TIP && baseRevision != WORKING_COPY) { tikhomirov@282: collect = getManifest(baseRevision); tikhomirov@285: baseRevFiles = new TreeSet(collect.files()); tikhomirov@58: } tikhomirov@94: if (inspector instanceof HgStatusCollector.Record) { tikhomirov@94: HgStatusCollector sc = baseRevisionCollector == null ? new HgStatusCollector(repo) : baseRevisionCollector; tikhomirov@282: // nodeidAfterChange(dirstate's parent) doesn't make too much sense, tikhomirov@282: // because the change might be actually in working copy. Nevertheless, tikhomirov@282: // as long as no nodeids can be provided for WC, seems reasonable to report tikhomirov@282: // latest known nodeid change (although at the moment this is not used and tikhomirov@282: // is done mostly not to leave stale initialization in the Record) tikhomirov@282: int rev1,rev2 = getDirstateParentManifest().changesetLocalRev(); tikhomirov@282: if (baseRevision == TIP || baseRevision == WORKING_COPY) { tikhomirov@282: rev1 = rev2 - 1; // just use revision prior to dirstate's parent tikhomirov@282: } else { tikhomirov@282: rev1 = baseRevision; tikhomirov@282: } tikhomirov@282: ((HgStatusCollector.Record) inspector).init(rev1, rev2, sc); tikhomirov@68: } tikhomirov@423: final CancelSupport cs = CancelSupport.Factory.get(inspector); tikhomirov@282: final HgIgnore hgIgnore = repo.getIgnore(); tikhomirov@58: repoWalker.reset(); tikhomirov@293: TreeSet processed = new TreeSet(); // names of files we handled as they known to Dirstate (not FileIterator) tikhomirov@348: final HgDirstate ds = getDirstateImpl(); tikhomirov@293: TreeSet knownEntries = ds.all(); // here just to get dirstate initialized tikhomirov@58: while (repoWalker.hasNext()) { tikhomirov@423: cs.checkCancelled(); tikhomirov@58: repoWalker.next(); tikhomirov@284: final Path fname = getPathPool().path(repoWalker.name()); tikhomirov@287: FileInfo f = repoWalker.file(); tikhomirov@293: Path knownInDirstate; tikhomirov@226: if (!f.exists()) { tikhomirov@226: // file coming from iterator doesn't exist. tikhomirov@293: if ((knownInDirstate = ds.known(fname)) != null) { tikhomirov@293: // found in dirstate tikhomirov@293: processed.add(knownInDirstate); tikhomirov@294: if (ds.checkRemoved(knownInDirstate) == null) { tikhomirov@294: inspector.missing(knownInDirstate); tikhomirov@226: } else { tikhomirov@294: inspector.removed(knownInDirstate); tikhomirov@226: } tikhomirov@226: // do not report it as removed later tikhomirov@226: if (collect != null) { tikhomirov@294: baseRevFiles.remove(knownInDirstate); tikhomirov@226: } tikhomirov@226: } else { tikhomirov@226: // chances are it was known in baseRevision. We may rely tikhomirov@226: // that later iteration over baseRevFiles leftovers would yield correct Removed, tikhomirov@226: // but it doesn't hurt to be explicit (provided we know fname *is* inScope of the FileIterator tikhomirov@285: if (collect != null && baseRevFiles.remove(fname)) { tikhomirov@226: inspector.removed(fname); tikhomirov@226: } else { tikhomirov@226: // not sure I shall report such files (i.e. arbitrary name coming from FileIterator) tikhomirov@226: // as unknown. Command-line HG aborts "system can't find the file specified" tikhomirov@226: // in similar case (against wc), or just gives nothing if --change is specified. tikhomirov@226: // however, as it's unlikely to get unexisting files from FileIterator, and tikhomirov@226: // its better to see erroneous file status rather than not to see any (which is too easy tikhomirov@226: // to overlook), I think unknown() is reasonable approach here tikhomirov@226: inspector.unknown(fname); tikhomirov@226: } tikhomirov@226: } tikhomirov@226: continue; tikhomirov@226: } tikhomirov@293: if ((knownInDirstate = ds.known(fname)) != null) { tikhomirov@226: // tracked file. tikhomirov@58: // modified, added, removed, clean tikhomirov@293: processed.add(knownInDirstate); tikhomirov@58: if (collect != null) { // need to check against base revision, not FS file tikhomirov@294: checkLocalStatusAgainstBaseRevision(baseRevFiles, collect, baseRevision, knownInDirstate, f, inspector); tikhomirov@58: } else { tikhomirov@294: checkLocalStatusAgainstFile(knownInDirstate, f, inspector); tikhomirov@58: } tikhomirov@58: } else { tikhomirov@226: if (hgIgnore.isIgnored(fname)) { // hgignore shall be consulted only for non-tracked files tikhomirov@226: inspector.ignored(fname); tikhomirov@226: } else { tikhomirov@226: inspector.unknown(fname); tikhomirov@226: } tikhomirov@226: // the file is not tracked. Even if it's known at baseRevision, we don't need to remove it tikhomirov@226: // from baseRevFiles, it might need to be reported as removed as well (cmdline client does tikhomirov@226: // yield two statuses for the same file) tikhomirov@58: } tikhomirov@58: } tikhomirov@58: if (collect != null) { tikhomirov@285: for (Path fromBase : baseRevFiles) { tikhomirov@226: if (repoWalker.inScope(fromBase)) { tikhomirov@226: inspector.removed(fromBase); tikhomirov@423: cs.checkCancelled(); tikhomirov@226: } tikhomirov@58: } tikhomirov@58: } tikhomirov@293: knownEntries.removeAll(processed); tikhomirov@284: for (Path m : knownEntries) { tikhomirov@284: if (!repoWalker.inScope(m)) { tikhomirov@226: // do not report as missing/removed those FileIterator doesn't care about. tikhomirov@226: continue; tikhomirov@226: } tikhomirov@423: cs.checkCancelled(); tikhomirov@74: // missing known file from a working dir tikhomirov@293: if (ds.checkRemoved(m) == null) { tikhomirov@74: // not removed from the repository = 'deleted' tikhomirov@284: inspector.missing(m); tikhomirov@74: } else { tikhomirov@74: // removed from the repo tikhomirov@76: // if we check against non-tip revision, do not report files that were added past that revision and now removed. tikhomirov@285: if (collect == null || baseRevFiles.contains(m)) { tikhomirov@284: inspector.removed(m); tikhomirov@76: } tikhomirov@58: } tikhomirov@58: } tikhomirov@58: } tikhomirov@58: tikhomirov@423: /** tikhomirov@429: * A {@link #walk(int, HgStatusInspector)} that records all the status information in the {@link HgStatusCollector.Record} object. tikhomirov@423: * tikhomirov@429: * @see #walk(int, HgStatusInspector) tikhomirov@429: * @param baseRevision revision index to check against, or {@link HgRepository#WORKING_COPY}. Note, {@link HgRepository#TIP} is not supported. tikhomirov@423: * @return information object that describes change between the revisions tikhomirov@423: * @throws IOException to propagate IO errors from {@link FileIterator} tikhomirov@423: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception tikhomirov@423: */ tikhomirov@423: public HgStatusCollector.Record status(int baseRevision) throws IOException, HgRuntimeException { tikhomirov@94: HgStatusCollector.Record rv = new HgStatusCollector.Record(); tikhomirov@423: try { tikhomirov@423: walk(baseRevision, rv); tikhomirov@423: } catch (CancelledException ex) { tikhomirov@423: // can't happen as long our Record class doesn't implement CancelSupport tikhomirov@423: HgInvalidStateException t = new HgInvalidStateException("Internal error"); tikhomirov@423: t.initCause(ex); tikhomirov@423: throw t; tikhomirov@423: } tikhomirov@58: return rv; tikhomirov@58: } tikhomirov@58: tikhomirov@58: //******************************************** tikhomirov@58: tikhomirov@58: tikhomirov@287: private void checkLocalStatusAgainstFile(Path fname, FileInfo f, HgStatusInspector inspector) { tikhomirov@58: HgDirstate.Record r; tikhomirov@348: if ((r = getDirstateImpl().checkNormal(fname)) != null) { tikhomirov@58: // either clean or modified tikhomirov@290: final boolean timestampEqual = f.lastModified() == r.modificationTime(), sizeEqual = r.size() == f.length(); tikhomirov@280: if (timestampEqual && sizeEqual) { tikhomirov@413: // if flags change (chmod -x), timestamp does not change tikhomirov@413: if (checkFlagsEqual(f, r.mode())) { tikhomirov@413: inspector.clean(fname); tikhomirov@413: } else { tikhomirov@413: inspector.modified(fname); // flags are not the same tikhomirov@413: } tikhomirov@290: } else if (!sizeEqual && r.size() >= 0) { tikhomirov@280: inspector.modified(fname); tikhomirov@58: } else { tikhomirov@413: // size is the same or unknown, and, perhaps, different timestamp tikhomirov@413: // check actual content to avoid false modified files tikhomirov@397: try { tikhomirov@413: if (!checkFlagsEqual(f, r.mode())) { tikhomirov@413: // flags modified, no need to do expensive content check tikhomirov@413: inspector.modified(fname); tikhomirov@397: } else { tikhomirov@413: HgDataFile df = repo.getFileNode(fname); tikhomirov@413: if (!df.exists()) { tikhomirov@413: String msg = String.format("File %s known as normal in dirstate (%d, %d), doesn't exist at %s", fname, r.modificationTime(), r.size(), repo.getStoragePath(df)); tikhomirov@413: throw new HgInvalidFileException(msg, null).setFileName(fname); tikhomirov@413: } tikhomirov@413: Nodeid rev = getDirstateParentManifest().nodeid(fname); tikhomirov@413: // rev might be null here if fname comes to dirstate as a result of a merge operation tikhomirov@413: // where one of the parents (first parent) had no fname file, but second parent had. tikhomirov@413: // E.g. fork revision 3, revision 4 gets .hgtags, few modifications and merge(3,12) tikhomirov@413: // see Issue 14 for details tikhomirov@413: if (rev == null || !areTheSame(f, df, rev)) { tikhomirov@413: inspector.modified(df.getPath()); tikhomirov@413: } else { tikhomirov@413: inspector.clean(df.getPath()); tikhomirov@413: } tikhomirov@397: } tikhomirov@425: } catch (HgRuntimeException ex) { tikhomirov@397: repo.getContext().getLog().warn(getClass(), ex, null); tikhomirov@397: inspector.invalid(fname, ex); tikhomirov@120: } tikhomirov@58: } tikhomirov@348: } else if ((r = getDirstateImpl().checkAdded(fname)) != null) { tikhomirov@290: if (r.copySource() == null) { tikhomirov@280: inspector.added(fname); tikhomirov@58: } else { tikhomirov@290: inspector.copied(r.copySource(), fname); tikhomirov@58: } tikhomirov@348: } else if ((r = getDirstateImpl().checkRemoved(fname)) != null) { tikhomirov@280: inspector.removed(fname); tikhomirov@348: } else if ((r = getDirstateImpl().checkMerged(fname)) != null) { tikhomirov@280: inspector.modified(fname); tikhomirov@58: } tikhomirov@58: } tikhomirov@58: tikhomirov@58: // XXX refactor checkLocalStatus methods in more OO way tikhomirov@287: private void checkLocalStatusAgainstBaseRevision(Set baseRevNames, ManifestRevision collect, int baseRevision, Path fname, FileInfo f, HgStatusInspector inspector) { tikhomirov@58: // fname is in the dirstate, either Normal, Added, Removed or Merged tikhomirov@285: Nodeid nid1 = collect.nodeid(fname); tikhomirov@285: HgManifest.Flags flags = collect.flags(fname); tikhomirov@58: HgDirstate.Record r; tikhomirov@58: if (nid1 == null) { tikhomirov@58: // normal: added? tikhomirov@58: // added: not known at the time of baseRevision, shall report tikhomirov@58: // merged: was not known, report as added? tikhomirov@348: if ((r = getDirstateImpl().checkNormal(fname)) != null) { tikhomirov@157: try { tikhomirov@157: Path origin = HgStatusCollector.getOriginIfCopy(repo, fname, baseRevNames, baseRevision); tikhomirov@157: if (origin != null) { tikhomirov@226: inspector.copied(getPathPool().path(origin), fname); tikhomirov@157: return; tikhomirov@157: } tikhomirov@425: } catch (HgInvalidFileException ex) { tikhomirov@360: // report failure and continue status collection tikhomirov@360: inspector.invalid(fname, ex); tikhomirov@90: } tikhomirov@348: } else if ((r = getDirstateImpl().checkAdded(fname)) != null) { tikhomirov@290: if (r.copySource() != null && baseRevNames.contains(r.copySource())) { tikhomirov@290: baseRevNames.remove(r.copySource()); // XXX surely I shall not report rename source as Removed? tikhomirov@290: inspector.copied(r.copySource(), fname); tikhomirov@58: return; tikhomirov@58: } tikhomirov@58: // fall-through, report as added tikhomirov@348: } else if (getDirstateImpl().checkRemoved(fname) != null) { tikhomirov@58: // removed: removed file was not known at the time of baseRevision, and we should not report it as removed tikhomirov@58: return; tikhomirov@58: } tikhomirov@226: inspector.added(fname); tikhomirov@58: } else { tikhomirov@58: // was known; check whether clean or modified tikhomirov@285: Nodeid nidFromDirstate = getDirstateParentManifest().nodeid(fname); tikhomirov@348: if ((r = getDirstateImpl().checkNormal(fname)) != null && nid1.equals(nidFromDirstate)) { tikhomirov@282: // regular file, was the same up to WC initialization. Check if was modified since, and, if not, report right away tikhomirov@282: // same code as in #checkLocalStatusAgainstFile tikhomirov@290: final boolean timestampEqual = f.lastModified() == r.modificationTime(), sizeEqual = r.size() == f.length(); tikhomirov@282: boolean handled = false; tikhomirov@280: if (timestampEqual && sizeEqual) { tikhomirov@280: inspector.clean(fname); tikhomirov@282: handled = true; tikhomirov@290: } else if (!sizeEqual && r.size() >= 0) { tikhomirov@280: inspector.modified(fname); tikhomirov@282: handled = true; tikhomirov@413: } else if (!checkFlagsEqual(f, flags)) { tikhomirov@282: // seems like flags have changed, no reason to check content further tikhomirov@282: inspector.modified(fname); tikhomirov@282: handled = true; tikhomirov@282: } tikhomirov@282: if (handled) { tikhomirov@285: baseRevNames.remove(fname); // consumed, processed, handled. tikhomirov@280: return; tikhomirov@280: } tikhomirov@282: // otherwise, shall check actual content (size not the same, or unknown (-1 or -2), or timestamp is different, tikhomirov@282: // or nodeid in dirstate is different, but local change might have brought it back to baseRevision state) tikhomirov@280: // FALL THROUGH tikhomirov@280: } tikhomirov@348: if (r != null || (r = getDirstateImpl().checkMerged(fname)) != null || (r = getDirstateImpl().checkAdded(fname)) != null) { tikhomirov@397: try { tikhomirov@397: // check actual content to see actual changes tikhomirov@397: // when added - seems to be the case of a file added once again, hence need to check if content is different tikhomirov@397: // either clean or modified tikhomirov@397: HgDataFile fileNode = repo.getFileNode(fname); tikhomirov@397: if (areTheSame(f, fileNode, nid1)) { tikhomirov@397: inspector.clean(fname); tikhomirov@397: } else { tikhomirov@397: inspector.modified(fname); tikhomirov@397: } tikhomirov@425: } catch (HgRuntimeException ex) { tikhomirov@397: repo.getContext().getLog().warn(getClass(), ex, null); tikhomirov@397: inspector.invalid(fname, ex); tikhomirov@58: } tikhomirov@285: baseRevNames.remove(fname); // consumed, processed, handled. tikhomirov@348: } else if (getDirstateImpl().checkRemoved(fname) != null) { tikhomirov@226: // was known, and now marked as removed, report it right away, do not rely on baseRevNames processing later tikhomirov@226: inspector.removed(fname); tikhomirov@285: baseRevNames.remove(fname); // consumed, processed, handled. tikhomirov@58: } tikhomirov@226: // only those left in baseRevNames after processing are reported as removed tikhomirov@58: } tikhomirov@58: tikhomirov@58: // 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 tikhomirov@58: // 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 tikhomirov@58: // 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: tikhomirov@58: // changeset nodeid + hash(actual content) => entry (Nodeid) in the next Manifest tikhomirov@58: // then it's sufficient to check parents from dirstate, and if they do not match parents from file's baseRevision (non matching parents means different nodeids). tikhomirov@58: // The question is whether original Hg treats this case (same content, different parents and hence nodeids) as 'modified' or 'clean' tikhomirov@58: } tikhomirov@58: tikhomirov@425: private boolean areTheSame(FileInfo f, HgDataFile dataFile, Nodeid revision) throws HgInvalidFileException { tikhomirov@157: // XXX consider adding HgDataDile.compare(File/byte[]/whatever) operation to optimize comparison tikhomirov@157: ByteArrayChannel bac = new ByteArrayChannel(); tikhomirov@157: try { tikhomirov@367: int fileRevisionIndex = dataFile.getRevisionIndex(revision); tikhomirov@157: // need content with metadata striped off - although theoretically chances are metadata may be different, tikhomirov@157: // WC doesn't have it anyway tikhomirov@367: dataFile.content(fileRevisionIndex, bac); tikhomirov@157: } catch (CancelledException ex) { tikhomirov@157: // silently ignore - can't happen, ByteArrayChannel is not cancellable tikhomirov@157: } tikhomirov@397: return areTheSame(f, bac.toArray(), dataFile.getPath()); tikhomirov@157: } tikhomirov@157: tikhomirov@423: private boolean areTheSame(FileInfo f, final byte[] data, Path p) throws HgInvalidFileException { tikhomirov@287: ReadableByteChannel is = null; tikhomirov@295: class Check implements ByteChannel { tikhomirov@295: final boolean debug = repo.getContext().getLog().isDebug(); tikhomirov@295: boolean sameSoFar = true; tikhomirov@295: int x = 0; tikhomirov@219: tikhomirov@295: public int write(ByteBuffer buffer) { tikhomirov@295: for (int i = buffer.remaining(); i > 0; i--, x++) { tikhomirov@295: if (x >= data.length /*file has been appended*/ || data[x] != buffer.get()) { tikhomirov@295: if (debug) { tikhomirov@295: byte[] xx = new byte[15]; tikhomirov@295: if (buffer.position() > 5) { tikhomirov@295: buffer.position(buffer.position() - 5); tikhomirov@117: } tikhomirov@334: buffer.get(xx, 0, min(xx.length, i-1 /*-1 for the one potentially read at buffer.get in if() */)); tikhomirov@334: String exp; tikhomirov@334: if (x < data.length) { tikhomirov@334: exp = new String(data, max(0, x - 4), min(data.length - x, 20)); tikhomirov@334: } else { tikhomirov@334: int offset = max(0, x - 4); tikhomirov@334: exp = new String(data, offset, min(data.length - offset, 20)); tikhomirov@334: } tikhomirov@334: repo.getContext().getLog().debug(getClass(), "expected >>%s<< but got >>%s<<", exp, new String(xx)); tikhomirov@117: } tikhomirov@295: sameSoFar = false; tikhomirov@295: break; tikhomirov@219: } tikhomirov@117: } tikhomirov@295: buffer.position(buffer.limit()); // mark as read tikhomirov@295: return buffer.limit(); tikhomirov@295: } tikhomirov@295: tikhomirov@295: public boolean sameSoFar() { tikhomirov@295: return sameSoFar; tikhomirov@295: } tikhomirov@295: public boolean ultimatelyTheSame() { tikhomirov@295: return sameSoFar && x == data.length; tikhomirov@295: } tikhomirov@295: }; tikhomirov@295: Check check = new Check(); tikhomirov@295: try { tikhomirov@295: is = f.newInputChannel(); tikhomirov@295: ByteBuffer fb = ByteBuffer.allocate(min(1 + data.length * 2 /*to fit couple of lines appended; never zero*/, 8192)); tikhomirov@295: FilterByteChannel filters = new FilterByteChannel(check, repo.getFiltersFromWorkingDirToRepo(p)); tikhomirov@356: Preview preview = Adaptable.Factory.getAdapter(filters, Preview.class, null); tikhomirov@355: if (preview != null) { tikhomirov@355: while (is.read(fb) != -1) { tikhomirov@355: fb.flip(); tikhomirov@355: preview.preview(fb); tikhomirov@355: fb.clear(); tikhomirov@355: } tikhomirov@355: // reset channel to read once again tikhomirov@355: try { tikhomirov@355: is.close(); tikhomirov@355: } catch (IOException ex) { tikhomirov@355: repo.getContext().getLog().info(getClass(), ex, null); tikhomirov@355: } tikhomirov@355: is = f.newInputChannel(); tikhomirov@355: fb.clear(); tikhomirov@355: } tikhomirov@295: while (is.read(fb) != -1 && check.sameSoFar()) { tikhomirov@295: fb.flip(); tikhomirov@295: filters.write(fb); tikhomirov@295: fb.compact(); tikhomirov@295: } tikhomirov@295: return check.ultimatelyTheSame(); tikhomirov@295: } catch (CancelledException ex) { tikhomirov@295: repo.getContext().getLog().warn(getClass(), ex, "Unexpected cancellation"); tikhomirov@295: return check.ultimatelyTheSame(); tikhomirov@295: } catch (IOException ex) { tikhomirov@397: throw new HgInvalidFileException("File comparison failed", ex).setFileName(p); tikhomirov@295: } finally { tikhomirov@295: if (is != null) { tikhomirov@295: try { tikhomirov@287: is.close(); tikhomirov@295: } catch (IOException ex) { tikhomirov@295: repo.getContext().getLog().info(getClass(), ex, null); tikhomirov@117: } tikhomirov@117: } tikhomirov@117: } tikhomirov@117: } tikhomirov@117: tikhomirov@413: /** tikhomirov@413: * @return true if flags are the same tikhomirov@413: */ tikhomirov@413: private boolean checkFlagsEqual(FileInfo f, HgManifest.Flags originalManifestFlags) { tikhomirov@413: boolean same = true; tikhomirov@413: if (repoWalker.supportsLinkFlag()) { tikhomirov@413: if (originalManifestFlags == HgManifest.Flags.Link) { tikhomirov@413: return f.isSymlink(); tikhomirov@413: } tikhomirov@413: // original flag is not link, hence flags are the same if file is not link, too. tikhomirov@413: same = !f.isSymlink(); tikhomirov@413: } // otherwise treat flags the same tikhomirov@413: if (repoWalker.supportsExecFlag()) { tikhomirov@413: if (originalManifestFlags == HgManifest.Flags.Exec) { tikhomirov@413: return f.isExecutable(); tikhomirov@413: } tikhomirov@413: // original flag has no executable attribute, hence file shall not be executable, too tikhomirov@413: same = same || !f.isExecutable(); tikhomirov@413: } tikhomirov@413: return same; tikhomirov@413: } tikhomirov@413: tikhomirov@413: private boolean checkFlagsEqual(FileInfo f, int dirstateFileMode) { tikhomirov@413: // source/include/linux/stat.h tikhomirov@413: final int S_IFLNK = 0120000, S_IXUSR = 00100; tikhomirov@413: // TODO post-1.0 HgManifest.Flags.parse(int) tikhomirov@413: if ((dirstateFileMode & S_IFLNK) == S_IFLNK) { tikhomirov@413: return checkFlagsEqual(f, HgManifest.Flags.Link); tikhomirov@413: } tikhomirov@413: if ((dirstateFileMode & S_IXUSR) == S_IXUSR) { tikhomirov@413: return checkFlagsEqual(f, HgManifest.Flags.Exec); tikhomirov@413: } tikhomirov@415: return checkFlagsEqual(f, HgManifest.Flags.RegularFile); // no flags tikhomirov@58: } tikhomirov@58: tikhomirov@229: /** tikhomirov@229: * Configure status collector to consider only subset of a working copy tree. Tries to be as effective as possible, and to tikhomirov@229: * traverse only relevant part of working copy on the filesystem. tikhomirov@229: * tikhomirov@229: * @param hgRepo repository tikhomirov@229: * @param paths repository-relative files and/or directories. Directories are processed recursively. tikhomirov@229: * tikhomirov@229: * @return new instance of {@link HgWorkingCopyStatusCollector}, ready to {@link #walk(int, HgStatusInspector) walk} associated working copy tikhomirov@229: */ tikhomirov@229: @Experimental(reason="Provisional API") tikhomirov@229: public static HgWorkingCopyStatusCollector create(HgRepository hgRepo, Path... paths) { tikhomirov@229: ArrayList f = new ArrayList(5); tikhomirov@229: ArrayList d = new ArrayList(5); tikhomirov@229: for (Path p : paths) { tikhomirov@229: if (p.isDirectory()) { tikhomirov@229: d.add(p); tikhomirov@229: } else { tikhomirov@229: f.add(p); tikhomirov@229: } tikhomirov@229: } tikhomirov@229: // final Path[] dirs = f.toArray(new Path[d.size()]); tikhomirov@229: if (d.isEmpty()) { tikhomirov@229: final Path[] files = f.toArray(new Path[f.size()]); tikhomirov@425: FileIterator fi = new FileListIterator(hgRepo.getContext(), hgRepo.getWorkingDir(), files); tikhomirov@229: return new HgWorkingCopyStatusCollector(hgRepo, fi); tikhomirov@229: } tikhomirov@229: // tikhomirov@229: tikhomirov@229: //FileIterator fi = file.isDirectory() ? new DirFileIterator(hgRepo, file) : new FileListIterator(, file); tikhomirov@229: FileIterator fi = new HgInternals(hgRepo).createWorkingDirWalker(new PathScope(true, paths)); tikhomirov@226: return new HgWorkingCopyStatusCollector(hgRepo, fi); tikhomirov@226: } tikhomirov@229: tikhomirov@229: /** tikhomirov@229: * Configure collector object to calculate status for matching files only. tikhomirov@229: * This method may be less effective than explicit list of files as it iterates over whole repository tikhomirov@229: * (thus supplied matcher doesn't need to care if directories to files in question are also in scope, tikhomirov@229: * see {@link FileWalker#FileWalker(File, Path.Source, Path.Matcher)}) tikhomirov@229: * tikhomirov@229: * @return new instance of {@link HgWorkingCopyStatusCollector}, ready to {@link #walk(int, HgStatusInspector) walk} associated working copy tikhomirov@229: */ tikhomirov@229: @Experimental(reason="Provisional API. May add boolean strict argument for those who write smart matchers that can be used in FileWalker") tikhomirov@229: public static HgWorkingCopyStatusCollector create(HgRepository hgRepo, Path.Matcher scope) { tikhomirov@229: FileIterator w = new HgInternals(hgRepo).createWorkingDirWalker(null); tikhomirov@229: FileIterator wf = (scope == null || scope instanceof Path.Matcher.Any) ? w : new FileIteratorFilter(w, scope); tikhomirov@229: // the reason I need to iterate over full repo and apply filter is that I have no idea whatsoever about tikhomirov@229: // patterns in the scope. I.e. if scope lists a file (PathGlobMatcher("a/b/c.txt")), FileWalker won't get deep tikhomirov@229: // to the file unless matcher would also explicitly include "a/", "a/b/" in scope. Since I can't rely tikhomirov@229: // users would write robust matchers, and I don't see a decent way to enforce that (i.e. factory to produce tikhomirov@229: // correct matcher from Path is much like what PathScope does, and can be accessed directly with #create(repo, Path...) tikhomirov@229: // method above/ tikhomirov@229: return new HgWorkingCopyStatusCollector(hgRepo, wf); tikhomirov@229: } tikhomirov@226: tikhomirov@226: private static class FileListIterator implements FileIterator { tikhomirov@226: private final File dir; tikhomirov@226: private final Path[] paths; tikhomirov@226: private int index; tikhomirov@287: private RegularFileInfo nextFile; tikhomirov@413: private final boolean execCap, linkCap; tikhomirov@425: private final SessionContext sessionContext; tikhomirov@226: tikhomirov@425: public FileListIterator(SessionContext ctx, File startDir, Path... files) { tikhomirov@425: sessionContext = ctx; tikhomirov@226: dir = startDir; tikhomirov@226: paths = files; tikhomirov@226: reset(); tikhomirov@413: execCap = Internals.checkSupportsExecutables(startDir); tikhomirov@413: linkCap = Internals.checkSupportsSymlinks(startDir); tikhomirov@226: } tikhomirov@226: tikhomirov@226: public void reset() { tikhomirov@226: index = -1; tikhomirov@425: nextFile = new RegularFileInfo(sessionContext, execCap, linkCap); tikhomirov@226: } tikhomirov@226: tikhomirov@226: public boolean hasNext() { tikhomirov@226: return paths.length > 0 && index < paths.length-1; tikhomirov@226: } tikhomirov@226: tikhomirov@226: public void next() { tikhomirov@226: index++; tikhomirov@226: if (index == paths.length) { tikhomirov@226: throw new NoSuchElementException(); tikhomirov@226: } tikhomirov@287: nextFile.init(new File(dir, paths[index].toString())); tikhomirov@226: } tikhomirov@226: tikhomirov@226: public Path name() { tikhomirov@226: return paths[index]; tikhomirov@226: } tikhomirov@226: tikhomirov@287: public FileInfo file() { tikhomirov@226: return nextFile; tikhomirov@226: } tikhomirov@226: tikhomirov@226: public boolean inScope(Path file) { tikhomirov@226: for (int i = 0; i < paths.length; i++) { tikhomirov@226: if (paths[i].equals(file)) { tikhomirov@226: return true; tikhomirov@226: } tikhomirov@226: } tikhomirov@226: return false; tikhomirov@226: } tikhomirov@413: tikhomirov@413: public boolean supportsExecFlag() { tikhomirov@423: return execCap; tikhomirov@413: } tikhomirov@413: tikhomirov@413: public boolean supportsLinkFlag() { tikhomirov@423: return linkCap; tikhomirov@413: } tikhomirov@226: } tikhomirov@226: tikhomirov@229: private static class FileIteratorFilter implements FileIterator { tikhomirov@229: private final Path.Matcher filter; tikhomirov@229: private final FileIterator walker; tikhomirov@229: private boolean didNext = false; tikhomirov@226: tikhomirov@229: public FileIteratorFilter(FileIterator fileWalker, Path.Matcher filterMatcher) { tikhomirov@229: assert fileWalker != null; tikhomirov@229: assert filterMatcher != null; tikhomirov@229: filter = filterMatcher; tikhomirov@229: walker = fileWalker; tikhomirov@226: } tikhomirov@226: tikhomirov@350: public void reset() throws IOException { tikhomirov@226: walker.reset(); tikhomirov@226: } tikhomirov@226: tikhomirov@350: public boolean hasNext() throws IOException { tikhomirov@229: while (walker.hasNext()) { tikhomirov@229: walker.next(); tikhomirov@229: if (filter.accept(walker.name())) { tikhomirov@229: didNext = true; tikhomirov@229: return true; tikhomirov@229: } tikhomirov@229: } tikhomirov@229: return false; tikhomirov@226: } tikhomirov@226: tikhomirov@350: public void next() throws IOException { tikhomirov@229: if (didNext) { tikhomirov@229: didNext = false; tikhomirov@229: } else { tikhomirov@229: if (!hasNext()) { tikhomirov@229: throw new NoSuchElementException(); tikhomirov@229: } tikhomirov@229: } tikhomirov@226: } tikhomirov@226: tikhomirov@226: public Path name() { tikhomirov@226: return walker.name(); tikhomirov@226: } tikhomirov@226: tikhomirov@287: public FileInfo file() { tikhomirov@226: return walker.file(); tikhomirov@226: } tikhomirov@226: tikhomirov@226: public boolean inScope(Path file) { tikhomirov@229: return filter.accept(file); tikhomirov@226: } tikhomirov@413: tikhomirov@413: public boolean supportsExecFlag() { tikhomirov@413: return walker.supportsExecFlag(); tikhomirov@413: } tikhomirov@413: tikhomirov@413: public boolean supportsLinkFlag() { tikhomirov@413: return walker.supportsLinkFlag(); tikhomirov@413: } tikhomirov@226: } tikhomirov@58: }