Package org.tmatesoft.hg.repo

Source Code of org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector$FileIteratorFilter

/*
* Copyright (c) 2011-2013 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.repo;

import static java.lang.Math.max;
import static java.lang.Math.min;
import static org.tmatesoft.hg.repo.HgRepository.*;
import static org.tmatesoft.hg.util.LogFacility.Severity.*;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ReadableByteChannel;
import java.util.ArrayList;
import java.util.Collections;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;

import org.tmatesoft.hg.core.Nodeid;
import org.tmatesoft.hg.core.SessionContext;
import org.tmatesoft.hg.internal.ByteArrayChannel;
import org.tmatesoft.hg.internal.FileUtils;
import org.tmatesoft.hg.internal.FilterByteChannel;
import org.tmatesoft.hg.internal.Internals;
import org.tmatesoft.hg.internal.ManifestRevision;
import org.tmatesoft.hg.internal.PathPool;
import org.tmatesoft.hg.internal.PathScope;
import org.tmatesoft.hg.internal.Preview;
import org.tmatesoft.hg.util.Adaptable;
import org.tmatesoft.hg.util.ByteChannel;
import org.tmatesoft.hg.util.CancelSupport;
import org.tmatesoft.hg.util.CancelledException;
import org.tmatesoft.hg.util.Convertor;
import org.tmatesoft.hg.util.FileInfo;
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.RegularFileInfo;

/**
*
* @author Artem Tikhomirov
* @author TMate Software Ltd.
*/
public class HgWorkingCopyStatusCollector {

  private final HgRepository repo;
  private final FileIterator repoWalker;
  private HgDirstate dirstate;
  private HgStatusCollector baseRevisionCollector;
  private Convertor<Path> pathPool;
  private ManifestRevision dirstateParentManifest;

  /**
   * Collector that iterates over complete working copy
   */
  public HgWorkingCopyStatusCollector(HgRepository hgRepo) {
    this(hgRepo, new HgInternals(hgRepo).createWorkingDirWalker(null));
  }

  /**
   * Collector may analyze and report status for any arbitrary sub-tree of the working copy.
   * File iterator shall return names of the files relative to the repository root.
   *
   * @param hgRepo status target repository
   * @param workingCopyWalker iterator over files in the working copy
   */
  public HgWorkingCopyStatusCollector(HgRepository hgRepo, FileIterator workingCopyWalker) {
    repo = hgRepo;
    repoWalker = workingCopyWalker;
  }
 
  /**
   * Optionally, supply a collector instance that may cache (or have already cached) base revision
   * @param sc may be null
   */
  public void setBaseRevisionCollector(HgStatusCollector sc) {
    baseRevisionCollector = sc;
  }

  /*package-local*/ Convertor<Path> getPathPool() {
    if (pathPool == null) {
      if (baseRevisionCollector == null) {
        pathPool = new PathPool(new PathRewrite.Empty());
      } else {
        return baseRevisionCollector.getPathPool();
      }
    }
    return pathPool;
  }

  public void setPathPool(Convertor<Path> pathConvertor) {
    pathPool = pathConvertor;
  }

  /**
   * Access to directory state information this collector uses.
   * @return directory state holder, never <code>null</code>
   */
  public HgDirstate getDirstate() throws HgInvalidControlFileException {
    if (dirstate == null) {
      Convertor<Path> pp = getPathPool();
      Path.Source ps;
      if (pp instanceof Path.Source) {
        ps = (Path.Source) pp;
      } else {
        ps = new Path.SimpleSource(new PathRewrite.Empty(), pp);
      }
      dirstate = repo.loadDirstate(ps);
    }
    return dirstate;
  }
 
  private HgDirstate getDirstateImpl() {
    return dirstate;
  }
 
  private ManifestRevision getManifest(int changelogLocalRev) throws HgRuntimeException {
    assert changelogLocalRev >= 0;
    ManifestRevision mr;
    if (baseRevisionCollector != null) {
      mr = baseRevisionCollector.raw(changelogLocalRev);
    } else {
      mr = new ManifestRevision(null, null);
      repo.getManifest().walk(changelogLocalRev, changelogLocalRev, mr);
    }
    return mr;
  }

