Package org.tmatesoft.hg.core

Source Code of org.tmatesoft.hg.core.HgChangesetFileSneaker

/*
* 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.core;

import java.util.ArrayDeque;

import org.tmatesoft.hg.internal.ManifestRevision;
import org.tmatesoft.hg.repo.HgDataFile;
import org.tmatesoft.hg.repo.HgInvalidStateException;
import org.tmatesoft.hg.repo.HgManifest;
import org.tmatesoft.hg.repo.HgManifest.Flags;
import org.tmatesoft.hg.repo.HgRepository;
import org.tmatesoft.hg.repo.HgRuntimeException;
import org.tmatesoft.hg.util.Outcome;
import org.tmatesoft.hg.util.Pair;
import org.tmatesoft.hg.util.Path;

/**
* Primary purpose is to provide information about file revisions at specific changeset. Multiple {@link #check(Path)} calls
* are possible once {@link #changeset(Nodeid)} (and optionally, {@link #followRenames(boolean)}) were set.
*
* <p>Sample:
* <pre><code>
*   HgChangesetFileSneaker i = new HgChangesetFileSneaker(hgRepo).changeset(Nodeid.fromString("<40 digits>")).followRenames(true);
*   if (i.check(file)) {
*     HgCatCommand catCmd = new HgCatCommand(hgRepo).revision(i.getFileRevision());
*     catCmd.execute(...);
*     ...
*   }
* </pre></code>
*
* TODO may add #manifest(Nodeid) to select manifest according to its revision (not only changeset revision as it's now)
*
* <p>Unlike {@link HgManifest#getFileRevision(int, Path)}, this class is useful when few files from the same changeset have to be inspected
*
* @see HgManifest#getFileRevision(int, Path)
* @author Artem Tikhomirov
* @author TMate Software Ltd.
*/
public final class HgChangesetFileSneaker {

  private final HgRepository repo;
  private boolean followRenames;
  private Nodeid cset;
  private ManifestRevision cachedManifest;
  private HgFileRevision fileRevision;
  private boolean renamed;
  private Outcome checkResult;

  public HgChangesetFileSneaker(HgRepository hgRepo) {
    repo = hgRepo;
  }

  /**
   * Select specific changelog revision
   *
   * @param nid changeset identifier
   * @return <code>this</code> for convenience
   */
  public HgChangesetFileSneaker changeset(Nodeid nid) {
    if (nid == null || nid.isNull()) {
      throw new IllegalArgumentException();
    }
    cset = nid;
    cachedManifest = null;
    fileRevision = null;
    return this;
  }

  /**
   * Whether to check file origins, default is false (look up only the name supplied)
   *
   * @param follow <code>true</code> to check copy/rename origin of the file if it is a copy.
   * @return <code>this</code> for convenience
   */
  public HgChangesetFileSneaker followRenames(boolean follow) {
    followRenames = follow;
    fileRevision = null;
    return this;
  }

  /**
   * Shortcut to perform {@link #check(Path)} and {@link #exists()}. Result of the check may be accessed via {@link #getCheckResult()}.
   * Errors during the check, if any, are reported through exception.
   *
   * @param file name of the file in question
   * @return <code>true</code> if file is known at the selected changeset.
   * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state
   * @throws IllegalArgumentException if {@link #changeset(Nodeid)} not specified or file argument is bad.
   */
  public boolean checkExists(Path file) throws HgException {
    check(file);
    // next seems reasonable, however renders boolean return value useless. perhaps void or distinct method?
//    if (checkResult.isOk() && !exists()) {
//      throw new HgPathNotFoundException(checkResult.getMessage(), file);
//    }
    if (!checkResult.isOk() && checkResult.getException() instanceof HgRuntimeException) {
      throw new HgLibraryFailureException((HgRuntimeException) checkResult.getException());
    }
    return checkResult.isOk() && exists();
  }

