# HG changeset patch # User Artem Tikhomirov # Date 1371650664 -7200 # Node ID 1deea2f332183c947937f6df988c2c6417efc217 # Parent a8ce405da1f5c259ba0b6bd6b177502f3cedc304 Push: phase1 - prepare bundle with changes diff -r a8ce405da1f5 -r 1deea2f33218 src/org/tmatesoft/hg/internal/BundleGenerator.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/BundleGenerator.java Wed Jun 19 16:04:24 2013 +0200 @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2013 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.internal; + +import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION; +import static org.tmatesoft.hg.repo.HgRepository.TIP; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.tmatesoft.hg.console.Bundle; +import org.tmatesoft.hg.core.HgIOException; +import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.Patch.PatchDataSource; +import org.tmatesoft.hg.repo.HgBundle; +import org.tmatesoft.hg.repo.HgChangelog; +import org.tmatesoft.hg.repo.HgChangelog.RawChangeset; +import org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgInternals; +import org.tmatesoft.hg.repo.HgInvalidControlFileException; +import org.tmatesoft.hg.repo.HgLookup; +import org.tmatesoft.hg.repo.HgManifest; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgRuntimeException; + +/** + * @see http://mercurial.selenic.com/wiki/BundleFormat + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class BundleGenerator { + + private final Internals repo; + + public BundleGenerator(Internals hgRepo) { + repo = hgRepo; + } + + public File create(List changesets) throws HgIOException, IOException { + final HgChangelog clog = repo.getRepo().getChangelog(); + final HgManifest manifest = repo.getRepo().getManifest(); + IntVector clogRevsVector = new IntVector(changesets.size(), 0); + for (Nodeid n : changesets) { + clogRevsVector.add(clog.getRevisionIndex(n)); + } + clogRevsVector.sort(true); + final int[] clogRevs = clogRevsVector.toArray(); + System.out.printf("Changelog: %s\n", Arrays.toString(clogRevs)); + final IntMap clogMap = new IntMap(changesets.size()); + final IntVector manifestRevs = new IntVector(changesets.size(), 0); + final List files = new ArrayList(); + clog.range(new HgChangelog.Inspector() { + public void next(int revisionIndex, Nodeid nodeid, RawChangeset cset) throws HgRuntimeException { + clogMap.put(revisionIndex, nodeid); + manifestRevs.add(manifest.getRevisionIndex(cset.manifest())); + for (String f : cset.files()) { + HgDataFile df = repo.getRepo().getFileNode(f); + if (!files.contains(df)) { + files.add(df); + } + } + } + }, clogRevs); + manifestRevs.sort(true); + System.out.printf("Manifest: %s\n", Arrays.toString(manifestRevs.toArray(true))); + /////////////// + for (HgDataFile df : sortedByName(files)) { + RevlogStream s = repo.getImplAccess().getStream(df); + final IntVector fileRevs = new IntVector(); + s.iterate(0, TIP, false, new RevlogStream.Inspector() { + + public void next(int revisionIndex, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) throws HgRuntimeException { + if (Arrays.binarySearch(clogRevs, linkRevision) >= 0) { + fileRevs.add(revisionIndex); + } + } + }); + fileRevs.sort(true); + System.out.printf("%s: %s\n", df.getPath(), Arrays.toString(fileRevs.toArray(true))); + } + if (Boolean.FALSE.booleanValue()) { + return null; + } + /////////////// + // + final File bundleFile = File.createTempFile("hg4j-", "bundle"); + final OutputStreamSerializer outRaw = new OutputStreamSerializer(new FileOutputStream(bundleFile)); + outRaw.write("HG10UN".getBytes(), 0, 6); + // + RevlogStream clogStream = repo.getImplAccess().getChangelogStream(); + new ChunkGenerator(outRaw, clogMap).iterate(clogStream, clogRevs); + outRaw.writeInt(0); // null chunk for changelog group + // + RevlogStream manifestStream = repo.getImplAccess().getManifestStream(); + new ChunkGenerator(outRaw, clogMap).iterate(manifestStream, manifestRevs.toArray(true)); + outRaw.writeInt(0); // null chunk for manifest group + // + for (HgDataFile df : sortedByName(files)) { + RevlogStream s = repo.getImplAccess().getStream(df); + final IntVector fileRevs = new IntVector(); + s.iterate(0, TIP, false, new RevlogStream.Inspector() { + + public void next(int revisionIndex, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) throws HgRuntimeException { + if (Arrays.binarySearch(clogRevs, linkRevision) >= 0) { + fileRevs.add(revisionIndex); + } + } + }); + fileRevs.sort(true); + if (!fileRevs.isEmpty()) { + // although BundleFormat page says "filename length, filename" for a file, + // in fact there's a sort of 'filename chunk', i.e. filename length field includes + // not only length of filename, but also length of the field itseld, i.e. filename.length+sizeof(int) + byte[] fnameBytes = df.getPath().toString().getBytes(); // FIXME check encoding in native hg (and fix accordingly in HgBundle) + outRaw.writeInt(fnameBytes.length + 4); + outRaw.writeByte(fnameBytes); + new ChunkGenerator(outRaw, clogMap).iterate(s, fileRevs.toArray(true)); + outRaw.writeInt(0); // null chunk for file group + } + } + outRaw.done(); + //return new HgBundle(repo.getSessionContext(), repo.getDataAccess(), bundleFile); + return bundleFile; + } + + private static Collection sortedByName(List files) { + Collections.sort(files, new Comparator() { + + public int compare(HgDataFile o1, HgDataFile o2) { + return o1.getPath().compareTo(o2.getPath()); + } + }); + return files; + } + + + public static void main(String[] args) throws Exception { + final HgLookup hgLookup = new HgLookup(); + HgRepository hgRepo = hgLookup.detectFromWorkingDir(); + BundleGenerator bg = new BundleGenerator(HgInternals.getImplementationRepo(hgRepo)); + ArrayList l = new ArrayList(); + l.add(Nodeid.fromAscii("9ef1fab9f5e3d51d70941121dc27410e28069c2d")); // 640 + l.add(Nodeid.fromAscii("2f33f102a8fa59274a27ebbe1c2903cecac6c5d5")); // 639 + l.add(Nodeid.fromAscii("d074971287478f69ab0a64176ce2284d8c1e91c3")); // 638 + File bundleFile = bg.create(l); + HgBundle b = hgLookup.loadBundle(bundleFile); +// Bundle.dump(b); // FIXME dependency from dependant code + } + + private static class ChunkGenerator implements RevlogStream.Inspector { + + private final DataSerializer ds; + private final IntMap parentMap; + private final IntMap clogMap; + private byte[] prevContent; + private int startParent; + + public ChunkGenerator(DataSerializer dataSerializer, IntMap clogNodeidMap) { + ds = dataSerializer; + parentMap = new IntMap(clogNodeidMap.size());; + clogMap = clogNodeidMap; + } + + public void iterate(RevlogStream s, int[] revisions) throws HgRuntimeException { + int[] p = s.parents(revisions[0], new int[2]); + startParent = p[0]; + int[] revs2read; + if (startParent == NO_REVISION) { + revs2read = revisions; + prevContent = new byte[0]; + } else { + revs2read = new int[revisions.length + 1]; + revs2read[0] = startParent; + System.arraycopy(revisions, 0, revs2read, 1, revisions.length); + } + s.iterate(revs2read, true, this); + } + + public void next(int revisionIndex, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) throws HgRuntimeException { + try { + parentMap.put(revisionIndex, Nodeid.fromBinary(nodeid, 0)); + byte[] nextContent = data.byteArray(); + data.done(); + if (revisionIndex == startParent) { + prevContent = nextContent; + return; + } + Patch p = GeneratePatchInspector.delta(prevContent, nextContent); + prevContent = nextContent; + nextContent = null; + PatchDataSource pds = p.new PatchDataSource(); + int len = pds.serializeLength() + 84; + ds.writeInt(len); + ds.write(nodeid, 0, Nodeid.SIZE); + // TODO assert parents match those in previous group elements + if (parent1Revision != NO_REVISION) { + ds.writeByte(parentMap.get(parent1Revision).toByteArray()); + } else { + ds.writeByte(Nodeid.NULL.toByteArray()); + } + if (parent2Revision != NO_REVISION) { + ds.writeByte(parentMap.get(parent2Revision).toByteArray()); + } else { + ds.writeByte(Nodeid.NULL.toByteArray()); + } + ds.writeByte(clogMap.get(linkRevision).toByteArray()); + pds.serialize(ds); + } catch (IOException ex) { + // XXX odd to have object with IOException to use where no checked exception is allowed + throw new HgInvalidControlFileException(ex.getMessage(), ex, null); + } catch (HgIOException ex) { + throw new HgInvalidControlFileException(ex, true); // XXX any way to refactor ChunkGenerator not to get checked exception here? + } + } + } + + private static class OutputStreamSerializer extends DataSerializer { + private final OutputStream out; + public OutputStreamSerializer(OutputStream outputStream) { + out = outputStream; + } + + @Override + public void write(byte[] data, int offset, int length) throws HgIOException { + try { + out.write(data, offset, length); + } catch (IOException ex) { + throw new HgIOException(ex.getMessage(), ex, null); + } + } + + @Override + public void done() throws HgIOException { + try { + out.close(); + super.done(); + } catch (IOException ex) { + throw new HgIOException(ex.getMessage(), ex, null); + } + } + } +} diff -r a8ce405da1f5 -r 1deea2f33218 src/org/tmatesoft/hg/internal/RevlogStream.java --- a/src/org/tmatesoft/hg/internal/RevlogStream.java Wed Jun 19 16:03:11 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/RevlogStream.java Wed Jun 19 16:04:24 2013 +0200 @@ -17,6 +17,7 @@ package org.tmatesoft.hg.internal; import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; +import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION; import static org.tmatesoft.hg.repo.HgRepository.TIP; import static org.tmatesoft.hg.internal.Internals.REVLOGV1_RECORD_SIZE; @@ -236,6 +237,34 @@ return getBaseRevision(revisionIndex); } + /** + * Read indexes of parent revisions + * @param revisionIndex index of child revision + * @param parents array to hold return value, length >= 2 + * @return value of parents parameter for convenience + * @throws HgInvalidControlFileException if attempt to read index file failed + * @throws HgInvalidRevisionException if revisionIndex argument doesn't represent a valid record in the revlog + */ + public int[] parents(int revisionIndex, int[] parents) throws HgInvalidControlFileException, HgInvalidRevisionException { + assert parents.length > 1; + revisionIndex = checkRevisionIndex(revisionIndex); + DataAccess daIndex = getIndexStream(true); + try { + int recordOffset = getIndexOffsetInt(revisionIndex); + daIndex.seek(recordOffset + 24); + int p1 = daIndex.readInt(); + int p2 = daIndex.readInt(); + // although NO_REVISION == -1, it doesn't hurt to ensure this + parents[0] = p1 == -1 ? NO_REVISION : p1; + parents[1] = p2 == -1 ? NO_REVISION : p2; + return parents; + } catch (IOException ex) { + throw new HgInvalidControlFileException("Parents lookup failed", ex, indexFile).setRevisionIndex(revisionIndex); + } finally { + daIndex.done(); + } + } + // Perhaps, RevlogStream should be limited to use of plain int revisions for access, // while Nodeids should be kept on the level up, in Revlog. Guess, Revlog better keep // map of nodeids, and once this comes true, we may get rid of this method.