  private void initDirstateParentManifest() throws HgRuntimeException {
    Nodeid dirstateParent = getDirstateImpl().parents().first();
    if (dirstateParent.isNull()) {
      dirstateParentManifest = baseRevisionCollector != null ? baseRevisionCollector.raw(NO_REVISION) : HgStatusCollector.createEmptyManifestRevision();
    } else {
      int changeloRevIndex = repo.getChangelog().getRevisionIndex(dirstateParent);
      dirstateParentManifest = getManifest(changeloRevIndex);
    }
  }

  // WC not necessarily points to TIP, but may be result of update to any previous revision.
  // In such case, we need to compare local files not to their TIP content, but to specific version at the time of selected revision
  private ManifestRevision getDirstateParentManifest() {
    return dirstateParentManifest;
  }
 
  /**
   * Walk working copy, analyze status for each file found and missing.
   * May be invoked few times.
   *
   * <p>There's no dedicated constant for working copy parent, at least now.
   * Use {@link HgRepository#WORKING_COPY} to indicate comparison
   * shall be run against working copy parent. Although a bit confusing, single case doesn't
   * justify a dedicated constant.
   *
   * @param baseRevision revision index to check against, or {@link HgRepository#WORKING_COPY}. Note, {@link HgRepository#TIP} is not supported.
   * @param inspector callback to receive status information
   * @throws IOException to propagate IO errors from {@link FileIterator}
   * @throws CancelledException if operation execution was cancelled
   * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
   */
  public void walk(int baseRevision, HgStatusInspector inspector) throws IOException, CancelledException, HgRuntimeException {
    if (HgInternals.wrongRevisionIndex(baseRevision) || baseRevision == BAD_REVISION) {
      throw new HgInvalidRevisionException(baseRevision);
    }
    if (getDirstateImpl() == null) {
        getDirstate();
    }
    if (getDirstateParentManifest() == null) {
      initDirstateParentManifest();
    }
    // XXX NOTE, use of TIP for working copy parent is questionable, at least. Instead, TIP shall mean latest cset or not allowed at all
    ManifestRevision collect = null; // non null indicates we compare against base revision
    Set<Path> baseRevFiles = Collections.emptySet(); // files from base revision not affected by status calculation
    if (baseRevision != TIP && baseRevision != WORKING_COPY) {
      collect = getManifest(baseRevision);
      baseRevFiles = new TreeSet<Path>(collect.files());
    }
    if (inspector instanceof HgStatusCollector.Record) {
      HgStatusCollector sc = baseRevisionCollector == null ? new HgStatusCollector(repo) : baseRevisionCollector;
      // nodeidAfterChange(dirstate's parent) doesn't make too much sense,
      // because the change might be actually in working copy. Nevertheless,
      // as long as no nodeids can be provided for WC, seems reasonable to report
      // latest known nodeid change (although at the moment this is not used and
      // is done mostly not to leave stale initialization in the Record)
      int rev1,rev2 = getDirstateParentManifest().changesetRevisionIndex();
      if (baseRevision == TIP || baseRevision == WORKING_COPY) {
        rev1 = rev2 - 1; // just use revision prior to dirstate's parent
      } else {
        rev1 = baseRevision;
      }
      ((HgStatusCollector.Record) inspector).init(rev1, rev2, sc);
    }
    final CancelSupport cs = CancelSupport.Factory.get(inspector);
    final HgIgnore hgIgnore = repo.getIgnore();
    repoWalker.reset();
    TreeSet<Path> processed = new TreeSet<Path>(); // names of files we handled as they known to Dirstate (not FileIterator)
    final HgDirstate ds = getDirstateImpl();
    TreeSet<Path> knownEntries = ds.all(); // here just to get dirstate initialized
    while (repoWalker.hasNext()) {
      cs.checkCancelled();
      repoWalker.next();
      final Path fname = getPathPool().mangle(repoWalker.name());
      FileInfo f = repoWalker.file();
      Path knownInDirstate;
      if (!f.exists()) {
        // file coming from iterator doesn't exist.
        if ((knownInDirstate = ds.known(fname)) != null) {
          // found in dirstate
          processed.add(knownInDirstate);
          if (ds.checkRemoved(knownInDirstate) == null) {
            inspector.missing(knownInDirstate);
          } else {
            inspector.removed(knownInDirstate);
          }
          // do not report it as removed later
          if (collect != null) {
            baseRevFiles.remove(knownInDirstate);
          }
        } 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)) {
            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 ((knownInDirstate = ds.known(fname)) != null) {
        // tracked file.
        // modified, added, removed, clean
        processed.add(knownInDirstate);
        if (collect != null) { // need to check against base revision, not FS file
          checkLocalStatusAgainstBaseRevision(baseRevFiles, collect, baseRevision, knownInDirstate, f, inspector);
        } else {
          checkLocalStatusAgainstFile(knownInDirstate, f, inspector);
        }
      } else {
        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) {
      // perhaps, this code shall go after processing leftovers of knownEntries, below
      // as it's sort of last resort - what to do with otherwise unprocessed base revision files
      for (Path fromBase : baseRevFiles) {
        if (repoWalker.inScope(fromBase)) {
          inspector.removed(fromBase);
          processed.add(fromBase);
          cs.checkCancelled();
        }
      }
    }
    knownEntries.removeAll(processed);
    for (Path m : knownEntries) {
      if (!repoWalker.inScope(m)) {
        // do not report as missing/removed those FileIterator doesn't care about.
        continue;
      }
      cs.checkCancelled();
      // missing known file from a working dir 
      if (ds.checkRemoved(m) == null) {
        // not removed from the repository = 'deleted' 
        inspector.missing(m);
      } else {
        // removed from the repo
        // if we check against non-tip revision, do not report files that were added past that revision and now removed.
        if (collect == null || baseRevFiles.contains(m)) {
          inspector.removed(m);
        }
      }
    }
  }

  /**
   * A {@link #walk(int, HgStatusInspector)} that records all the status information in the {@link HgStatusCollector.Record} object.
   *
   * @see #walk(int, HgStatusInspector)
   * @param baseRevision revision index to check against, or {@link HgRepository#WORKING_COPY}. Note, {@link HgRepository#TIP} is not supported.
   * @return information object that describes change between the revisions
   * @throws IOException to propagate IO errors from {@link FileIterator}
   * @throws HgRuntimeException subclass thereof to indicate issues with the library. <em>Runtime exception</em>
   */
  public HgStatusCollector.Record status(int baseRevision) throws IOException, HgRuntimeException {
    HgStatusCollector.Record rv = new HgStatusCollector.Record();
    try {
      walk(baseRevision, rv);
    } catch (CancelledException ex) {
      // can't happen as long our Record class doesn't implement CancelSupport
      HgInvalidStateException t = new HgInvalidStateException("Internal error");
      t.initCause(ex);
      throw t;
    }
    return rv;
  }
 
  /**
   * Compares file state from working directory against parent recorded in dirstate.
   * Might be handy for merged files, always reported as 'modified' or files deemed modified
   * based on their flags change.
   *
   * @param fname repository-relative path to the file in question
   * @param fileInfo file content mediator
   * @return <code>true</code> when content in working dir differs from that of manifest-recorded revision
   */
  public boolean hasTangibleChanges(Path fname, FileInfo fileInfo) throws HgRuntimeException {
    // see #checkLocalStatusAgainstFile() below for the origin of changed file check
    HgDataFile df = repo.getFileNode(fname);
    if (!df.exists()) {
      throw new HgInvalidFileException("File not found", null).setFileName(fname);
    }
    Nodeid rev = getDirstateParentManifest().nodeid(fname);
    return rev == null || !areTheSame(fileInfo, df, rev);
  }

  //********************************************

 
  private void checkLocalStatusAgainstFile(Path fname, FileInfo f, HgStatusInspector inspector) {
    HgDirstate.Record r;
    if ((r = getDirstateImpl().checkNormal(fname)) != null) {
      // either clean or modified
      final boolean timestampEqual = f.lastModified() == r.modificationTime(), sizeEqual = r.size() == f.length();
      if (timestampEqual && sizeEqual) {
        // if flags change (chmod -x), timestamp does not change
        if (checkFlagsEqual(f, r.mode())) {
          inspector.clean(fname);
        } else {
          inspector.modified(fname); // flags are not the same
        }
      } else if (!sizeEqual && r.size() >= 0) {
        inspector.modified(fname);
      } else if (r.size() == -2) {
        // DirState wiki calls this np2 metastate:
        // 'np2': merged from other parent (status == 'n', size == -2)
        inspector.modified(fname);
      } else {
        // size is the same or unknown, and, perhaps, different timestamp
        // check actual content to avoid false modified files
        try {
          if (!checkFlagsEqual(f, r.mode())) {
            // flags modified, no need to do expensive content check
            inspector.modified(fname);
          } else {
            HgDataFile df = repo.getFileNode(fname);
            if (!df.exists()) {
              Internals implRepo = repo.getImplHelper();
              String msg = String.format("File %s known as normal in dirstate (%d, %d), doesn't exist at %s", fname, r.modificationTime(), r.size(), implRepo.getStoragePath(df));
              throw new HgInvalidFileException(msg, null).setFileName(fname);
            }
            Nodeid rev = getDirstateParentManifest().nodeid(fname);
            // rev might be null here if fname comes to dirstate as a result of a merge operation
            // where one of the parents (first parent) had no fname file, but second parent had.
            // E.g. fork revision 3, revision 4 gets .hgtags, few modifications and merge(3,12)
            // see Issue 14 for details
            if (rev == null || !areTheSame(f, df, rev)) {
              inspector.modified(df.getPath());
            } else {
              inspector.clean(df.getPath());
            }
          }
        } catch (HgRuntimeException ex) {
          repo.getSessionContext().getLog().dump(getClass(), Warn, ex, null);
          inspector.invalid(fname, ex);
        }
      }
    } else if ((r = getDirstateImpl().checkAdded(fname)) != null) {
      if (r.copySource() == null) {
        inspector.added(fname);
      } else {
        inspector.copied(r.copySource(), fname);
      }
    } else if ((r = getDirstateImpl().checkRemoved(fname)) != null) {
      inspector.removed(fname);
    } else if ((r = getDirstateImpl().checkMerged(fname)) != null) {
      inspector.modified(fname);
    }
  }
 
  // XXX refactor checkLocalStatus methods in more OO way
  private void checkLocalStatusAgainstBaseRevision(Set<Path> baseRevNames, ManifestRevision collect, int baseRevision, Path fname, FileInfo f, HgStatusInspector inspector) throws HgRuntimeException {
    // fname is in the dirstate, either Normal, Added, Removed or Merged
    Nodeid nid1 = collect.nodeid(fname);
    HgManifest.Flags flags = collect.flags(fname);
    HgDirstate.Record r;
    final HgDirstate ds = getDirstateImpl();
    if (nid1 == null) {
      // not known at the time of baseRevision:
      // normal, added, merged: either added or copied since base revision.
      // removed: nothing to report,
      if (ds.checkNormal(fname) != null || ds.checkMerged(fname) != null) {
        try {
          // XXX perhaps, shall take second parent into account if not null, too?
          Nodeid nidFromDirstate = getDirstateParentManifest().nodeid(fname);
          if (nidFromDirstate != null) {
            // see if file revision known in this parent got copied from one of baseRevNames
            Path origin = HgStatusCollector.getOriginIfCopy(repo, fname, nidFromDirstate, collect.files(), baseRevision);
            if (origin != null) {
              inspector.copied(getPathPool().mangle(origin), fname);
              return;
            }
          }
          // fall-through, report as added
        } catch (HgInvalidFileException ex) {
          // report failure and continue status collection
          inspector.invalid(fname, ex);
        }
      } else if ((r = ds.checkAdded(fname)) != null) {
        if (r.copySource() != null && baseRevNames.contains(r.copySource())) {
          // shall not remove rename source from baseRevNames, as the source
          // likely needs to be reported as Removed as well
          inspector.copied(r.copySource(), fname);
          return;
        }
        // fall-through, report as added
      } else if (ds.checkRemoved(fname) != null) {
        // removed: removed file was not known at the time of baseRevision, and we should not report it as removed
        return;
      }
      inspector.added(fname);
    } else {
      // was known; check whether clean or modified
      Nodeid nidFromDirstate = getDirstateParentManifest().nodeid(fname);
      if ((r = ds.checkNormal(fname)) != null && nid1.equals(nidFromDirstate)) {
        // regular file, was the same up to WC initialization. Check if was modified since, and, if not, report right away
        // same code as in #checkLocalStatusAgainstFile
        final boolean timestampEqual = f.lastModified() == r.modificationTime(), sizeEqual = r.size() == f.length();
        boolean handled = false;
        if (timestampEqual && sizeEqual) {
          inspector.clean(fname);
          handled = true;
        } else if (!sizeEqual && r.size() >= 0) {
          inspector.modified(fname);
          handled = true;
        } else if (!checkFlagsEqual(f, flags)) {
          // seems like flags have changed, no reason to check content further
          inspector.modified(fname);
          handled = true;
        }
        if (handled) {
          baseRevNames.remove(fname); // consumed, processed, handled.
          return;
        }
        // otherwise, shall check actual content (size not the same, or unknown (-1 or -2), or timestamp is different,
        // or nodeid in dirstate is different, but local change might have brought it back to baseRevision state)
        // FALL THROUGH
      }
      if (r != null || (r = ds.checkMerged(fname)) != null || (r = ds.checkAdded(fname)) != null) {
        try {
          // check actual content to see actual changes
          // when added - seems to be the case of a file added once again, hence need to check if content is different
          // either clean or modified
          HgDataFile fileNode = repo.getFileNode(fname);
          if (areTheSame(f, fileNode, nid1)) {
            inspector.clean(fname);
          } else {
            inspector.modified(fname);
          }
        } catch (HgRuntimeException ex) {
          repo.getSessionContext().getLog().dump(getClass(), Warn, ex, null);
          inspector.invalid(fname, ex);
        }
        baseRevNames.remove(fname); // consumed, processed, handled.
      } else if (getDirstateImpl().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); // consumed, processed, handled.
      }
      // only those left in baseRevNames after processing are reported as removed
    }

    // TODO [post-1.1] 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
    // 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
    // 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:
    // changeset nodeid + hash(actual content) => entry (Nodeid) in the next Manifest
    // 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).
    // The question is whether original Hg treats this case (same content, different parents and hence nodeids) as 'modified' or 'clean'
  }