  /**
   * Find file (or its origin, if {@link #followRenames(boolean)} was set to <code>true</code>) among files known at specified {@link #changeset(Nodeid)}.
   *
   * @param file name of the file in question
   * @return status object that describes outcome, {@link Outcome#isOk() Ok} status indicates successful completion of the operation, but doesn't imply
   * file existence, use {@link #exists()} for that purpose. Message of the status may provide further hints on what exactly had happened.
   * @throws IllegalArgumentException if {@link #changeset(Nodeid)} not specified or file argument is bad.
   */
  public Outcome check(Path file) {
    fileRevision = null;
    checkResult = null;
    renamed = false;
    if (cset == null || file == null || file.isDirectory()) {
      throw new IllegalArgumentException();
    }
    HgDataFile dataFile = repo.getFileNode(file);
    if (!dataFile.exists()) {
      checkResult = new Outcome(Outcome.Kind.Success, String.format("File named %s is not known in the repository", file));
      return checkResult;
    }
    Nodeid toExtract = null;
    String phaseMsg = "Extract manifest revision failed";
    try {
      if (cachedManifest == null) {
        int csetRev = repo.getChangelog().getRevisionIndex(cset);
        cachedManifest = new ManifestRevision(null, null); // XXX how about context and cached manifest revisions
        repo.getManifest().walk(csetRev, csetRev, cachedManifest);
        // cachedManifest shall be meaningful - changelog.getRevisionIndex() above ensures we've got version that exists.
      }
      toExtract = cachedManifest.nodeid(file);
      phaseMsg = "Follow copy/rename failed";
      if (toExtract == null && followRenames) {
        int csetIndex = repo.getChangelog().getRevisionIndex(cset);
        int ccFileRevIndex = dataFile.getLastRevision(); // copy candidate
        int csetFileEnds = dataFile.getChangesetRevisionIndex(ccFileRevIndex);
        if (csetIndex > csetFileEnds) {
          return new Outcome(Outcome.Kind.Success, String.format("%s: last known changeset for the file %s is %d. Follow renames is possible towards older changesets only", phaseMsg, file, csetFileEnds));
        }
        // @see FileRenameHistory, with similar code, which doesn't trace alternative paths
        // traceback stack keeps record of all files with isCopy(fileRev) == true we've tried to follow, so that we can try earlier file
        // revisions in case followed fileRev didn't succeed
        ArrayDeque<Pair<HgDataFile, Integer>> traceback = new ArrayDeque<Pair<HgDataFile, Integer>>();
        do {
          int ccCsetIndex = dataFile.getChangesetRevisionIndex(ccFileRevIndex);
          if (ccCsetIndex <= csetIndex) {
            // present dataFile is our (distant) origin
            toExtract = dataFile.getRevision(ccFileRevIndex);
            renamed = true;
            break;
          }
          if (!dataFile.isCopy(ccFileRevIndex)) {
            // nothing left to return to when traceback.isEmpty()
            while (ccFileRevIndex == 0 && !traceback.isEmpty()) {
              Pair<HgDataFile, Integer> lastTurnPoint = traceback.pop();
              dataFile = lastTurnPoint.first();
              ccFileRevIndex = lastTurnPoint.second(); // generally ccFileRevIndex != 0 here, but doesn't hurt to check, hence while
              // fall through to shift down from the file revision we've already looked at
            }
            ccFileRevIndex--;
            continue;
          }
          if (ccFileRevIndex > 0) {
            // there's no reason to memorize turn point if it's the very first revision
            // of the file and we won't be able to try any other earlier revision
            traceback.push(new Pair<HgDataFile, Integer>(dataFile, ccFileRevIndex));
          }
          HgFileRevision origin = dataFile.getCopySource(ccFileRevIndex);
          dataFile = repo.getFileNode(origin.getPath());
          ccFileRevIndex = dataFile.getRevisionIndex(origin.getRevision());
        } while (ccFileRevIndex >= 0);
        // didn't get to csetIndex, no ancestor in file rename history found.
      }
    } catch (HgRuntimeException ex) {
      checkResult = new Outcome(Outcome.Kind.Failure, phaseMsg, ex);
      return checkResult;
    }
    if (toExtract != null) {
      Flags extractRevFlags = cachedManifest.flags(dataFile.getPath());
      fileRevision = new HgFileRevision(repo, toExtract, extractRevFlags, dataFile.getPath());
      checkResult = new Outcome(Outcome.Kind.Success, String.format("File %s, revision %s found at changeset %s", dataFile.getPath(), toExtract.shortNotation(), cset.shortNotation()));
      return checkResult;
    }
    checkResult = new Outcome(Outcome.Kind.Success, String.format("File %s nor its origins were known at revision %s", file, cset.shortNotation()));
    return checkResult;
  }
 
  /**
   * Re-get latest check status object
   */
  public Outcome getCheckResult() {
    assertCheckRan();
    return checkResult;
  }
 
  /**
   * @return result of the last {@link #check(Path)} call.
   */
  public boolean exists() {
    assertCheckRan();
    return fileRevision != null;
  }
 
  /**
   * @return <code>true</code> if checked file was known by another name at the time of specified changeset.
   */
  public boolean hasAnotherName() {
    assertCheckRan();
    return renamed;
  }

  /**
   * @return holder for file revision information
   */
  public HgFileRevision getFileRevision() {
    assertCheckRan();
    return fileRevision;
  }

  /**
   * Name of the checked file as it was known at the time of the specified changeset.
   *
   * @return handy shortcut for <code>getFileRevision().getPath()</code>
   */
  public Path filename() {
    assertCheckRan();
    return fileRevision.getPath();
  }
 
  /**
   * Revision of the checked file
   *
   * @return handy shortcut for <code>getFileRevision().getRevision()</code>
   */
  public Nodeid revision() {
    assertCheckRan();
    return fileRevision.getRevision();
  }

  private void assertCheckRan() {
    if (checkResult == null) {
      throw new HgInvalidStateException("Shall invoke #check(Path) first");
    }
  }

}
TOP

Related Classes of org.tmatesoft.hg.core.HgChangesetFileSneaker

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.