tikhomirov@530: /* tikhomirov@530: * Copyright (c) 2013 TMate Software Ltd tikhomirov@530: * tikhomirov@530: * This program is free software; you can redistribute it and/or modify tikhomirov@530: * it under the terms of the GNU General Public License as published by tikhomirov@530: * the Free Software Foundation; version 2 of the License. tikhomirov@530: * tikhomirov@530: * This program is distributed in the hope that it will be useful, tikhomirov@530: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@530: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@530: * GNU General Public License for more details. tikhomirov@530: * tikhomirov@530: * For information on how to redistribute this software under tikhomirov@530: * the terms of a license other than GNU General Public License tikhomirov@530: * contact TMate Software at support@hg4j.com tikhomirov@530: */ tikhomirov@530: package org.tmatesoft.hg.internal; tikhomirov@530: tikhomirov@530: import static org.tmatesoft.hg.internal.Internals.REVLOGV1_RECORD_SIZE; tikhomirov@534: import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION; tikhomirov@530: tikhomirov@530: import java.io.IOException; tikhomirov@530: import java.nio.ByteBuffer; tikhomirov@530: tikhomirov@530: import org.tmatesoft.hg.core.Nodeid; tikhomirov@534: import org.tmatesoft.hg.core.SessionContext; tikhomirov@534: import org.tmatesoft.hg.repo.HgInvalidControlFileException; tikhomirov@534: import org.tmatesoft.hg.repo.HgInvalidStateException; tikhomirov@530: tikhomirov@530: /** tikhomirov@530: * tikhomirov@534: * TODO separate operation to check if index is too big and split into index+data tikhomirov@532: * tikhomirov@530: * @author Artem Tikhomirov tikhomirov@530: * @author TMate Software Ltd. tikhomirov@530: */ tikhomirov@530: public class RevlogStreamWriter { tikhomirov@530: tikhomirov@530: tikhomirov@534: /*XXX public because HgCloneCommand uses it*/ tikhomirov@534: public static class HeaderWriter implements DataSerializer.DataSource { tikhomirov@530: private final ByteBuffer header; tikhomirov@530: private final boolean isInline; tikhomirov@530: private long offset; tikhomirov@530: private int length, compressedLength; tikhomirov@530: private int baseRev, linkRev, p1, p2; tikhomirov@534: private byte[] nodeid; tikhomirov@530: tikhomirov@530: public HeaderWriter(boolean inline) { tikhomirov@530: isInline = inline; tikhomirov@530: header = ByteBuffer.allocate(REVLOGV1_RECORD_SIZE); tikhomirov@530: } tikhomirov@530: tikhomirov@530: public HeaderWriter offset(long offset) { tikhomirov@530: this.offset = offset; tikhomirov@530: return this; tikhomirov@530: } tikhomirov@530: tikhomirov@532: public int baseRevision() { tikhomirov@532: return baseRev; tikhomirov@532: } tikhomirov@532: tikhomirov@530: public HeaderWriter baseRevision(int baseRevision) { tikhomirov@530: this.baseRev = baseRevision; tikhomirov@530: return this; tikhomirov@530: } tikhomirov@530: tikhomirov@530: public HeaderWriter length(int len, int compressedLen) { tikhomirov@530: this.length = len; tikhomirov@530: this.compressedLength = compressedLen; tikhomirov@530: return this; tikhomirov@530: } tikhomirov@530: tikhomirov@530: public HeaderWriter parents(int parent1, int parent2) { tikhomirov@530: p1 = parent1; tikhomirov@530: p2 = parent2; tikhomirov@530: return this; tikhomirov@530: } tikhomirov@530: tikhomirov@530: public HeaderWriter linkRevision(int linkRevision) { tikhomirov@534: linkRev = linkRevision; tikhomirov@530: return this; tikhomirov@530: } tikhomirov@530: tikhomirov@530: public HeaderWriter nodeid(Nodeid n) { tikhomirov@534: nodeid = n.toByteArray(); tikhomirov@530: return this; tikhomirov@530: } tikhomirov@534: tikhomirov@534: public HeaderWriter nodeid(byte[] nodeidBytes) { tikhomirov@534: nodeid = nodeidBytes; tikhomirov@534: return this; tikhomirov@534: } tikhomirov@534: tikhomirov@534: public void serialize(DataSerializer out) throws IOException { tikhomirov@530: header.clear(); tikhomirov@530: if (offset == 0) { tikhomirov@530: int version = 1 /* RevlogNG */; tikhomirov@530: if (isInline) { tikhomirov@530: final int INLINEDATA = 1 << 16; // FIXME extract constant tikhomirov@530: version |= INLINEDATA; tikhomirov@530: } tikhomirov@530: header.putInt(version); tikhomirov@530: header.putInt(0); tikhomirov@530: } else { tikhomirov@530: header.putLong(offset << 16); tikhomirov@530: } tikhomirov@530: header.putInt(compressedLength); tikhomirov@530: header.putInt(length); tikhomirov@530: header.putInt(baseRev); tikhomirov@530: header.putInt(linkRev); tikhomirov@530: header.putInt(p1); tikhomirov@530: header.putInt(p2); tikhomirov@534: header.put(nodeid); tikhomirov@530: // assume 12 bytes left are zeros tikhomirov@534: out.write(header.array(), 0, header.capacity()); tikhomirov@530: tikhomirov@530: // regardless whether it's inline or separate data, tikhomirov@530: // offset field always represent cumulative compressedLength tikhomirov@530: // (while offset in the index file with inline==true differs by n*sizeof(header), where n is entry's position in the file) tikhomirov@530: offset += compressedLength; tikhomirov@530: } tikhomirov@534: tikhomirov@534: public int serializeLength() { tikhomirov@534: return header.capacity(); tikhomirov@534: } tikhomirov@534: } tikhomirov@534: tikhomirov@534: private final DigestHelper dh = new DigestHelper(); tikhomirov@534: private final RevlogCompressor revlogDataZip; tikhomirov@534: tikhomirov@534: tikhomirov@534: public RevlogStreamWriter(SessionContext ctx, RevlogStream stream) { tikhomirov@534: revlogDataZip = new RevlogCompressor(ctx); tikhomirov@530: } tikhomirov@530: tikhomirov@534: private int lastEntryBase, lastEntryIndex; tikhomirov@534: private byte[] lastEntryContent; tikhomirov@534: private Nodeid lastEntryRevision; tikhomirov@534: private IntMap revisionCache = new IntMap(32); tikhomirov@533: tikhomirov@533: public void addRevision(byte[] content, int linkRevision, int p1, int p2) { tikhomirov@534: int revCount = revlogStream.revisionCount(); tikhomirov@534: lastEntryIndex = revCount == 0 ? NO_REVISION : revCount - 1; tikhomirov@534: populateLastEntry(); tikhomirov@534: // tikhomirov@533: PatchGenerator pg = new PatchGenerator(); tikhomirov@534: Patch patch = pg.delta(lastEntryContent, content); tikhomirov@534: int patchSerializedLength = patch.serializedLength(); tikhomirov@534: tikhomirov@534: final boolean writeComplete = preferCompleteOverPatch(patchSerializedLength, content.length); tikhomirov@534: DataSerializer.DataSource dataSource = writeComplete ? new DataSerializer.ByteArrayDataSource(content) : patch.new PatchDataSource(); tikhomirov@534: revlogDataZip.reset(dataSource); tikhomirov@534: final int compressedLen; tikhomirov@534: final boolean useUncompressedData = preferCompressedOverComplete(revlogDataZip.getCompressedLength(), dataSource.serializeLength()); tikhomirov@534: if (useUncompressedData) { tikhomirov@534: // compression wasn't too effective, tikhomirov@534: compressedLen = dataSource.serializeLength() + 1 /*1 byte for 'u' - uncompressed prefix byte*/; tikhomirov@534: } else { tikhomirov@534: compressedLen= revlogDataZip.getCompressedLength(); tikhomirov@534: } tikhomirov@534: // tikhomirov@534: Nodeid p1Rev = revision(p1); tikhomirov@534: Nodeid p2Rev = revision(p2); tikhomirov@534: byte[] revisionNodeidBytes = dh.sha1(p1Rev, p2Rev, content).asBinary(); tikhomirov@534: // tikhomirov@534: tikhomirov@534: DataSerializer indexFile, dataFile, activeFile; tikhomirov@534: indexFile = dataFile = activeFile = null; tikhomirov@534: try { tikhomirov@534: // tikhomirov@534: activeFile = indexFile = revlogStream.getIndexStreamWriter(); tikhomirov@534: final boolean isInlineData = revlogStream.isInlineData(); tikhomirov@534: HeaderWriter revlogHeader = new HeaderWriter(isInlineData); tikhomirov@534: revlogHeader.length(content.length, compressedLen); tikhomirov@534: revlogHeader.nodeid(revisionNodeidBytes); tikhomirov@534: revlogHeader.linkRevision(linkRevision); tikhomirov@534: revlogHeader.parents(p1, p2); tikhomirov@534: revlogHeader.baseRevision(writeComplete ? lastEntryIndex+1 : lastEntryBase); tikhomirov@534: // tikhomirov@534: revlogHeader.serialize(indexFile); tikhomirov@534: tikhomirov@534: if (isInlineData) { tikhomirov@534: dataFile = indexFile; tikhomirov@534: } else { tikhomirov@534: dataFile = revlogStream.getDataStreamWriter(); tikhomirov@534: } tikhomirov@534: activeFile = dataFile; tikhomirov@534: if (useUncompressedData) { tikhomirov@534: dataFile.writeByte((byte) 'u'); tikhomirov@534: dataSource.serialize(dataFile); tikhomirov@534: } else { tikhomirov@534: int actualCompressedLenWritten = revlogDataZip.writeCompressedData(dataFile); tikhomirov@534: if (actualCompressedLenWritten != compressedLen) { tikhomirov@534: throw new HgInvalidStateException(String.format("Expected %d bytes of compressed data, but actually wrote %d in %s", compressedLen, actualCompressedLenWritten, revlogStream.getDataFileName())); tikhomirov@534: } tikhomirov@534: } tikhomirov@534: tikhomirov@534: lastEntryContent = content; tikhomirov@534: lastEntryBase = revlogHeader.baseRevision(); tikhomirov@534: lastEntryIndex++; tikhomirov@534: lastEntryRevision = Nodeid.fromBinary(revisionNodeidBytes, 0); tikhomirov@534: revisionCache.put(lastEntryIndex, lastEntryRevision); tikhomirov@534: } catch (IOException ex) { tikhomirov@534: String m = String.format("Failed to write revision %d", lastEntryIndex+1, null); tikhomirov@534: HgInvalidControlFileException t = new HgInvalidControlFileException(m, ex, null); tikhomirov@534: if (activeFile == dataFile) { tikhomirov@534: throw revlogStream.initWithDataFile(t); tikhomirov@534: } else { tikhomirov@534: throw revlogStream.initWithIndexFile(t); tikhomirov@534: } tikhomirov@534: } finally { tikhomirov@534: indexFile.done(); tikhomirov@534: if (dataFile != null && dataFile != indexFile) { tikhomirov@534: dataFile.done(); tikhomirov@534: } tikhomirov@534: } tikhomirov@533: } tikhomirov@533: tikhomirov@534: private RevlogStream revlogStream; tikhomirov@534: private Nodeid revision(int revisionIndex) { tikhomirov@534: if (revisionIndex == NO_REVISION) { tikhomirov@534: return Nodeid.NULL; tikhomirov@534: } tikhomirov@534: Nodeid n = revisionCache.get(revisionIndex); tikhomirov@534: if (n == null) { tikhomirov@534: n = Nodeid.fromBinary(revlogStream.nodeid(revisionIndex), 0); tikhomirov@534: revisionCache.put(revisionIndex, n); tikhomirov@534: } tikhomirov@534: return n; tikhomirov@534: } tikhomirov@534: tikhomirov@534: private void populateLastEntry() throws HgInvalidControlFileException { tikhomirov@534: if (lastEntryIndex != NO_REVISION && lastEntryContent == null) { tikhomirov@534: assert lastEntryIndex >= 0; tikhomirov@534: final IOException[] failure = new IOException[1]; tikhomirov@534: revlogStream.iterate(lastEntryIndex, lastEntryIndex, true, new RevlogStream.Inspector() { tikhomirov@534: tikhomirov@534: public void next(int revisionIndex, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) { tikhomirov@534: try { tikhomirov@534: lastEntryBase = baseRevision; tikhomirov@534: lastEntryRevision = Nodeid.fromBinary(nodeid, 0); tikhomirov@534: lastEntryContent = data.byteArray(); tikhomirov@534: } catch (IOException ex) { tikhomirov@534: failure[0] = ex; tikhomirov@534: } tikhomirov@534: } tikhomirov@534: }); tikhomirov@534: if (failure[0] != null) { tikhomirov@534: String m = String.format("Failed to get content of most recent revision %d", lastEntryIndex); tikhomirov@534: throw revlogStream.initWithDataFile(new HgInvalidControlFileException(m, failure[0], null)); tikhomirov@534: } tikhomirov@534: } tikhomirov@534: } tikhomirov@534: tikhomirov@534: public static boolean preferCompleteOverPatch(int patchLength, int fullContentLength) { tikhomirov@534: return !decideWorthEffort(patchLength, fullContentLength); tikhomirov@534: } tikhomirov@534: tikhomirov@534: public static boolean preferCompressedOverComplete(int compressedLen, int fullContentLength) { tikhomirov@534: if (compressedLen <= 0) { // just in case, meaningless otherwise tikhomirov@534: return false; tikhomirov@534: } tikhomirov@534: return decideWorthEffort(compressedLen, fullContentLength); tikhomirov@534: } tikhomirov@534: tikhomirov@534: // true if length obtained with effort is worth it tikhomirov@534: private static boolean decideWorthEffort(int lengthWithExtraEffort, int lengthWithoutEffort) { tikhomirov@534: return lengthWithExtraEffort < (/* 3/4 of original */lengthWithoutEffort - (lengthWithoutEffort >>> 2)); tikhomirov@530: } tikhomirov@530: }