  private boolean areTheSame(FileInfo f, HgDataFile dataFile, Nodeid revision) throws HgRuntimeException {
    // XXX consider adding HgDataDile.compare(File/byte[]/whatever) operation to optimize comparison
    ByteArrayChannel bac = new ByteArrayChannel();
    try {
      int fileRevisionIndex = dataFile.getRevisionIndex(revision);
      // need content with metadata striped off - although theoretically chances are metadata may be different,
      // WC doesn't have it anyway
      dataFile.content(fileRevisionIndex, bac);
    } catch (CancelledException ex) {
      // silently ignore - can't happen, ByteArrayChannel is not cancellable
    }
    return areTheSame(f, bac.toArray(), dataFile.getPath());
  }
 
  private boolean areTheSame(FileInfo f, final byte[] data, Path p) throws HgInvalidFileException {
    ReadableByteChannel is = null;
    class Check implements ByteChannel {
      final boolean debug = repo.getSessionContext().getLog().isDebug();
      boolean sameSoFar = true;
      int x = 0;

      public int write(ByteBuffer buffer) {
        for (int i = buffer.remaining(); i > 0; i--, x++) {
          if (x >= data.length /*file has been appended*/ || data[x] != buffer.get()) {
            if (debug) {
              byte[] xx = new byte[15];
              if (buffer.position() > 5) {
                buffer.position(buffer.position() - 5);
              }
              buffer.get(xx, 0, min(xx.length, i-1 /*-1 for the one potentially read at buffer.get in if() */));
              String exp;
              if (x < data.length) {
                exp = new String(data, max(0, x - 4), min(data.length - x, 20));
              } else {
                int offset = max(0, x - 4);
                exp = new String(data, offset, min(data.length - offset, 20));
              }
              repo.getSessionContext().getLog().dump(getClass(), Debug, "expected >>%s<< but got >>%s<<", exp, new String(xx));
            }
            sameSoFar = false;
            break;
          }
        }
        buffer.position(buffer.limit()); // mark as read
        return buffer.limit();
      }
     
      public boolean sameSoFar() {
        return sameSoFar;
      }
      public boolean ultimatelyTheSame() {
        return sameSoFar && x == data.length;
      }
    };
    Check check = new Check();
    try {
      is = f.newInputChannel();
//      ByteBuffer fb = ByteBuffer.allocate(min(1 + data.length * 2 /*to fit couple of lines appended; never zero*/, 8192));
      ByteBuffer fb = ByteBuffer.allocate(8192); // FIXME temp fix to ensure big enough buffer for KeywordFilter
      FilterByteChannel filters = new FilterByteChannel(check, repo.getFiltersFromWorkingDirToRepo(p));
      Preview preview = Adaptable.Factory.getAdapter(filters, Preview.class, null);
      if (preview != null) {
        while (is.read(fb) != -1) {
          fb.flip();
          preview.preview(fb);
          fb.clear();
        }
        // reset channel to read once again
        try {
          is.close();
        } catch (IOException ex) {
          repo.getSessionContext().getLog().dump(getClass(), Info, ex, null);
        }
        is = f.newInputChannel();
        fb.clear();
      }
      while (is.read(fb) != -1 && check.sameSoFar()) {
        fb.flip();
        filters.write(fb);
        fb.compact();
      }
      return check.ultimatelyTheSame();
    } catch (CancelledException ex) {
      repo.getSessionContext().getLog().dump(getClass(), Warn, ex, "Unexpected cancellation");
      return check.ultimatelyTheSame();
    } catch (IOException ex) {
      throw new HgInvalidFileException("File comparison failed", ex).setFileName(p);
    } finally {
      new FileUtils(repo.getSessionContext().getLog(), this).closeQuietly(is);
    }
  }

