Mercurial > hg4j
diff src/org/tmatesoft/hg/core/HgCloneCommand.java @ 186:44a34baabea0
Clone refactored into a command. HgBundle needs means to control its lifecycle, to be deleted when no longer needed
author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
---|---|
date | Thu, 14 Apr 2011 00:47:04 +0200 |
parents | |
children | 9b99d27aeddc |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/core/HgCloneCommand.java Thu Apr 14 00:47:04 2011 +0200 @@ -0,0 +1,327 @@ +/* + * Copyright (c) 2011 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.core; + +import static org.tmatesoft.hg.core.Nodeid.NULL; +import static org.tmatesoft.hg.internal.RequiresFile.*; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.TreeMap; +import java.util.zip.DeflaterOutputStream; + +import org.tmatesoft.hg.internal.ByteArrayDataAccess; +import org.tmatesoft.hg.internal.DataAccess; +import org.tmatesoft.hg.internal.DigestHelper; +import org.tmatesoft.hg.internal.Internals; +import org.tmatesoft.hg.repo.HgBundle; +import org.tmatesoft.hg.repo.HgBundle.GroupElement; +import org.tmatesoft.hg.repo.HgLookup; +import org.tmatesoft.hg.repo.HgRemoteRepository; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.util.PathRewrite; + +/** + * WORK IN PROGRESS, DO NOT USE + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class HgCloneCommand { + + private File destination; + private HgRemoteRepository srcRepo; + + public HgCloneCommand() { + } + + public HgCloneCommand destination(File folder) { + destination = folder; + return this; + } + + public HgCloneCommand source(HgRemoteRepository hgRemote) { + srcRepo = hgRemote; + return this; + } + + public HgRepository execute() throws HgException, CancelledException { + if (destination == null) { + throw new HgBadArgumentException("Destination not set", null); + } + if (srcRepo == null || srcRepo.isInvalid()) { + throw new HgBadArgumentException("Bad source repository", null); + } + if (destination.exists()) { + if (!destination.isDirectory()) { + throw new HgBadArgumentException(String.format("%s is not a directory", destination), null); + } else if (destination.list().length > 0) { + throw new HgBadArgumentException(String.format("% shall be empty", destination), null); + } + } else { + destination.mkdirs(); + } + // if cloning remote repo, which can stream and no revision is specified - + // can use 'stream_out' wireproto + // + // pull all changes from the very beginning + // XXX consult getContext() if by any chance has a bundle ready, if not, then read and register + HgBundle completeChanges = srcRepo.getChanges(Collections.singletonList(NULL)); + WriteDownMate mate = new WriteDownMate(destination); + try { + // instantiate new repo in the destdir + mate.initEmptyRepository(); + // pull changes + completeChanges.inspectAll(mate); + mate.complete(); + } catch (IOException ex) { + throw new HgException(ex); + } finally { + completeChanges.unlink(); + } + return new HgLookup().detect(destination); + } + + + // 1. process changelog, memorize nodeids to index + // 2. process manifest, using map from step 3, collect manifest nodeids + // 3. process every file, using map from 3, and consult set from step 4 to ensure repo is correct + private static class WriteDownMate implements HgBundle.Inspector { + private final File hgDir; + private FileOutputStream indexFile; + private final PathRewrite storagePathHelper; + + private final TreeMap<Nodeid, Integer> changelogIndexes = new TreeMap<Nodeid, Integer>(); + private boolean collectChangelogIndexes = false; + + private int base = -1; + private long offset = 0; + private DataAccess prevRevContent; + private final DigestHelper dh = new DigestHelper(); + private final ArrayList<Nodeid> revisionSequence = new ArrayList<Nodeid>(); // last visited nodes first + + private final LinkedList<String> fncacheFiles = new LinkedList<String>(); + + public WriteDownMate(File destDir) { + hgDir = new File(destDir, ".hg"); + Internals i = new Internals(); + i.setStorageConfig(1, STORE | FNCACHE | DOTENCODE); + storagePathHelper = i.buildDataFilesHelper(); + } + + public void initEmptyRepository() throws IOException { + hgDir.mkdir(); + FileOutputStream requiresFile = new FileOutputStream(new File(hgDir, "requires")); + requiresFile.write("revlogv1\nstore\nfncache\ndotencode\n".getBytes()); + requiresFile.close(); + new File(hgDir, "store").mkdir(); // with that, hg verify says ok. + } + + public void complete() throws IOException { + FileOutputStream fncacheFile = new FileOutputStream(new File(hgDir, "store/fncache")); + for (String s : fncacheFiles) { + fncacheFile.write(s.getBytes()); + fncacheFile.write(0x0A); // http://mercurial.selenic.com/wiki/fncacheRepoFormat + } + fncacheFile.close(); + } + + public void changelogStart() { + try { + base = -1; + offset = 0; + revisionSequence.clear(); + indexFile = new FileOutputStream(new File(hgDir, "store/00changelog.i")); + collectChangelogIndexes = true; + } catch (IOException ex) { + throw new HgBadStateException(ex); + } + } + + public void changelogEnd() { + try { + if (prevRevContent != null) { + prevRevContent.done(); + prevRevContent = null; + } + collectChangelogIndexes = false; + indexFile.close(); + indexFile = null; + } catch (IOException ex) { + throw new HgBadStateException(ex); + } + } + + public void manifestStart() { + try { + base = -1; + offset = 0; + revisionSequence.clear(); + indexFile = new FileOutputStream(new File(hgDir, "store/00manifest.i")); + } catch (IOException ex) { + throw new HgBadStateException(ex); + } + } + + public void manifestEnd() { + try { + if (prevRevContent != null) { + prevRevContent.done(); + prevRevContent = null; + } + indexFile.close(); + indexFile = null; + } catch (IOException ex) { + throw new HgBadStateException(ex); + } + } + + public void fileStart(String name) { + try { + base = -1; + offset = 0; + revisionSequence.clear(); + fncacheFiles.add("data/" + name + ".i"); // FIXME this is pure guess, + // need to investigate more how filenames are kept in fncache + File file = new File(hgDir, storagePathHelper.rewrite(name)); + file.getParentFile().mkdirs(); + indexFile = new FileOutputStream(file); + } catch (IOException ex) { + throw new HgBadStateException(ex); + } + } + + public void fileEnd(String name) { + try { + if (prevRevContent != null) { + prevRevContent.done(); + prevRevContent = null; + } + indexFile.close(); + indexFile = null; + } catch (IOException ex) { + throw new HgBadStateException(ex); + } + } + + private int knownRevision(Nodeid p) { + if (NULL.equals(p)) { + return -1; + } else { + for (int i = revisionSequence.size() - 1; i >= 0; i--) { + if (revisionSequence.get(i).equals(p)) { + return i; + } + } + } + throw new HgBadStateException(String.format("Can't find index of %s", p.shortNotation())); + } + + public boolean element(GroupElement ge) { + try { + assert indexFile != null; + boolean writeComplete = false; + Nodeid p1 = ge.firstParent(); + Nodeid p2 = ge.secondParent(); + if (NULL.equals(p1) && NULL.equals(p2) /* or forced flag, does REVIDX_PUNCHED_FLAG indicate that? */) { + prevRevContent = new ByteArrayDataAccess(new byte[0]); + writeComplete = true; + } + byte[] content = ge.apply(prevRevContent); + byte[] calculated = dh.sha1(p1, p2, content).asBinary(); + final Nodeid node = ge.node(); + if (!node.equalsTo(calculated)) { + throw new HgBadStateException("Checksum failed"); + } + final int link; + if (collectChangelogIndexes) { + changelogIndexes.put(node, revisionSequence.size()); + link = revisionSequence.size(); + } else { + Integer csRev = changelogIndexes.get(ge.cset()); + if (csRev == null) { + throw new HgBadStateException(String.format("Changelog doesn't contain revision %s", ge.cset().shortNotation())); + } + link = csRev.intValue(); + } + final int p1Rev = knownRevision(p1), p2Rev = knownRevision(p2); + DataAccess patchContent = ge.rawData(); + writeComplete = writeComplete || patchContent.length() >= (/* 3/4 of actual */content.length - (content.length >>> 2)); + if (writeComplete) { + base = revisionSequence.size(); + } + final byte[] sourceData = writeComplete ? content : patchContent.byteArray(); + final byte[] data; + ByteArrayOutputStream bos = new ByteArrayOutputStream(content.length); + DeflaterOutputStream dos = new DeflaterOutputStream(bos); + dos.write(sourceData); + dos.close(); + final byte[] compressedData = bos.toByteArray(); + dos = null; + bos = null; + final Byte dataPrefix; + if (compressedData.length >= (sourceData.length - (sourceData.length >>> 2))) { + // compression wasn't too effective, + data = sourceData; + dataPrefix = 'u'; + } else { + data = compressedData; + dataPrefix = null; + } + + ByteBuffer header = ByteBuffer.allocate(64 /* REVLOGV1_RECORD_SIZE */); + if (offset == 0) { + final int INLINEDATA = 1 << 16; + header.putInt(1 /* RevlogNG */ | INLINEDATA); + header.putInt(0); + } else { + header.putLong(offset << 16); + } + final int compressedLen = data.length + (dataPrefix == null ? 0 : 1); + header.putInt(compressedLen); + header.putInt(content.length); + header.putInt(base); + header.putInt(link); + header.putInt(p1Rev); + header.putInt(p2Rev); + header.put(node.toByteArray()); + // assume 12 bytes left are zeros + indexFile.write(header.array()); + if (dataPrefix != null) { + indexFile.write(dataPrefix.byteValue()); + } + indexFile.write(data); + // + offset += compressedLen; + revisionSequence.add(node); + prevRevContent.done(); + prevRevContent = new ByteArrayDataAccess(content); + } catch (IOException ex) { + throw new HgBadStateException(ex); + } + return true; + } + } + +}