tikhomirov@17: /* tikhomirov@388: * Copyright (c) 2010-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@2: */ tikhomirov@74: package org.tmatesoft.hg.repo; tikhomirov@2: tikhomirov@367: import static org.tmatesoft.hg.repo.HgInternals.wrongRevisionIndex; tikhomirov@148: import static org.tmatesoft.hg.repo.HgRepository.*; tikhomirov@456: import static org.tmatesoft.hg.util.LogFacility.Severity.*; tikhomirov@74: tikhomirov@157: import java.io.ByteArrayOutputStream; tikhomirov@237: import java.io.File; tikhomirov@237: import java.io.FileInputStream; tikhomirov@148: import java.io.IOException; tikhomirov@115: import java.nio.ByteBuffer; tikhomirov@237: import java.nio.channels.FileChannel; tikhomirov@78: import java.util.ArrayList; tikhomirov@240: import java.util.Arrays; tikhomirov@78: import java.util.Collection; tikhomirov@78: tikhomirov@425: import org.tmatesoft.hg.core.HgChangesetFileSneaker; tikhomirov@74: import org.tmatesoft.hg.core.Nodeid; tikhomirov@157: import org.tmatesoft.hg.internal.DataAccess; tikhomirov@121: import org.tmatesoft.hg.internal.FilterByteChannel; tikhomirov@277: import org.tmatesoft.hg.internal.FilterDataAccess; tikhomirov@276: import org.tmatesoft.hg.internal.IntMap; tikhomirov@420: import org.tmatesoft.hg.internal.Internals; tikhomirov@77: import org.tmatesoft.hg.internal.RevlogStream; tikhomirov@115: import org.tmatesoft.hg.util.ByteChannel; tikhomirov@237: import org.tmatesoft.hg.util.CancelSupport; tikhomirov@148: import org.tmatesoft.hg.util.CancelledException; tikhomirov@388: import org.tmatesoft.hg.util.LogFacility; tikhomirov@305: import org.tmatesoft.hg.util.Pair; tikhomirov@133: import org.tmatesoft.hg.util.Path; tikhomirov@237: import org.tmatesoft.hg.util.ProgressSupport; tikhomirov@74: tikhomirov@5: tikhomirov@17: tikhomirov@2: /** tikhomirov@425: * Regular user data file stored in the repository. tikhomirov@425: * tikhomirov@425: *
Note, most methods accept index in the file's revision history, not that of changelog. Easy way to obtain
tikhomirov@425: * changeset revision index from file's is to use {@link #getChangesetRevisionIndex(int)}. To obtain file's revision
tikhomirov@425: * index for a given changeset, {@link HgManifest#getFileRevision(int, Path)} or {@link HgChangesetFileSneaker} may
tikhomirov@425: * come handy.
tikhomirov@74: *
tikhomirov@74: * @author Artem Tikhomirov
tikhomirov@74: * @author TMate Software Ltd.
tikhomirov@2: */
tikhomirov@426: public final class HgDataFile extends Revlog {
tikhomirov@2:
tikhomirov@3: // absolute from repo root?
tikhomirov@3: // slashes, unix-style?
tikhomirov@3: // repo location agnostic, just to give info to user, not to access real storage
tikhomirov@74: private final Path path;
tikhomirov@134: private Metadata metadata; // get initialized on first access to file content.
tikhomirov@2:
tikhomirov@115: /*package-local*/HgDataFile(HgRepository hgRepo, Path filePath, RevlogStream content) {
tikhomirov@21: super(hgRepo, content);
tikhomirov@115: path = filePath;
tikhomirov@3: }
tikhomirov@115:
tikhomirov@115: /*package-local*/HgDataFile(HgRepository hgRepo, Path filePath) {
tikhomirov@115: super(hgRepo);
tikhomirov@115: path = filePath;
tikhomirov@115: }
tikhomirov@115:
tikhomirov@115: // exists is not the best name possible. now it means no file with such name was ever known to the repo.
tikhomirov@426: // it might be confused with files existed before but lately removed. TODO HgFileNode.exists makes more sense.
tikhomirov@426: // or HgDataFile.known()
tikhomirov@3: public boolean exists() {
tikhomirov@3: return content != null; // XXX need better impl
tikhomirov@2: }
tikhomirov@2:
tikhomirov@426: /**
tikhomirov@426: * Human-readable file name, i.e. "COPYING", not "store/data/_c_o_p_y_i_n_g.i"
tikhomirov@426: */
tikhomirov@74: public Path getPath() {
tikhomirov@157: return path; // hgRepo.backresolve(this) -> name? In this case, what about hashed long names?
tikhomirov@2: }
tikhomirov@2:
tikhomirov@275: /**
tikhomirov@416: * Handy shorthand for {@link #getLength(int) length(getRevisionIndex(nodeid))}
tikhomirov@354: *
tikhomirov@354: * @param nodeid revision of the file
tikhomirov@354: *
tikhomirov@275: * @return size of the file content at the given revision
tikhomirov@425: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@275: */
tikhomirov@425: public int getLength(Nodeid nodeid) throws HgRuntimeException {
tikhomirov@396: try {
tikhomirov@416: return getLength(getRevisionIndex(nodeid));
tikhomirov@396: } catch (HgInvalidControlFileException ex) {
tikhomirov@396: throw ex.isRevisionSet() ? ex : ex.setRevision(nodeid);
tikhomirov@396: } catch (HgInvalidRevisionException ex) {
tikhomirov@396: throw ex.isRevisionSet() ? ex : ex.setRevision(nodeid);
tikhomirov@396: }
tikhomirov@275: }
tikhomirov@275:
tikhomirov@275: /**
tikhomirov@368: * @param fileRevisionIndex - revision local index, non-negative. From predefined constants, only {@link HgRepository#TIP} makes sense.
tikhomirov@275: * @return size of the file content at the revision identified by local revision number.
tikhomirov@425: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@275: */
tikhomirov@425: public int getLength(int fileRevisionIndex) throws HgRuntimeException {
tikhomirov@401: if (wrongRevisionIndex(fileRevisionIndex) || fileRevisionIndex == BAD_REVISION) {
tikhomirov@401: throw new HgInvalidRevisionException(fileRevisionIndex);
tikhomirov@401: }
tikhomirov@401: if (fileRevisionIndex == TIP) {
tikhomirov@401: fileRevisionIndex = getLastRevision();
tikhomirov@401: } else if (fileRevisionIndex == WORKING_COPY) {
tikhomirov@401: File f = getRepo().getFile(this);
tikhomirov@401: if (f.exists()) {
tikhomirov@420: // single revision can't be greater than 2^32, shall be safe to cast to int
tikhomirov@420: return Internals.ltoi(f.length());
tikhomirov@401: }
tikhomirov@401: Nodeid fileRev = getWorkingCopyRevision();
tikhomirov@401: if (fileRev == null) {
tikhomirov@401: throw new HgInvalidRevisionException(String.format("File %s is not part of working copy", getPath()), null, fileRevisionIndex);
tikhomirov@401: }
tikhomirov@401: fileRevisionIndex = getRevisionIndex(fileRev);
tikhomirov@401: }
tikhomirov@367: if (metadata == null || !metadata.checked(fileRevisionIndex)) {
tikhomirov@367: checkAndRecordMetadata(fileRevisionIndex);
tikhomirov@275: }
tikhomirov@367: final int dataLen = content.dataLength(fileRevisionIndex);
tikhomirov@367: if (metadata.known(fileRevisionIndex)) {
tikhomirov@367: return dataLen - metadata.dataOffset(fileRevisionIndex);
tikhomirov@275: }
tikhomirov@275: return dataLen;
tikhomirov@22: }
tikhomirov@416:
tikhomirov@416: /**
tikhomirov@237: * Reads content of the file from working directory. If file present in the working directory, its actual content without
tikhomirov@237: * any filters is supplied through the sink. If file does not exist in the working dir, this method provides content of a file
tikhomirov@401: * as if it would be refreshed in the working copy, i.e. its corresponding revision (according to dirstate) is read from the
tikhomirov@401: * repository, and filters repo -> working copy get applied.
tikhomirov@401: *
tikhomirov@401: * NOTE, if file is missing from the working directory and is not part of the dirstate (but otherwise legal repository file,
tikhomirov@401: * e.g. from another branch), no content would be supplied.
tikhomirov@237: *
tikhomirov@396: * @param sink content consumer
tikhomirov@380: * @throws CancelledException if execution of the operation was cancelled
tikhomirov@425: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@237: */
tikhomirov@425: public void workingCopy(ByteChannel sink) throws CancelledException, HgRuntimeException {
tikhomirov@237: File f = getRepo().getFile(this);
tikhomirov@237: if (f.exists()) {
tikhomirov@237: final CancelSupport cs = CancelSupport.Factory.get(sink);
tikhomirov@237: final ProgressSupport progress = ProgressSupport.Factory.get(sink);
tikhomirov@237: final long flength = f.length();
tikhomirov@237: final int bsize = (int) Math.min(flength, 32*1024);
tikhomirov@237: progress.start((int) (flength > Integer.MAX_VALUE ? flength >>> 15 /*32 kb buf size*/ : flength));
tikhomirov@237: ByteBuffer buf = ByteBuffer.allocate(bsize);
tikhomirov@237: FileChannel fc = null;
tikhomirov@237: try {
tikhomirov@237: fc = new FileInputStream(f).getChannel();
tikhomirov@237: while (fc.read(buf) != -1) {
tikhomirov@237: cs.checkCancelled();
tikhomirov@237: buf.flip();
tikhomirov@237: int consumed = sink.write(buf);
tikhomirov@237: progress.worked(flength > Integer.MAX_VALUE ? 1 : consumed);
tikhomirov@237: buf.compact();
tikhomirov@237: }
tikhomirov@237: } catch (IOException ex) {
tikhomirov@396: throw new HgInvalidFileException("Working copy read failed", ex, f);
tikhomirov@237: } finally {
tikhomirov@237: progress.done();
tikhomirov@237: if (fc != null) {
tikhomirov@237: try {
tikhomirov@237: fc.close();
tikhomirov@237: } catch (IOException ex) {
tikhomirov@456: getRepo().getContext().getLog().dump(getClass(), Warn, ex, null);
tikhomirov@237: }
tikhomirov@237: }
tikhomirov@237: }
tikhomirov@237: } else {
tikhomirov@401: Nodeid fileRev = getWorkingCopyRevision();
tikhomirov@401: if (fileRev == null) {
tikhomirov@401: // no content for this data file in the working copy - it is not part of the actual working state.
tikhomirov@401: // XXX perhaps, shall report this to caller somehow, not silently pass no data?
tikhomirov@388: return;
tikhomirov@388: }
tikhomirov@401: final int fileRevIndex = getRevisionIndex(fileRev);
tikhomirov@401: contentWithFilters(fileRevIndex, sink);
tikhomirov@401: }
tikhomirov@401: }
tikhomirov@401:
tikhomirov@401: /**
tikhomirov@401: * @return file revision as recorded in repository manifest for dirstate parent, or null
if no file revision can be found
tikhomirov@401: */
tikhomirov@401: private Nodeid getWorkingCopyRevision() throws HgInvalidControlFileException {
tikhomirov@401: final Pairtrue
if this file is a copy of another from the repository
tikhomirov@425: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@354: */
tikhomirov@425: public boolean isCopy() throws HgRuntimeException {
tikhomirov@134: if (metadata == null || !metadata.checked(0)) {
tikhomirov@275: checkAndRecordMetadata(0);
tikhomirov@78: }
tikhomirov@134: if (!metadata.known(0)) {
tikhomirov@78: return false;
tikhomirov@78: }
tikhomirov@78: return metadata.find(0, "copy") != null;
tikhomirov@78: }
tikhomirov@78:
tikhomirov@354: /**
tikhomirov@354: * Get name of the file this one was copied from.
tikhomirov@354: *
tikhomirov@354: * @return name of the file origin
tikhomirov@425: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@354: */
tikhomirov@425: public Path getCopySourceName() throws HgRuntimeException {
tikhomirov@78: if (isCopy()) {
tikhomirov@78: return Path.create(metadata.find(0, "copy"));
tikhomirov@78: }
tikhomirov@78: throw new UnsupportedOperationException(); // XXX REVISIT, think over if Exception is good (clients would check isCopy() anyway, perhaps null is sufficient?)
tikhomirov@78: }
tikhomirov@78:
tikhomirov@415: /**
tikhomirov@415: *
tikhomirov@415: * @return revision this file was copied from
tikhomirov@425: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@415: */
tikhomirov@425: public Nodeid getCopySourceRevision() throws HgRuntimeException {
tikhomirov@78: if (isCopy()) {
tikhomirov@78: return Nodeid.fromAscii(metadata.find(0, "copyrev")); // XXX reuse/cache Nodeid
tikhomirov@78: }
tikhomirov@78: throw new UnsupportedOperationException();
tikhomirov@78: }
tikhomirov@417:
tikhomirov@415: /**
tikhomirov@417: * Get file flags recorded in the manifest
tikhomirov@416: * @param fileRevisionIndex - revision local index, non-negative, or {@link HgRepository#TIP}.
tikhomirov@417: * @see HgManifest#getFileFlags(int, Path)
tikhomirov@425: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@415: */
tikhomirov@425: public HgManifest.Flags getFlags(int fileRevisionIndex) throws HgRuntimeException {
tikhomirov@415: int changesetRevIndex = getChangesetRevisionIndex(fileRevisionIndex);
tikhomirov@417: return getRepo().getManifest().getFileFlags(changesetRevIndex, getPath());
tikhomirov@415: }
tikhomirov@416:
tikhomirov@88: @Override
tikhomirov@88: public String toString() {
tikhomirov@88: StringBuilder sb = new StringBuilder(getClass().getSimpleName());
tikhomirov@88: sb.append('(');
tikhomirov@88: sb.append(getPath());
tikhomirov@88: sb.append(')');
tikhomirov@88: return sb.toString();
tikhomirov@88: }
tikhomirov@275:
tikhomirov@396: private void checkAndRecordMetadata(int localRev) throws HgInvalidControlFileException {
tikhomirov@275: // content() always initializes metadata.
tikhomirov@401: // TODO [post-1.0] this is expensive way to find out metadata, distinct RevlogStream.Iterator would be better.
tikhomirov@275: // Alternatively, may parameterize MetadataContentPipe to do prepare only.
tikhomirov@275: // For reference, when throwing CancelledException, hg status -A --rev 3:80 takes 70 ms
tikhomirov@275: // however, if we just consume buffer instead (buffer.position(buffer.limit()), same command takes ~320ms
tikhomirov@275: // (compared to command-line counterpart of 190ms)
tikhomirov@275: try {
tikhomirov@275: content(localRev, new ByteChannel() { // No-op channel
tikhomirov@275: public int write(ByteBuffer buffer) throws IOException, CancelledException {
tikhomirov@275: throw new CancelledException();
tikhomirov@275: }
tikhomirov@275: });
tikhomirov@275: } catch (CancelledException ex) {
tikhomirov@275: // it's ok, we did that
tikhomirov@366: } catch (HgInvalidControlFileException ex) {
tikhomirov@396: throw ex.isRevisionIndexSet() ? ex : ex.setRevisionIndex(localRev);
tikhomirov@275: }
tikhomirov@275: }
tikhomirov@78:
tikhomirov@88: private static final class MetadataEntry {
tikhomirov@78: private final String entry;
tikhomirov@78: private final int valueStart;
tikhomirov@388:
tikhomirov@388: // key may be null
tikhomirov@78: /*package-local*/MetadataEntry(String key, String value) {
tikhomirov@388: if (key == null) {
tikhomirov@388: entry = value;
tikhomirov@388: valueStart = -1; // not 0 to tell between key == null and key == ""
tikhomirov@388: } else {
tikhomirov@388: entry = key + value;
tikhomirov@388: valueStart = key.length();
tikhomirov@388: }
tikhomirov@78: }
tikhomirov@78: /*package-local*/boolean matchKey(String key) {
tikhomirov@388: return key == null ? valueStart == -1 : key.length() == valueStart && entry.startsWith(key);
tikhomirov@78: }
tikhomirov@134: // uncomment once/if needed
tikhomirov@134: // public String key() {
tikhomirov@134: // return entry.substring(0, valueStart);
tikhomirov@134: // }
tikhomirov@78: public String value() {
tikhomirov@388: return valueStart == -1 ? entry : entry.substring(valueStart);
tikhomirov@78: }
tikhomirov@78: }
tikhomirov@78:
tikhomirov@78: private static class Metadata {
tikhomirov@276: private static class Record {
tikhomirov@276: public final int offset;
tikhomirov@276: public final MetadataEntry[] entries;
tikhomirov@276:
tikhomirov@276: public Record(int off, MetadataEntry[] entr) {
tikhomirov@276: offset = off;
tikhomirov@276: entries = entr;
tikhomirov@276: }
tikhomirov@276: }
tikhomirov@78: // XXX sparse array needed
tikhomirov@276: private final IntMap