  /**
   * @return <code>true</code> if flags are the same
   */
  private boolean checkFlagsEqual(FileInfo f, HgManifest.Flags originalManifestFlags) {
    boolean same = true;
    if (repoWalker.supportsLinkFlag()) {
      if (originalManifestFlags == HgManifest.Flags.Link) {
        return f.isSymlink();
      }
      // original flag is not link, hence flags are the same if file is not link, too.
      same = !f.isSymlink();
    } // otherwise treat flags the same
    if (repoWalker.supportsExecFlag()) {
      if (originalManifestFlags == HgManifest.Flags.Exec) {
        return f.isExecutable();
      }
      // original flag has no executable attribute, hence file shall not be executable, too
      same = same || !f.isExecutable();
    }
    return same;
  }
 
  private boolean checkFlagsEqual(FileInfo f, int dirstateFileMode) {
    return checkFlagsEqual(f, HgManifest.Flags.parse(dirstateFileMode));
  }

  /**
   * 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
   */
  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.getSessionContext(), hgRepo.getWorkingDir(), 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
   */
  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;
    private final Path[] paths;
    private int index;
    private RegularFileInfo nextFile;
    private final boolean execCap, linkCap;
    private final SessionContext sessionContext;

    public FileListIterator(SessionContext ctx, File startDir, Path... files) {
      sessionContext = ctx;
      dir = startDir;
      paths = files;
      reset();
      execCap = Internals.checkSupportsExecutables(startDir);
      linkCap = Internals.checkSupportsSymlinks(startDir);
    }

