tikhomirov@22: /* tikhomirov@534: * 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@425: import static org.tmatesoft.hg.repo.HgRepository.*; tikhomirov@456: import static org.tmatesoft.hg.util.LogFacility.Severity.Warn; tikhomirov@56: tikhomirov@157: import java.io.IOException; tikhomirov@157: import java.nio.ByteBuffer; tikhomirov@317: import java.util.ArrayList; tikhomirov@56: import java.util.Arrays; tikhomirov@171: import java.util.List; tikhomirov@29: tikhomirov@74: import org.tmatesoft.hg.core.Nodeid; tikhomirov@157: import org.tmatesoft.hg.internal.DataAccess; tikhomirov@324: import org.tmatesoft.hg.internal.Experimental; tikhomirov@448: import org.tmatesoft.hg.internal.IntMap; tikhomirov@355: import org.tmatesoft.hg.internal.Preview; tikhomirov@77: import org.tmatesoft.hg.internal.RevlogStream; tikhomirov@324: import org.tmatesoft.hg.util.Adaptable; tikhomirov@157: import org.tmatesoft.hg.util.ByteChannel; tikhomirov@157: import org.tmatesoft.hg.util.CancelSupport; tikhomirov@157: import org.tmatesoft.hg.util.CancelledException; tikhomirov@355: import org.tmatesoft.hg.util.LogFacility; tikhomirov@157: import org.tmatesoft.hg.util.ProgressSupport; tikhomirov@74: tikhomirov@74: tikhomirov@2: /** tikhomirov@157: * Base class for all Mercurial entities that are serialized in a so called revlog format (changelog, manifest, data files). tikhomirov@157: * tikhomirov@157: * Implementation note: tikhomirov@157: * Hides actual actual revlog stream implementation and its access methods (i.e. RevlogStream.Inspector), iow shall not expose anything internal tikhomirov@157: * in public methods. tikhomirov@157: * tikhomirov@74: * @author Artem Tikhomirov tikhomirov@74: * @author TMate Software Ltd. tikhomirov@2: */ tikhomirov@74: abstract class Revlog { tikhomirov@2: tikhomirov@115: private final HgRepository repo; tikhomirov@21: protected final RevlogStream content; tikhomirov@2: tikhomirov@115: protected Revlog(HgRepository hgRepo, RevlogStream contentStream) { tikhomirov@2: if (hgRepo == null) { tikhomirov@74: throw new IllegalArgumentException(); tikhomirov@74: } tikhomirov@115: if (contentStream == null) { tikhomirov@74: throw new IllegalArgumentException(); tikhomirov@2: } tikhomirov@115: repo = hgRepo; tikhomirov@115: content = contentStream; tikhomirov@115: } tikhomirov@115: tikhomirov@115: // invalid Revlog tikhomirov@115: protected Revlog(HgRepository hgRepo) { tikhomirov@115: repo = hgRepo; tikhomirov@115: content = null; tikhomirov@2: } tikhomirov@2: tikhomirov@2: public final HgRepository getRepo() { tikhomirov@115: return repo; tikhomirov@2: } tikhomirov@2: tikhomirov@425: /** tikhomirov@425: * @return total number of revisions kept in this revlog tikhomirov@425: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception tikhomirov@425: */ tikhomirov@425: public final int getRevisionCount() throws HgRuntimeException { tikhomirov@21: return content.revisionCount(); tikhomirov@21: } tikhomirov@80: tikhomirov@425: /** tikhomirov@534: * @return index of last known revision, a.k.a. {@link HgRepository#TIP}, or {@link HgRepository#NO_REVISION} if revlog is empty tikhomirov@425: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception tikhomirov@425: */ tikhomirov@425: public final int getLastRevision() throws HgRuntimeException { tikhomirov@534: // although old code gives correct result when revlog is empty (NO_REVISION deliberately == -1), tikhomirov@534: // it's still better to be explicit tikhomirov@534: int revCount = content.revisionCount(); tikhomirov@534: return revCount == 0 ? NO_REVISION : revCount - 1; tikhomirov@135: } tikhomirov@354: tikhomirov@354: /** tikhomirov@388: * Map revision index to unique revision identifier (nodeid). tikhomirov@354: * tikhomirov@437: * @param revisionIndex index of the entry in this revlog, may be {@link HgRepository#TIP} tikhomirov@354: * @return revision nodeid of the entry tikhomirov@354: * tikhomirov@425: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception tikhomirov@354: */ tikhomirov@437: public final Nodeid getRevision(int revisionIndex) throws HgRuntimeException { tikhomirov@328: // XXX cache nodeids? Rather, if context.getCache(this).getRevisionMap(create == false) != null, use it tikhomirov@437: return Nodeid.fromBinary(content.nodeid(revisionIndex), 0); tikhomirov@80: } tikhomirov@317: tikhomirov@328: /** tikhomirov@425: * Effective alternative to map few revision indexes to corresponding nodeids at once. tikhomirov@425: *
Note, there are few aspects to be careful about when using this method
true if revision is part of this revlog
tikhomirov@425: 	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@354: 	 */
tikhomirov@425: 	public final boolean isKnown(Nodeid nodeid) throws HgRuntimeException {
tikhomirov@367: 		final int rn = content.findRevisionIndex(nodeid);
tikhomirov@218: 		if (BAD_REVISION == rn) {
tikhomirov@39: 			return false;
tikhomirov@39: 		}
tikhomirov@49: 		if (rn < 0 || rn >= content.revisionCount()) {
tikhomirov@49: 			// Sanity check
tikhomirov@423: 			throw new HgInvalidStateException(String.format("Revision index %d found for nodeid %s is not from the range [0..%d]", rn, nodeid.shortNotation(), content.revisionCount()-1));
tikhomirov@49: 		}
tikhomirov@49: 		return true;
tikhomirov@39: 	}
tikhomirov@49: 
tikhomirov@37: 	/**
tikhomirov@394: 	 * Access to revision data as is, equivalent to rawContent(getRevisionIndex(nodeid), sink)
tikhomirov@394: 	 * 
tikhomirov@394: 	 * @param nodeid revision to retrieve
tikhomirov@394: 	 * @param sink data destination
tikhomirov@394: 	 * 
tikhomirov@394: 	 * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog
tikhomirov@394: 	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
tikhomirov@394: 	 * @throws CancelledException if content retrieval operation was cancelled
tikhomirov@394: 	 * 
tikhomirov@394: 	 * @see #rawContent(int, ByteChannel)
tikhomirov@37: 	 */
tikhomirov@394: 	protected void rawContent(Nodeid nodeid, ByteChannel sink) throws HgInvalidControlFileException, CancelledException, HgInvalidRevisionException {
tikhomirov@367: 		rawContent(getRevisionIndex(nodeid), sink);
tikhomirov@37: 	}
tikhomirov@29: 	
tikhomirov@37: 	/**
tikhomirov@394: 	 * Access to revision data as is (decompressed, but otherwise unprocessed, i.e. not parsed for e.g. changeset or manifest entries).
tikhomirov@394: 	 *  
tikhomirov@416: 	 * @param revisionIndex index of this revlog change (not a changelog revision index), non-negative. From predefined constants, only {@link HgRepository#TIP} makes sense.
tikhomirov@394: 	 * @param sink data destination
tikhomirov@394: 	 * 
tikhomirov@394: 	 * @throws HgInvalidRevisionException if supplied argument doesn't represent revision index in this revlog
tikhomirov@394: 	 * @throws HgInvalidControlFileException if access to revlog index/data entry failed
tikhomirov@394: 	 * @throws CancelledException if content retrieval operation was cancelled
tikhomirov@37: 	 */
tikhomirov@416: 	protected void rawContent(int revisionIndex, ByteChannel sink) throws HgInvalidControlFileException, CancelledException, HgInvalidRevisionException {
tikhomirov@157: 		if (sink == null) {
tikhomirov@157: 			throw new IllegalArgumentException();
tikhomirov@157: 		}
tikhomirov@394: 		try {
tikhomirov@490: 			ContentPipe insp = new ContentPipe(sink, 0, repo.getSessionContext().getLog());
tikhomirov@394: 			insp.checkCancelled();
tikhomirov@416: 			content.iterate(revisionIndex, revisionIndex, true, insp);
tikhomirov@394: 			insp.checkFailed();
tikhomirov@394: 		} catch (IOException ex) {
tikhomirov@416: 			HgInvalidControlFileException e = new HgInvalidControlFileException(String.format("Access to revision %d content failed", revisionIndex), ex, null);
tikhomirov@416: 			e.setRevisionIndex(revisionIndex);
tikhomirov@418: 			// TODO post 1.0 e.setFileName(content.getIndexFile() or this.getHumanFriendlyPath()) - shall decide whether 
tikhomirov@418: 			// protected abstract getHFPath() with impl in HgDataFile, HgManifest and HgChangelog or path is data of either Revlog or RevlogStream
tikhomirov@394: 			// Do the same (add file name) below
tikhomirov@394: 			throw e;
tikhomirov@394: 		} catch (HgInvalidControlFileException ex) {
tikhomirov@425: 			throw ex.isRevisionIndexSet() ? ex : ex.setRevisionIndex(revisionIndex);
tikhomirov@394: 		}
tikhomirov@37: 	}
tikhomirov@37: 
tikhomirov@56: 	/**
tikhomirov@405: 	 * Fills supplied arguments with information about revision parents.
tikhomirov@56: 	 * 
tikhomirov@56: 	 * @param revision - revision to query parents, or {@link HgRepository#TIP}
tikhomirov@405: 	 * @param parentRevisions - int[2] to get local revision numbers of parents (e.g. {6, -1}), {@link HgRepository#NO_REVISION} indicates parent not set
tikhomirov@56: 	 * @param parent1 - byte[20] or null, if parent's nodeid is not needed
tikhomirov@56: 	 * @param parent2 - byte[20] or null, if second parent's nodeid is not needed
tikhomirov@425: 	 * @throws IllegalArgumentException if passed arrays can't fit requested data
tikhomirov@425: 	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@56: 	 */
tikhomirov@425: 	public void parents(int revision, int[] parentRevisions, byte[] parent1, byte[] parent2) throws HgRuntimeException, IllegalArgumentException {
tikhomirov@56: 		if (revision != TIP && !(revision >= 0 && revision < content.revisionCount())) {
tikhomirov@347: 			throw new HgInvalidRevisionException(revision);
tikhomirov@56: 		}
tikhomirov@56: 		if (parentRevisions == null || parentRevisions.length < 2) {
tikhomirov@56: 			throw new IllegalArgumentException(String.valueOf(parentRevisions));
tikhomirov@56: 		}
tikhomirov@56: 		if (parent1 != null && parent1.length < 20) {
tikhomirov@56: 			throw new IllegalArgumentException(parent1.toString());
tikhomirov@56: 		}
tikhomirov@56: 		if (parent2 != null && parent2.length < 20) {
tikhomirov@56: 			throw new IllegalArgumentException(parent2.toString());
tikhomirov@56: 		}
tikhomirov@77: 		class ParentCollector implements RevlogStream.Inspector {
tikhomirov@56: 			public int p1 = -1;
tikhomirov@56: 			public int p2 = -1;
tikhomirov@56: 			public byte[] nodeid;
tikhomirov@56: 			
tikhomirov@157: 			public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) {
tikhomirov@56: 				p1 = parent1Revision;
tikhomirov@56: 				p2 = parent2Revision;
tikhomirov@56: 				this.nodeid = new byte[20];
tikhomirov@56: 				// nodeid arg now comes in 32 byte from (as in file format description), however upper 12 bytes are zeros.
tikhomirov@56: 				System.arraycopy(nodeid, nodeid.length > 20 ? nodeid.length - 20 : 0, this.nodeid, 0, 20);
tikhomirov@56: 			}
tikhomirov@56: 		};
tikhomirov@56: 		ParentCollector pc = new ParentCollector();
tikhomirov@56: 		content.iterate(revision, revision, false, pc);
tikhomirov@405: 		// although next code looks odd (NO_REVISION *is* -1), it's safer to be explicit
tikhomirov@405: 		parentRevisions[0] = pc.p1 == -1 ? NO_REVISION : pc.p1;
tikhomirov@405: 		parentRevisions[1] = pc.p2 == -1 ? NO_REVISION : pc.p2;
tikhomirov@56: 		if (parent1 != null) {
tikhomirov@405: 			if (parentRevisions[0] == NO_REVISION) {
tikhomirov@56: 				Arrays.fill(parent1, 0, 20, (byte) 0);
tikhomirov@56: 			} else {
tikhomirov@56: 				content.iterate(parentRevisions[0], parentRevisions[0], false, pc);
tikhomirov@56: 				System.arraycopy(pc.nodeid, 0, parent1, 0, 20);
tikhomirov@56: 			}
tikhomirov@56: 		}
tikhomirov@56: 		if (parent2 != null) {
tikhomirov@405: 			if (parentRevisions[1] == NO_REVISION) {
tikhomirov@56: 				Arrays.fill(parent2, 0, 20, (byte) 0);
tikhomirov@56: 			} else {
tikhomirov@56: 				content.iterate(parentRevisions[1], parentRevisions[1], false, pc);
tikhomirov@56: 				System.arraycopy(pc.nodeid, 0, parent2, 0, 20);
tikhomirov@56: 			}
tikhomirov@56: 		}
tikhomirov@56: 	}
tikhomirov@425: 
tikhomirov@425: 	/**
tikhomirov@425: 	 * EXPERIMENTAL CODE, DO NOT USE
tikhomirov@425: 	 * 
tikhomirov@425: 	 * Alternative revlog iteration
tikhomirov@425: 	 * 
tikhomirov@425: 	 * @param start
tikhomirov@425: 	 * @param end
tikhomirov@425: 	 * @param inspector
tikhomirov@425: 	 * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception
tikhomirov@425: 	 */
tikhomirov@324: 	@Experimental
tikhomirov@471: 	public final void indexWalk(int start, int end, final Revlog.Inspector inspector) throws HgRuntimeException { 
tikhomirov@324: 		int lastRev = getLastRevision();
tikhomirov@448: 		final int _start = start == TIP ? lastRev : start;
tikhomirov@324: 		if (end == TIP) {
tikhomirov@324: 			end = lastRev;
tikhomirov@324: 		}
tikhomirov@356: 		final RevisionInspector revisionInsp = Adaptable.Factory.getAdapter(inspector, RevisionInspector.class, null);
tikhomirov@356: 		final ParentInspector parentInsp = Adaptable.Factory.getAdapter(inspector, ParentInspector.class, null);
tikhomirov@448: 		final Nodeid[] allRevisions = parentInsp == null ? null : new Nodeid[end - _start + 1];
tikhomirov@448: 		// next are to build set of parent indexes that are not part of the range iteration
tikhomirov@448: 		// i.e. those parents we need to read separately. See Issue 31 for details.
tikhomirov@448: 		final int[]      firstParentIndexes = parentInsp == null || _start == 0 ? null : new int[allRevisions.length];
tikhomirov@448: 		final int[]     secondParentIndexes = parentInsp == null || _start == 0 ? null : new int[allRevisions.length];
tikhomirov@448: 		final IntMapnull
tikhomirov@277: 		 * @param seekOffset - when positive, orders to pipe bytes to the sink starting from specified offset, not from the first byte available in DataAccess
tikhomirov@355: 		 * @param log optional facility to put warnings/debug messages into, may be null.
tikhomirov@277: 		 */
tikhomirov@355: 		public ContentPipe(ByteChannel _sink, int seekOffset, LogFacility log) {
tikhomirov@277: 			assert _sink != null;
tikhomirov@277: 			sink = _sink;
tikhomirov@277: 			setCancelSupport(CancelSupport.Factory.get(_sink));
tikhomirov@277: 			offset = seekOffset;
tikhomirov@355: 			logFacility = log;
tikhomirov@277: 		}
tikhomirov@277: 		
tikhomirov@425: 		protected void prepare(int revisionNumber, DataAccess da) throws IOException {
tikhomirov@277: 			if (offset > 0) { // save few useless reset/rewind operations
tikhomirov@277: 				da.seek(offset);
tikhomirov@277: 			}
tikhomirov@277: 		}
tikhomirov@277: 
tikhomirov@277: 		public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess da) {
tikhomirov@277: 			try {
tikhomirov@277: 				prepare(revisionNumber, da); // XXX perhaps, prepare shall return DA (sliced, if needed)
tikhomirov@277: 				final ProgressSupport progressSupport = ProgressSupport.Factory.get(sink);
tikhomirov@355: 				ByteBuffer buf = ByteBuffer.allocate(actualLen > 8192 ? 8192 : actualLen);
tikhomirov@356: 				Preview p = Adaptable.Factory.getAdapter(sink, Preview.class, null);
tikhomirov@355: 				if (p != null) {
tikhomirov@355: 					progressSupport.start(2 * da.length());
tikhomirov@355: 					while (!da.isEmpty()) {
tikhomirov@355: 						checkCancelled();
tikhomirov@355: 						da.readBytes(buf);
tikhomirov@355: 						p.preview(buf);
tikhomirov@355: 						buf.clear();
tikhomirov@355: 					}
tikhomirov@355: 					da.reset();
tikhomirov@355: 					prepare(revisionNumber, da);
tikhomirov@355: 					progressSupport.worked(da.length());
tikhomirov@355: 					buf.clear();
tikhomirov@355: 				} else {
tikhomirov@355: 					progressSupport.start(da.length());
tikhomirov@355: 				}
tikhomirov@277: 				while (!da.isEmpty()) {
tikhomirov@277: 					checkCancelled();
tikhomirov@277: 					da.readBytes(buf);
tikhomirov@355: 					buf.flip(); // post: position == 0
tikhomirov@277: 					// XXX I may not rely on returned number of bytes but track change in buf position instead.
tikhomirov@355: 					
tikhomirov@355: 					int consumed = sink.write(buf);
tikhomirov@355: 					if ((consumed == 0 || consumed != buf.position()) && logFacility != null) {
tikhomirov@456: 						logFacility.dump(getClass(), Warn, "Bad data sink when reading revision %d. Reported %d bytes consumed, byt actually read %d", revisionNumber, consumed, buf.position());
tikhomirov@355: 					}
tikhomirov@355: 					if (buf.position() == 0) {
tikhomirov@423: 						throw new HgInvalidStateException("Bad sink implementation (consumes no bytes) results in endless loop");
tikhomirov@355: 					}
tikhomirov@355: 					buf.compact(); // ensure (a) there's space for new (b) data starts at 0
tikhomirov@277: 					progressSupport.worked(consumed);
tikhomirov@277: 				}
tikhomirov@277: 				progressSupport.done(); // XXX shall specify whether #done() is invoked always or only if completed successfully.
tikhomirov@277: 			} catch (IOException ex) {
tikhomirov@277: 				recordFailure(ex);
tikhomirov@277: 			} catch (CancelledException ex) {
tikhomirov@277: 				recordFailure(ex);
tikhomirov@277: 			}
tikhomirov@277: 		}
tikhomirov@157: 	}
tikhomirov@2: }