tikhomirov@36: /* tikhomirov@532: * Copyright (c) 2011-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@36: */ tikhomirov@74: package org.tmatesoft.hg.repo; tikhomirov@36: tikhomirov@36: import java.io.File; tikhomirov@36: import java.io.IOException; tikhomirov@512: import java.util.ConcurrentModificationException; tikhomirov@36: tikhomirov@74: import org.tmatesoft.hg.core.Nodeid; tikhomirov@357: import org.tmatesoft.hg.core.SessionContext; tikhomirov@157: import org.tmatesoft.hg.internal.ByteArrayChannel; tikhomirov@157: import org.tmatesoft.hg.internal.ByteArrayDataAccess; tikhomirov@512: import org.tmatesoft.hg.internal.Callback; tikhomirov@74: import org.tmatesoft.hg.internal.DataAccess; tikhomirov@74: import org.tmatesoft.hg.internal.DataAccessProvider; tikhomirov@74: import org.tmatesoft.hg.internal.DigestHelper; tikhomirov@358: import org.tmatesoft.hg.internal.Experimental; tikhomirov@169: import org.tmatesoft.hg.internal.InflaterDataAccess; tikhomirov@526: import org.tmatesoft.hg.internal.Internals; tikhomirov@512: import org.tmatesoft.hg.internal.Lifecycle; tikhomirov@329: import org.tmatesoft.hg.internal.Patch; tikhomirov@154: import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; tikhomirov@512: import org.tmatesoft.hg.util.Adaptable; tikhomirov@157: import org.tmatesoft.hg.util.CancelledException; tikhomirov@74: tikhomirov@36: /** tikhomirov@423: * WORK IN PROGRESS tikhomirov@423: * tikhomirov@36: * @see http://mercurial.selenic.com/wiki/BundleFormat tikhomirov@169: * tikhomirov@74: * @author Artem Tikhomirov tikhomirov@74: * @author TMate Software Ltd. tikhomirov@36: */ tikhomirov@467: @Experimental(reason="API is not stable") tikhomirov@36: public class HgBundle { tikhomirov@36: tikhomirov@36: private final File bundleFile; tikhomirov@36: private final DataAccessProvider accessProvider; tikhomirov@357: // private final SessionContext sessionContext; tikhomirov@512: private Lifecycle.BasicCallback flowControl; tikhomirov@36: tikhomirov@357: HgBundle(SessionContext ctx, DataAccessProvider dap, File bundle) { tikhomirov@357: // sessionContext = ctx; tikhomirov@36: accessProvider = dap; tikhomirov@36: bundleFile = bundle; tikhomirov@36: } tikhomirov@36: tikhomirov@169: private DataAccess getDataStream() throws IOException { tikhomirov@37: DataAccess da = accessProvider.create(bundleFile); tikhomirov@169: byte[] signature = new byte[6]; tikhomirov@169: if (da.length() > 6) { tikhomirov@169: da.readBytes(signature, 0, 6); tikhomirov@169: if (signature[0] == 'H' && signature[1] == 'G' && signature[2] == '1' && signature[3] == '0') { tikhomirov@169: if (signature[4] == 'G' && signature[5] == 'Z') { tikhomirov@169: return new InflaterDataAccess(da, 6, da.length() - 6); tikhomirov@169: } tikhomirov@169: if (signature[4] == 'B' && signature[5] == 'Z') { tikhomirov@526: throw Internals.notImplemented(); tikhomirov@169: } tikhomirov@169: if (signature[4] != 'U' || signature[5] != 'N') { tikhomirov@423: throw new HgInvalidStateException(String.format("Bad bundle signature: %s", String.valueOf(signature))); tikhomirov@169: } tikhomirov@169: // "...UN", fall-through tikhomirov@169: } else { tikhomirov@169: da.reset(); tikhomirov@39: } tikhomirov@169: } tikhomirov@169: return da; tikhomirov@169: } tikhomirov@169: tikhomirov@186: private int uses = 0; tikhomirov@186: public HgBundle link() { tikhomirov@186: uses++; tikhomirov@186: return this; tikhomirov@186: } tikhomirov@186: public void unlink() { tikhomirov@186: uses--; tikhomirov@186: if (uses == 0 && bundleFile != null) { tikhomirov@186: bundleFile.deleteOnExit(); tikhomirov@186: } tikhomirov@186: } tikhomirov@186: public boolean inUse() { tikhomirov@186: return uses > 0; tikhomirov@186: } tikhomirov@186: tikhomirov@182: /** tikhomirov@182: * Get changes recorded in the bundle that are missing from the supplied repository. tikhomirov@182: * @param hgRepo repository that shall possess base revision for this bundle tikhomirov@182: * @param inspector callback to get each changeset found tikhomirov@182: */ tikhomirov@423: public void changes(final HgRepository hgRepo, final HgChangelog.Inspector inspector) throws HgRuntimeException { tikhomirov@182: Inspector bundleInsp = new Inspector() { tikhomirov@169: DigestHelper dh = new DigestHelper(); tikhomirov@169: boolean emptyChangelog = true; tikhomirov@169: private DataAccess prevRevContent; tikhomirov@182: private int revisionIndex; tikhomirov@169: tikhomirov@169: public void changelogStart() { tikhomirov@169: emptyChangelog = true; tikhomirov@182: revisionIndex = 0; tikhomirov@169: } tikhomirov@169: tikhomirov@169: public void changelogEnd() { tikhomirov@169: if (emptyChangelog) { tikhomirov@169: throw new IllegalStateException("No changelog group in the bundle"); // XXX perhaps, just be silent and/or log? tikhomirov@42: } tikhomirov@37: } tikhomirov@169: tikhomirov@169: /* tikhomirov@169: * Despite that BundleFormat wiki says: "Each Changelog entry patches the result of all previous patches tikhomirov@169: * (the previous, or parent patch of a given patch p is the patch that has a node equal to p's p1 field)", tikhomirov@169: * it seems not to hold true. Instead, each entry patches previous one, regardless of whether the one tikhomirov@169: * before is its parent (i.e. ge.firstParent()) or not. tikhomirov@169: * tikhomirov@169: Actual state in the changelog.i tikhomirov@169: Index Offset Flags Packed Actual Base Rev Link Rev Parent1 Parent2 nodeid tikhomirov@169: 50: 9212 0 209 329 48 50 49 -1 f1db8610da62a3e0beb8d360556ee1fd6eb9885e tikhomirov@169: 51: 9421 0 278 688 48 51 50 -1 9429c7bd1920fab164a9d2b621d38d57bcb49ae0 tikhomirov@169: 52: 9699 0 154 179 52 52 50 -1 30bd389788464287cee22ccff54c330a4b715de5 tikhomirov@169: 53: 9853 0 133 204 52 53 51 52 a6f39e595b2b54f56304470269a936ead77f5725 tikhomirov@169: 54: 9986 0 156 182 54 54 52 -1 fd4f2c98995beb051070630c272a9be87bef617d tikhomirov@169: tikhomirov@169: Excerpt from bundle (nodeid, p1, p2, cs): tikhomirov@169: f1db8610da62a3e0beb8d360556ee1fd6eb9885e 26e3eeaa39623de552b45ee1f55c14f36460f220 0000000000000000000000000000000000000000 f1db8610da62a3e0beb8d360556ee1fd6eb9885e; patches:4 tikhomirov@169: 9429c7bd1920fab164a9d2b621d38d57bcb49ae0 f1db8610da62a3e0beb8d360556ee1fd6eb9885e 0000000000000000000000000000000000000000 9429c7bd1920fab164a9d2b621d38d57bcb49ae0; patches:3 tikhomirov@169: > 30bd389788464287cee22ccff54c330a4b715de5 f1db8610da62a3e0beb8d360556ee1fd6eb9885e 0000000000000000000000000000000000000000 30bd389788464287cee22ccff54c330a4b715de5; patches:3 tikhomirov@169: a6f39e595b2b54f56304470269a936ead77f5725 9429c7bd1920fab164a9d2b621d38d57bcb49ae0 30bd389788464287cee22ccff54c330a4b715de5 a6f39e595b2b54f56304470269a936ead77f5725; patches:3 tikhomirov@169: fd4f2c98995beb051070630c272a9be87bef617d 30bd389788464287cee22ccff54c330a4b715de5 0000000000000000000000000000000000000000 fd4f2c98995beb051070630c272a9be87bef617d; patches:3 tikhomirov@169: tikhomirov@169: To recreate 30bd..e5, one have to take content of 9429..e0, not its p1 f1db..5e tikhomirov@169: */ tikhomirov@169: public boolean element(GroupElement ge) { tikhomirov@169: emptyChangelog = false; tikhomirov@169: HgChangelog changelog = hgRepo.getChangelog(); tikhomirov@169: try { tikhomirov@169: if (prevRevContent == null) { tikhomirov@274: if (ge.firstParent().isNull() && ge.secondParent().isNull()) { tikhomirov@169: prevRevContent = new ByteArrayDataAccess(new byte[0]); tikhomirov@169: } else { tikhomirov@169: final Nodeid base = ge.firstParent(); tikhomirov@169: if (!changelog.isKnown(base) /*only first parent, that's Bundle contract*/) { tikhomirov@169: throw new IllegalStateException(String.format("Revision %s needs a parent %s, which is missing in the supplied repo %s", ge.node().shortNotation(), base.shortNotation(), hgRepo.toString())); tikhomirov@169: } tikhomirov@169: ByteArrayChannel bac = new ByteArrayChannel(); tikhomirov@418: changelog.rawContent(base, bac); // TODO post-1.0 get DataAccess directly, to avoid tikhomirov@169: // extra byte[] (inside ByteArrayChannel) duplication just for the sake of subsequent ByteArrayDataChannel wrap. tikhomirov@169: prevRevContent = new ByteArrayDataAccess(bac.toArray()); tikhomirov@169: } tikhomirov@169: } tikhomirov@169: // tikhomirov@169: byte[] csetContent = ge.apply(prevRevContent); tikhomirov@169: dh = dh.sha1(ge.firstParent(), ge.secondParent(), csetContent); // XXX ge may give me access to byte[] content of nodeid directly, perhaps, I don't need DH to be friend of Nodeid? tikhomirov@169: if (!ge.node().equalsTo(dh.asBinary())) { tikhomirov@423: throw new HgInvalidStateException(String.format("Integrity check failed on %s, node: %s", bundleFile, ge.node().shortNotation())); tikhomirov@169: } tikhomirov@169: ByteArrayDataAccess csetDataAccess = new ByteArrayDataAccess(csetContent); tikhomirov@169: RawChangeset cs = RawChangeset.parse(csetDataAccess); tikhomirov@182: inspector.next(revisionIndex++, ge.node(), cs); tikhomirov@170: prevRevContent.done(); tikhomirov@169: prevRevContent = csetDataAccess.reset(); tikhomirov@169: } catch (CancelledException ex) { tikhomirov@169: return false; tikhomirov@423: } catch (IOException ex) { tikhomirov@423: throw new HgInvalidFileException("Invalid bundle file", ex, bundleFile); // TODO post-1.0 revisit exception handling tikhomirov@427: } catch (HgInvalidDataFormatException ex) { tikhomirov@423: throw new HgInvalidControlFileException("Invalid bundle file", ex, bundleFile); tikhomirov@169: } tikhomirov@169: return true; tikhomirov@169: } tikhomirov@169: tikhomirov@169: public void manifestStart() {} tikhomirov@169: public void manifestEnd() {} tikhomirov@169: public void fileStart(String name) {} tikhomirov@169: public void fileEnd(String name) {} tikhomirov@169: tikhomirov@169: }; tikhomirov@423: inspectChangelog(bundleInsp); tikhomirov@169: } tikhomirov@169: tikhomirov@169: // callback to minimize amount of Strings and Nodeids instantiated tikhomirov@512: @Callback tikhomirov@169: public interface Inspector { tikhomirov@169: void changelogStart(); tikhomirov@169: tikhomirov@169: void changelogEnd(); tikhomirov@169: tikhomirov@169: void manifestStart(); tikhomirov@169: tikhomirov@169: void manifestEnd(); tikhomirov@169: tikhomirov@169: void fileStart(String name); tikhomirov@169: tikhomirov@169: void fileEnd(String name); tikhomirov@169: tikhomirov@169: /** tikhomirov@170: * XXX desperately need exceptions here tikhomirov@170: * @param element data element, instance might be reused, don't keep a reference to it or its raw data tikhomirov@169: * @return true to continue tikhomirov@169: */ tikhomirov@169: boolean element(GroupElement element); tikhomirov@169: } tikhomirov@169: tikhomirov@423: /** tikhomirov@423: * @param inspector callback to visit changelog entries tikhomirov@423: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception tikhomirov@423: * @throws IllegalArgumentException if inspector argument is null tikhomirov@423: */ tikhomirov@423: public void inspectChangelog(Inspector inspector) throws HgRuntimeException { tikhomirov@169: if (inspector == null) { tikhomirov@169: throw new IllegalArgumentException(); tikhomirov@169: } tikhomirov@512: final Lifecycle lifecycle = lifecycleSetUp(inspector); tikhomirov@295: DataAccess da = null; tikhomirov@169: try { tikhomirov@295: da = getDataStream(); tikhomirov@182: internalInspectChangelog(da, inspector); tikhomirov@295: } catch (IOException ex) { tikhomirov@295: throw new HgInvalidFileException("Bundle.inspectChangelog failed", ex, bundleFile); tikhomirov@37: } finally { tikhomirov@295: if (da != null) { tikhomirov@295: da.done(); tikhomirov@295: } tikhomirov@512: lifecycleTearDown(lifecycle); tikhomirov@37: } tikhomirov@37: } tikhomirov@37: tikhomirov@423: /** tikhomirov@423: * @param inspector callback to visit manifest entries tikhomirov@423: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception tikhomirov@423: * @throws IllegalArgumentException if inspector argument is null tikhomirov@423: */ tikhomirov@423: public void inspectManifest(Inspector inspector) throws HgRuntimeException { tikhomirov@169: if (inspector == null) { tikhomirov@169: throw new IllegalArgumentException(); tikhomirov@169: } tikhomirov@512: final Lifecycle lifecycle = lifecycleSetUp(inspector); tikhomirov@295: DataAccess da = null; tikhomirov@36: try { tikhomirov@295: da = getDataStream(); tikhomirov@169: if (da.isEmpty()) { tikhomirov@169: return; tikhomirov@169: } tikhomirov@169: skipGroup(da); // changelog tikhomirov@182: internalInspectManifest(da, inspector); tikhomirov@295: } catch (IOException ex) { tikhomirov@295: throw new HgInvalidFileException("Bundle.inspectManifest failed", ex, bundleFile); tikhomirov@36: } finally { tikhomirov@295: if (da != null) { tikhomirov@295: da.done(); tikhomirov@295: } tikhomirov@512: lifecycleTearDown(lifecycle); tikhomirov@36: } tikhomirov@36: } tikhomirov@36: tikhomirov@423: /** tikhomirov@423: * @param inspector callback to visit file entries tikhomirov@423: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception tikhomirov@423: * @throws IllegalArgumentException if inspector argument is null tikhomirov@423: */ tikhomirov@423: public void inspectFiles(Inspector inspector) throws HgRuntimeException { tikhomirov@169: if (inspector == null) { tikhomirov@169: throw new IllegalArgumentException(); tikhomirov@169: } tikhomirov@512: final Lifecycle lifecycle = lifecycleSetUp(inspector); tikhomirov@295: DataAccess da = null; tikhomirov@169: try { tikhomirov@295: da = getDataStream(); tikhomirov@182: if (da.isEmpty()) { tikhomirov@182: return; tikhomirov@169: } tikhomirov@182: skipGroup(da); // changelog tikhomirov@182: if (da.isEmpty()) { tikhomirov@182: return; tikhomirov@169: } tikhomirov@182: skipGroup(da); // manifest tikhomirov@182: internalInspectFiles(da, inspector); tikhomirov@295: } catch (IOException ex) { tikhomirov@295: throw new HgInvalidFileException("Bundle.inspectFiles failed", ex, bundleFile); tikhomirov@169: } finally { tikhomirov@295: if (da != null) { tikhomirov@295: da.done(); tikhomirov@295: } tikhomirov@512: lifecycleTearDown(lifecycle); tikhomirov@169: } tikhomirov@169: } tikhomirov@169: tikhomirov@423: /** tikhomirov@423: * @param inspector visit complete bundle (changelog, manifest and file entries) tikhomirov@423: * @throws HgRuntimeException subclass thereof to indicate issues with the library. Runtime exception tikhomirov@423: * @throws IllegalArgumentException if inspector argument is null tikhomirov@423: */ tikhomirov@423: public void inspectAll(Inspector inspector) throws HgRuntimeException { tikhomirov@169: if (inspector == null) { tikhomirov@169: throw new IllegalArgumentException(); tikhomirov@169: } tikhomirov@512: final Lifecycle lifecycle = lifecycleSetUp(inspector); tikhomirov@295: DataAccess da = null; tikhomirov@169: try { tikhomirov@295: da = getDataStream(); tikhomirov@182: internalInspectChangelog(da, inspector); tikhomirov@513: if (flowControl.isStopped()) { tikhomirov@513: return; tikhomirov@513: } tikhomirov@182: internalInspectManifest(da, inspector); tikhomirov@513: if (flowControl.isStopped()) { tikhomirov@513: return; tikhomirov@513: } tikhomirov@182: internalInspectFiles(da, inspector); tikhomirov@295: } catch (IOException ex) { tikhomirov@295: throw new HgInvalidFileException("Bundle.inspectAll failed", ex, bundleFile); tikhomirov@169: } finally { tikhomirov@295: if (da != null) { tikhomirov@295: da.done(); tikhomirov@295: } tikhomirov@512: lifecycleTearDown(lifecycle); tikhomirov@169: } tikhomirov@169: } tikhomirov@512: tikhomirov@512: // initialize flowControl, check for concurrent usage, starts lifecyle, if any tikhomirov@512: // return non-null only if inspector is interested in lifecycle events tikhomirov@512: private Lifecycle lifecycleSetUp(Inspector inspector) throws ConcurrentModificationException { tikhomirov@512: // Don't need flowControl in case Inspector doesn't implement Lifecycle, tikhomirov@512: // however is handy not to expect it == null inside internalInspect* tikhomirov@512: // XXX Once there's need to make this class thread-safe, tikhomirov@512: // shall move flowControl to thread-local state. tikhomirov@512: if (flowControl != null) { tikhomirov@512: throw new ConcurrentModificationException("HgBundle is in use and not thread-safe yet"); tikhomirov@512: } tikhomirov@512: flowControl = new Lifecycle.BasicCallback(); tikhomirov@512: final Lifecycle lifecycle = Adaptable.Factory.getAdapter(inspector, Lifecycle.class, null); tikhomirov@512: if (lifecycle != null) { tikhomirov@512: lifecycle.start(-1, flowControl, flowControl); tikhomirov@512: } tikhomirov@512: return lifecycle; tikhomirov@512: } tikhomirov@512: tikhomirov@512: private void lifecycleTearDown(Lifecycle lifecycle) { tikhomirov@512: if (lifecycle != null) { tikhomirov@512: lifecycle.finish(flowControl); tikhomirov@512: } tikhomirov@512: flowControl = null; tikhomirov@512: } tikhomirov@169: tikhomirov@182: private void internalInspectChangelog(DataAccess da, Inspector inspector) throws IOException { tikhomirov@182: if (da.isEmpty()) { tikhomirov@182: return; tikhomirov@182: } tikhomirov@182: inspector.changelogStart(); tikhomirov@513: if (flowControl.isStopped()) { tikhomirov@513: return; tikhomirov@513: } tikhomirov@182: readGroup(da, inspector); tikhomirov@513: if (flowControl.isStopped()) { tikhomirov@513: return; tikhomirov@513: } tikhomirov@182: inspector.changelogEnd(); tikhomirov@182: } tikhomirov@182: tikhomirov@182: private void internalInspectManifest(DataAccess da, Inspector inspector) throws IOException { tikhomirov@182: if (da.isEmpty()) { tikhomirov@182: return; tikhomirov@182: } tikhomirov@182: inspector.manifestStart(); tikhomirov@513: if (flowControl.isStopped()) { tikhomirov@513: return; tikhomirov@513: } tikhomirov@182: readGroup(da, inspector); tikhomirov@513: if (flowControl.isStopped()) { tikhomirov@513: return; tikhomirov@513: } tikhomirov@182: inspector.manifestEnd(); tikhomirov@182: } tikhomirov@182: tikhomirov@182: private void internalInspectFiles(DataAccess da, Inspector inspector) throws IOException { tikhomirov@182: while (!da.isEmpty()) { tikhomirov@182: int fnameLen = da.readInt(); tikhomirov@182: if (fnameLen <= 4) { tikhomirov@182: break; // null chunk, the last one. tikhomirov@182: } tikhomirov@182: byte[] fnameBuf = new byte[fnameLen - 4]; tikhomirov@182: da.readBytes(fnameBuf, 0, fnameBuf.length); tikhomirov@182: String name = new String(fnameBuf); tikhomirov@182: inspector.fileStart(name); tikhomirov@513: if (flowControl.isStopped()) { tikhomirov@513: return; tikhomirov@513: } tikhomirov@182: readGroup(da, inspector); tikhomirov@513: if (flowControl.isStopped()) { tikhomirov@513: return; tikhomirov@513: } tikhomirov@182: inspector.fileEnd(name); tikhomirov@182: } tikhomirov@182: } tikhomirov@182: tikhomirov@169: private static void readGroup(DataAccess da, Inspector inspector) throws IOException { tikhomirov@36: int len = da.readInt(); tikhomirov@169: boolean good2go = true; tikhomirov@532: Nodeid prevNodeid = Nodeid.NULL; tikhomirov@169: while (len > 4 && !da.isEmpty() && good2go) { tikhomirov@36: byte[] nb = new byte[80]; tikhomirov@36: da.readBytes(nb, 0, 80); tikhomirov@169: int dataLength = len - 84 /* length field + 4 nodeids */; tikhomirov@169: byte[] data = new byte[dataLength]; tikhomirov@169: da.readBytes(data, 0, dataLength); tikhomirov@169: DataAccess slice = new ByteArrayDataAccess(data); // XXX in fact, may pass a slicing DataAccess. tikhomirov@169: // Just need to make sure that we seek to proper location afterwards (where next GroupElement starts), tikhomirov@169: // regardless whether that slice has read it or not. tikhomirov@532: GroupElement ge = new GroupElement(nb, prevNodeid, slice); tikhomirov@169: good2go = inspector.element(ge); tikhomirov@170: slice.done(); // BADA doesn't implement done(), but it could (e.g. free array) tikhomirov@170: /// and we'd better tell it we are not going to use it any more. However, it's important to ensure Inspector tikhomirov@170: // implementations out there do not retain GroupElement.rawData() tikhomirov@532: prevNodeid = ge.node(); tikhomirov@36: len = da.isEmpty() ? 0 : da.readInt(); tikhomirov@36: } tikhomirov@169: // need to skip up to group end if inspector told he don't want to continue with the group, tikhomirov@169: // because outer code may try to read next group immediately as we return back. tikhomirov@169: while (len > 4 && !da.isEmpty()) { tikhomirov@169: da.skip(len - 4 /* length field */); tikhomirov@169: len = da.isEmpty() ? 0 : da.readInt(); tikhomirov@169: } tikhomirov@36: } tikhomirov@36: tikhomirov@169: private static void skipGroup(DataAccess da) throws IOException { tikhomirov@169: int len = da.readInt(); tikhomirov@169: while (len > 4 && !da.isEmpty()) { tikhomirov@169: da.skip(len - 4); // sizeof(int) tikhomirov@169: len = da.isEmpty() ? 0 : da.readInt(); tikhomirov@169: } tikhomirov@169: } tikhomirov@169: tikhomirov@358: @Experimental(reason="Cumbersome API, rawData and apply with byte[] perhaps need replacement with ByteChannel/ByteBuffer, and better Exceptions. Perhaps, shall split into interface and impl") tikhomirov@169: public static class GroupElement { tikhomirov@169: private final byte[] header; // byte[80] takes 120 bytes, 4 Nodeids - 192 tikhomirov@169: private final DataAccess dataAccess; tikhomirov@329: private Patch patches; tikhomirov@532: private final Nodeid deltaBase; tikhomirov@169: tikhomirov@532: GroupElement(byte[] fourNodeids, Nodeid deltaBaseRev, DataAccess rawDataAccess) { tikhomirov@36: assert fourNodeids != null && fourNodeids.length == 80; tikhomirov@36: header = fourNodeids; tikhomirov@532: deltaBase = deltaBaseRev; tikhomirov@169: dataAccess = rawDataAccess; tikhomirov@36: } tikhomirov@169: tikhomirov@358: /** tikhomirov@358: * node field of the group element tikhomirov@358: * @return node revision, never null tikhomirov@358: */ tikhomirov@36: public Nodeid node() { tikhomirov@36: return Nodeid.fromBinary(header, 0); tikhomirov@36: } tikhomirov@169: tikhomirov@358: /** tikhomirov@358: * p1 (parent 1) field of the group element tikhomirov@358: * @return revision of parent 1, never null tikhomirov@358: */ tikhomirov@36: public Nodeid firstParent() { tikhomirov@36: return Nodeid.fromBinary(header, 20); tikhomirov@36: } tikhomirov@169: tikhomirov@358: /** tikhomirov@358: * p2 (parent 2) field of the group element tikhomirov@358: * @return revision of parent 2, never null tikhomirov@358: */ tikhomirov@36: public Nodeid secondParent() { tikhomirov@36: return Nodeid.fromBinary(header, 40); tikhomirov@36: } tikhomirov@169: tikhomirov@358: /** tikhomirov@358: * cs (changeset link) field of the group element tikhomirov@358: * @return changeset revision, never null tikhomirov@358: */ tikhomirov@358: public Nodeid cset() { tikhomirov@36: return Nodeid.fromBinary(header, 60); tikhomirov@36: } tikhomirov@358: tikhomirov@532: /** tikhomirov@532: * Revision this element keeps patches against. For the patches of the very first revision returns {@link Nodeid#NULL}. tikhomirov@532: * @return revision of delta base, never null tikhomirov@532: */ tikhomirov@532: public Nodeid patchBase() { tikhomirov@532: return deltaBase; tikhomirov@532: } tikhomirov@532: tikhomirov@358: public byte[] rawDataByteArray() throws IOException { // XXX IOException or HgInvalidFileException? tikhomirov@358: return rawData().byteArray(); tikhomirov@358: } tikhomirov@358: tikhomirov@358: public byte[] apply(byte[] baseContent) throws IOException { tikhomirov@358: return apply(new ByteArrayDataAccess(baseContent)); tikhomirov@358: } tikhomirov@169: tikhomirov@358: /*package-local*/ DataAccess rawData() { tikhomirov@169: return dataAccess; tikhomirov@169: } tikhomirov@169: tikhomirov@329: /*package-local*/ Patch patch() throws IOException { tikhomirov@169: if (patches == null) { tikhomirov@169: dataAccess.reset(); tikhomirov@329: patches = new Patch(); tikhomirov@329: patches.read(dataAccess); tikhomirov@169: } tikhomirov@169: return patches; tikhomirov@169: } tikhomirov@169: tikhomirov@358: /*package-local*/ byte[] apply(DataAccess baseContent) throws IOException { tikhomirov@329: return patch().apply(baseContent, -1); tikhomirov@169: } tikhomirov@357: tikhomirov@357: public String toString() { tikhomirov@357: int patchCount; tikhomirov@357: try { tikhomirov@357: patchCount = patch().count(); tikhomirov@357: } catch (IOException ex) { tikhomirov@357: ex.printStackTrace(); tikhomirov@357: patchCount = -1; tikhomirov@357: } tikhomirov@357: return String.format("%s %s %s %s; patches:%d\n", node().shortNotation(), firstParent().shortNotation(), secondParent().shortNotation(), cset().shortNotation(), patchCount); tikhomirov@357: } tikhomirov@36: } tikhomirov@36: }