    public void reset() {
      index = -1;
      nextFile = new RegularFileInfo(sessionContext, execCap, linkCap);
    }

    public boolean hasNext() {
      return paths.length > 0 && index < paths.length-1;
    }

    public void next() {
      index++;
      if (index == paths.length) {
        throw new NoSuchElementException();
      }
      nextFile.init(new File(dir, paths[index].toString()));
    }

    public Path name() {
      return paths[index];
    }

    public FileInfo 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;
    }
   
    public boolean supportsExecFlag() {
      return execCap;
    }
   
    public boolean supportsLinkFlag() {
      return linkCap;
    }
  }
 
  private static class FileIteratorFilter implements FileIterator {
    private final Path.Matcher filter;
    private final FileIterator walker;
    private boolean didNext = false;

    public FileIteratorFilter(FileIterator fileWalker, Path.Matcher filterMatcher) {
      assert fileWalker != null;
      assert filterMatcher != null;
      filter = filterMatcher;
      walker = fileWalker;
    }

    public void reset() throws IOException {
      walker.reset();
    }

    public boolean hasNext() throws IOException {
      while (walker.hasNext()) {
        walker.next();
        if (filter.accept(walker.name())) {
          didNext = true;
          return true;
        }
      }
      return false;
    }

    public void next() throws IOException {
      if (didNext) {
        didNext = false;
      } else {
        if (!hasNext()) {
          throw new NoSuchElementException();
        }
      }
    }

    public Path name() {
      return walker.name();
    }

    public FileInfo file() {
      return walker.file();
    }

    public boolean inScope(Path file) {
      return filter.accept(file);
    }
   
    public boolean supportsExecFlag() {
      return walker.supportsExecFlag();
    }
   
    public boolean supportsLinkFlag() {
      return walker.supportsLinkFlag();
    }
  }
}
TOP

Related Classes of org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector$FileIteratorFilter

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.