tikhomirov@17: /* tikhomirov@603: * Copyright (c) 2010-2013 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@602: import static org.tmatesoft.hg.util.LogFacility.Severity.Info; tikhomirov@74: 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@240: import java.util.Arrays; 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@619: import org.tmatesoft.hg.internal.FileUtils; tikhomirov@121: import org.tmatesoft.hg.internal.FilterByteChannel; tikhomirov@277: import org.tmatesoft.hg.internal.FilterDataAccess; tikhomirov@420: import org.tmatesoft.hg.internal.Internals; tikhomirov@602: import org.tmatesoft.hg.internal.Metadata; 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@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@600: 		super(hgRepo, content, false);
tikhomirov@115: 		path = filePath;
tikhomirov@3: 	}
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@621: 		return content.exists();
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@619: 			FileInputStream fis = null;
tikhomirov@237: 			try {
tikhomirov@619: 				fis = new FileInputStream(f);
tikhomirov@619: 				FileChannel fc = fis.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@619: 				if (fis != null) {
tikhomirov@654: 					new FileUtils(getRepo().getSessionContext().getLog(), this).closeQuietly(fis);
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@628: 	 * @throws HgInvalidControlFileException if failed to access revlog index/data entry. Runtime exception
tikhomirov@628: 	 * @throws HgRuntimeException subclass thereof to indicate other issues with the library. Runtime exception
tikhomirov@401: 	 */
tikhomirov@628: 	private Nodeid getWorkingCopyRevision() throws HgRuntimeException {
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@571: 			Path.Source ps = getRepo().getSessionContext().getPathFactory();
tikhomirov@571: 			return ps.path(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@629: 
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@628: 	private void checkAndRecordMetadata(int localRev) throws HgRuntimeException {
tikhomirov@495: 		if (metadata == null) {
tikhomirov@602: 			metadata = new Metadata(getRepo());
tikhomirov@275: 		}
tikhomirov@495: 		// use MetadataInspector without delegate to process metadata only
tikhomirov@602: 		RevlogStream.Inspector insp = new MetadataInspector(metadata, null);
tikhomirov@495: 		super.content.iterate(localRev, localRev, true, insp);
tikhomirov@275: 	}
tikhomirov@78: 
tikhomirov@277: 	private static class MetadataInspector extends ErrorHandlingInspector implements RevlogStream.Inspector {
tikhomirov@157: 		private final Metadata metadata;
tikhomirov@277: 		private final RevlogStream.Inspector delegate;
tikhomirov@157: 
tikhomirov@495: 		/**
tikhomirov@495: 		 * @param _metadata never null
tikhomirov@495: 		 * @param chain null if no further data processing other than metadata is desired
tikhomirov@495: 		 */
tikhomirov@602: 		public MetadataInspector(Metadata _metadata, RevlogStream.Inspector chain) {
tikhomirov@157: 			metadata = _metadata;
tikhomirov@277: 			delegate = chain;
tikhomirov@277: 			setCancelSupport(CancelSupport.Factory.get(chain));
tikhomirov@157: 		}
tikhomirov@157: 
tikhomirov@628: 		public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) throws HgRuntimeException {
tikhomirov@277: 			try {
tikhomirov@602: 				if (metadata.tryRead(revisionNumber, data)) {
tikhomirov@277: 					// da is in prepared state (i.e. we consumed all bytes up to metadata end).
tikhomirov@277: 					// However, it's not safe to assume delegate won't call da.reset() for some reason,
tikhomirov@277: 					// and we need to ensure predictable result.
tikhomirov@277: 					data.reset();
tikhomirov@602: 					int offset = metadata.dataOffset(revisionNumber);
tikhomirov@602: 					data = new FilterDataAccess(data, offset, data.length() - offset);
tikhomirov@602: 				} else {
tikhomirov@602: 					data.reset();
tikhomirov@277: 				}
tikhomirov@277: 				if (delegate != null) {
tikhomirov@277: 					delegate.next(revisionNumber, actualLen, baseRevision, linkRevision, parent1Revision, parent2Revision, nodeid, data);
tikhomirov@277: 				}
tikhomirov@277: 			} catch (IOException ex) {
tikhomirov@277: 				recordFailure(ex);
tikhomirov@396: 			} catch (HgInvalidControlFileException ex) {
tikhomirov@396: 				recordFailure(ex.isRevisionIndexSet() ? ex : ex.setRevisionIndex(revisionNumber));
tikhomirov@157: 			}
tikhomirov@277: 		}
tikhomirov@277: 
tikhomirov@322: 		@Override
tikhomirov@425: 		public void checkFailed() throws HgRuntimeException, IOException, CancelledException {
tikhomirov@322: 			super.checkFailed();
tikhomirov@322: 			if (delegate instanceof ErrorHandlingInspector) {
tikhomirov@425: 				// TODO need to add ErrorDestination (ErrorTarget/Acceptor?) and pass it around (much like CancelSupport get passed)
tikhomirov@322: 				// so that delegate would be able report its failures directly to caller without this hack
tikhomirov@322: 				((ErrorHandlingInspector) delegate).checkFailed();
tikhomirov@322: 			}
tikhomirov@322: 		}
tikhomirov@17: 	}
tikhomirov@2: }