kitaev@213: /* kitaev@213: * Copyright (c) 2010-2011 TMate Software Ltd kitaev@213: * kitaev@213: * This program is free software; you can redistribute it and/or modify kitaev@213: * it under the terms of the GNU General Public License as published by kitaev@213: * the Free Software Foundation; version 2 of the License. kitaev@213: * kitaev@213: * This program is distributed in the hope that it will be useful, kitaev@213: * but WITHOUT ANY WARRANTY; without even the implied warranty of kitaev@213: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the kitaev@213: * GNU General Public License for more details. kitaev@213: * kitaev@213: * For information on how to redistribute this software under kitaev@213: * the terms of a license other than GNU General Public License kitaev@213: * contact TMate Software at support@hg4j.com kitaev@213: */ kitaev@213: package org.tmatesoft.hg.repo; kitaev@213: kitaev@213: import static org.tmatesoft.hg.repo.HgInternals.wrongLocalRevision; kitaev@213: import static org.tmatesoft.hg.repo.HgRepository.*; kitaev@213: kitaev@213: import java.io.ByteArrayOutputStream; kitaev@213: import java.io.IOException; kitaev@213: import java.nio.ByteBuffer; kitaev@213: import java.util.ArrayList; kitaev@213: import java.util.Collection; kitaev@213: import java.util.TreeMap; kitaev@213: kitaev@213: import org.tmatesoft.hg.core.HgDataStreamException; kitaev@213: import org.tmatesoft.hg.core.HgException; kitaev@213: import org.tmatesoft.hg.core.Nodeid; kitaev@213: import org.tmatesoft.hg.internal.DataAccess; kitaev@213: import org.tmatesoft.hg.internal.FilterByteChannel; kitaev@213: import org.tmatesoft.hg.internal.RevlogStream; kitaev@213: import org.tmatesoft.hg.util.ByteChannel; kitaev@213: import org.tmatesoft.hg.util.CancelledException; kitaev@213: import org.tmatesoft.hg.util.Path; kitaev@213: kitaev@213: kitaev@213: kitaev@213: /** kitaev@213: * ? name:HgFileNode? kitaev@213: * kitaev@213: * @author Artem Tikhomirov kitaev@213: * @author TMate Software Ltd. kitaev@213: */ kitaev@213: public class HgDataFile extends Revlog { kitaev@213: kitaev@213: // absolute from repo root? kitaev@213: // slashes, unix-style? kitaev@213: // repo location agnostic, just to give info to user, not to access real storage kitaev@213: private final Path path; kitaev@213: private Metadata metadata; // get initialized on first access to file content. kitaev@213: kitaev@213: /*package-local*/HgDataFile(HgRepository hgRepo, Path filePath, RevlogStream content) { kitaev@213: super(hgRepo, content); kitaev@213: path = filePath; kitaev@213: } kitaev@213: kitaev@213: /*package-local*/HgDataFile(HgRepository hgRepo, Path filePath) { kitaev@213: super(hgRepo); kitaev@213: path = filePath; kitaev@213: } kitaev@213: kitaev@213: // exists is not the best name possible. now it means no file with such name was ever known to the repo. kitaev@213: // it might be confused with files existed before but lately removed. kitaev@213: public boolean exists() { kitaev@213: return content != null; // XXX need better impl kitaev@213: } kitaev@213: kitaev@213: // human-readable (i.e. "COPYING", not "store/data/_c_o_p_y_i_n_g.i") kitaev@213: public Path getPath() { kitaev@213: return path; // hgRepo.backresolve(this) -> name? In this case, what about hashed long names? kitaev@213: } kitaev@213: kitaev@213: public int length(Nodeid nodeid) { kitaev@213: return content.dataLength(getLocalRevision(nodeid)); kitaev@213: } kitaev@213: kitaev@213: public void workingCopy(ByteChannel sink) throws IOException, CancelledException { kitaev@213: throw HgRepository.notImplemented(); kitaev@213: } kitaev@213: kitaev@213: // public void content(int revision, ByteChannel sink, boolean applyFilters) throws HgDataStreamException, IOException, CancelledException { kitaev@213: // byte[] content = content(revision); kitaev@213: // final CancelSupport cancelSupport = CancelSupport.Factory.get(sink); kitaev@213: // final ProgressSupport progressSupport = ProgressSupport.Factory.get(sink); kitaev@213: // ByteBuffer buf = ByteBuffer.allocate(512); kitaev@213: // int left = content.length; kitaev@213: // progressSupport.start(left); kitaev@213: // int offset = 0; kitaev@213: // cancelSupport.checkCancelled(); kitaev@213: // ByteChannel _sink = applyFilters ? new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath())) : sink; kitaev@213: // do { kitaev@213: // buf.put(content, offset, Math.min(left, buf.remaining())); kitaev@213: // buf.flip(); kitaev@213: // cancelSupport.checkCancelled(); kitaev@213: // // XXX I may not rely on returned number of bytes but track change in buf position instead. kitaev@213: // int consumed = _sink.write(buf); kitaev@213: // buf.compact(); kitaev@213: // offset += consumed; kitaev@213: // left -= consumed; kitaev@213: // progressSupport.worked(consumed); kitaev@213: // } while (left > 0); kitaev@213: // progressSupport.done(); // XXX shall specify whether #done() is invoked always or only if completed successfully. kitaev@213: // } kitaev@213: kitaev@213: /*XXX not sure distinct method contentWithFilters() is the best way to do, perhaps, callers shall add filters themselves?*/ kitaev@213: public void contentWithFilters(int revision, ByteChannel sink) throws HgDataStreamException, IOException, CancelledException { kitaev@213: content(revision, new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath()))); kitaev@213: } kitaev@213: kitaev@213: // for data files need to check heading of the file content for possible metadata kitaev@213: // @see http://mercurial.selenic.com/wiki/FileFormats#data.2BAC8- kitaev@213: public void content(int revision, ByteChannel sink) throws HgDataStreamException, IOException, CancelledException { kitaev@213: if (revision == TIP) { kitaev@213: revision = getLastRevision(); kitaev@213: } kitaev@213: if (revision == WORKING_COPY) { kitaev@213: workingCopy(sink); kitaev@213: return; kitaev@213: } kitaev@213: if (wrongLocalRevision(revision) || revision == BAD_REVISION) { kitaev@213: throw new IllegalArgumentException(String.valueOf(revision)); kitaev@213: } kitaev@213: if (sink == null) { kitaev@213: throw new IllegalArgumentException(); kitaev@213: } kitaev@213: if (metadata == null) { kitaev@213: metadata = new Metadata(); kitaev@213: } kitaev@213: ContentPipe insp; kitaev@213: if (metadata.none(revision)) { kitaev@213: insp = new ContentPipe(sink, 0); kitaev@213: } else if (metadata.known(revision)) { kitaev@213: insp = new ContentPipe(sink, metadata.dataOffset(revision)); kitaev@213: } else { kitaev@213: // do not know if there's metadata kitaev@213: insp = new MetadataContentPipe(sink, metadata); kitaev@213: } kitaev@213: insp.checkCancelled(); kitaev@213: super.content.iterate(revision, revision, true, insp); kitaev@213: try { kitaev@213: insp.checkFailed(); kitaev@213: } catch (HgDataStreamException ex) { kitaev@213: throw ex; kitaev@213: } catch (HgException ex) { kitaev@213: // shall not happen, unless we changed ContentPipe or its subclass kitaev@213: throw new HgDataStreamException(ex.getClass().getName(), ex); kitaev@213: } kitaev@213: } kitaev@213: kitaev@213: public void history(HgChangelog.Inspector inspector) { kitaev@213: history(0, getLastRevision(), inspector); kitaev@213: } kitaev@213: kitaev@213: public void history(int start, int end, HgChangelog.Inspector inspector) { kitaev@213: if (!exists()) { kitaev@213: throw new IllegalStateException("Can't get history of invalid repository file node"); kitaev@213: } kitaev@213: final int last = getLastRevision(); kitaev@213: if (start < 0 || start > last) { kitaev@213: throw new IllegalArgumentException(); kitaev@213: } kitaev@213: if (end == TIP) { kitaev@213: end = last; kitaev@213: } else if (end < start || end > last) { kitaev@213: throw new IllegalArgumentException(); kitaev@213: } kitaev@213: final int[] commitRevisions = new int[end - start + 1]; kitaev@213: RevlogStream.Inspector insp = new RevlogStream.Inspector() { kitaev@213: int count = 0; kitaev@213: kitaev@213: public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) { kitaev@213: commitRevisions[count++] = linkRevision; kitaev@213: } kitaev@213: }; kitaev@213: content.iterate(start, end, false, insp); kitaev@213: getRepo().getChangelog().range(inspector, commitRevisions); kitaev@213: } kitaev@213: kitaev@213: // for a given local revision of the file, find out local revision in the changelog kitaev@213: public int getChangesetLocalRevision(int revision) { kitaev@213: return content.linkRevision(revision); kitaev@213: } kitaev@213: kitaev@213: public Nodeid getChangesetRevision(Nodeid nid) { kitaev@213: int changelogRevision = getChangesetLocalRevision(getLocalRevision(nid)); kitaev@213: return getRepo().getChangelog().getRevision(changelogRevision); kitaev@213: } kitaev@213: kitaev@213: public boolean isCopy() throws HgDataStreamException { kitaev@213: if (metadata == null || !metadata.checked(0)) { kitaev@213: // content() always initializes metadata. kitaev@213: // FIXME this is expensive way to find out metadata, distinct RevlogStream.Iterator would be better. kitaev@213: // Alternatively, may parameterize MetadataContentPipe to do prepare only. kitaev@213: // For reference, when throwing CancelledException, hg status -A --rev 3:80 takes 70 ms kitaev@213: // however, if we just consume buffer instead (buffer.position(buffer.limit()), same command takes ~320ms kitaev@213: // (compared to command-line counterpart of 190ms) kitaev@213: try { kitaev@213: content(0, new ByteChannel() { // No-op channel kitaev@213: public int write(ByteBuffer buffer) throws IOException, CancelledException { kitaev@213: // pretend we consumed whole buffer kitaev@213: // int rv = buffer.remaining(); kitaev@213: // buffer.position(buffer.limit()); kitaev@213: // return rv; kitaev@213: throw new CancelledException(); kitaev@213: } kitaev@213: }); kitaev@213: } catch (CancelledException ex) { kitaev@213: // it's ok, we did that kitaev@213: } catch (Exception ex) { kitaev@213: throw new HgDataStreamException("Can't initialize metadata", ex); kitaev@213: } kitaev@213: } kitaev@213: if (!metadata.known(0)) { kitaev@213: return false; kitaev@213: } kitaev@213: return metadata.find(0, "copy") != null; kitaev@213: } kitaev@213: kitaev@213: public Path getCopySourceName() throws HgDataStreamException { kitaev@213: if (isCopy()) { kitaev@213: return Path.create(metadata.find(0, "copy")); kitaev@213: } kitaev@213: throw new UnsupportedOperationException(); // XXX REVISIT, think over if Exception is good (clients would check isCopy() anyway, perhaps null is sufficient?) kitaev@213: } kitaev@213: kitaev@213: public Nodeid getCopySourceRevision() throws HgDataStreamException { kitaev@213: if (isCopy()) { kitaev@213: return Nodeid.fromAscii(metadata.find(0, "copyrev")); // XXX reuse/cache Nodeid kitaev@213: } kitaev@213: throw new UnsupportedOperationException(); kitaev@213: } kitaev@213: kitaev@213: @Override kitaev@213: public String toString() { kitaev@213: StringBuilder sb = new StringBuilder(getClass().getSimpleName()); kitaev@213: sb.append('('); kitaev@213: sb.append(getPath()); kitaev@213: sb.append(')'); kitaev@213: return sb.toString(); kitaev@213: } kitaev@213: kitaev@213: private static final class MetadataEntry { kitaev@213: private final String entry; kitaev@213: private final int valueStart; kitaev@213: /*package-local*/MetadataEntry(String key, String value) { kitaev@213: entry = key + value; kitaev@213: valueStart = key.length(); kitaev@213: } kitaev@213: /*package-local*/boolean matchKey(String key) { kitaev@213: return key.length() == valueStart && entry.startsWith(key); kitaev@213: } kitaev@213: // uncomment once/if needed kitaev@213: // public String key() { kitaev@213: // return entry.substring(0, valueStart); kitaev@213: // } kitaev@213: public String value() { kitaev@213: return entry.substring(valueStart); kitaev@213: } kitaev@213: } kitaev@213: kitaev@213: private static class Metadata { kitaev@213: // XXX sparse array needed kitaev@213: private final TreeMap offsets = new TreeMap(); kitaev@213: private final TreeMap entries = new TreeMap(); kitaev@213: kitaev@213: private final Integer NONE = new Integer(-1); // do not duplicate -1 integers at least within single file (don't want statics) kitaev@213: kitaev@213: // true when there's metadata for given revision kitaev@213: boolean known(int revision) { kitaev@213: Integer i = offsets.get(revision); kitaev@213: return i != null && NONE != i; kitaev@213: } kitaev@213: kitaev@213: // true when revision has been checked for metadata presence. kitaev@213: public boolean checked(int revision) { kitaev@213: return offsets.containsKey(revision); kitaev@213: } kitaev@213: kitaev@213: // true when revision has been checked and found not having any metadata kitaev@213: boolean none(int revision) { kitaev@213: Integer i = offsets.get(revision); kitaev@213: return i == NONE; kitaev@213: } kitaev@213: kitaev@213: // mark revision as having no metadata. kitaev@213: void recordNone(int revision) { kitaev@213: Integer i = offsets.get(revision); kitaev@213: if (i == NONE) { kitaev@213: return; // already there kitaev@213: } kitaev@213: if (i != null) { kitaev@213: throw new IllegalStateException(String.format("Trying to override Metadata state for revision %d (known offset: %d)", revision, i)); kitaev@213: } kitaev@213: offsets.put(revision, NONE); kitaev@213: } kitaev@213: kitaev@213: // since this is internal class, callers are supposed to ensure arg correctness (i.e. ask known() before) kitaev@213: int dataOffset(int revision) { kitaev@213: return offsets.get(revision); kitaev@213: } kitaev@213: void add(int revision, int dataOffset, Collection e) { kitaev@213: assert !offsets.containsKey(revision); kitaev@213: offsets.put(revision, dataOffset); kitaev@213: entries.put(revision, e.toArray(new MetadataEntry[e.size()])); kitaev@213: } kitaev@213: String find(int revision, String key) { kitaev@213: for (MetadataEntry me : entries.get(revision)) { kitaev@213: if (me.matchKey(key)) { kitaev@213: return me.value(); kitaev@213: } kitaev@213: } kitaev@213: return null; kitaev@213: } kitaev@213: } kitaev@213: kitaev@213: private static class MetadataContentPipe extends ContentPipe { kitaev@213: kitaev@213: private final Metadata metadata; kitaev@213: kitaev@213: public MetadataContentPipe(ByteChannel sink, Metadata _metadata) { kitaev@213: super(sink, 0); kitaev@213: metadata = _metadata; kitaev@213: } kitaev@213: kitaev@213: @Override kitaev@213: protected void prepare(int revisionNumber, DataAccess da) throws HgException, IOException { kitaev@213: final int daLength = da.length(); kitaev@213: if (daLength < 4 || da.readByte() != 1 || da.readByte() != 10) { kitaev@213: metadata.recordNone(revisionNumber); kitaev@213: da.reset(); kitaev@213: return; kitaev@213: } kitaev@213: int lastEntryStart = 2; kitaev@213: int lastColon = -1; kitaev@213: ArrayList _metadata = new ArrayList(); kitaev@213: // XXX in fact, need smth like ByteArrayBuilder, similar to StringBuilder, kitaev@213: // which can't be used here because we can't convert bytes to chars as we read them kitaev@213: // (there might be multi-byte encoding), and we need to collect all bytes before converting to string kitaev@213: ByteArrayOutputStream bos = new ByteArrayOutputStream(); kitaev@213: String key = null, value = null; kitaev@213: boolean byteOne = false; kitaev@213: for (int i = 2; i < daLength; i++) { kitaev@213: byte b = da.readByte(); kitaev@213: if (b == '\n') { kitaev@213: if (byteOne) { // i.e. \n follows 1 kitaev@213: lastEntryStart = i+1; kitaev@213: // XXX is it possible to have here incomplete key/value (i.e. if last pair didn't end with \n) kitaev@213: break; kitaev@213: } kitaev@213: if (key == null || lastColon == -1 || i <= lastColon) { kitaev@213: throw new IllegalStateException(); // FIXME log instead and record null key in the metadata. Ex just to fail fast during dev kitaev@213: } kitaev@213: value = new String(bos.toByteArray()).trim(); kitaev@213: bos.reset(); kitaev@213: _metadata.add(new MetadataEntry(key, value)); kitaev@213: key = value = null; kitaev@213: lastColon = -1; kitaev@213: lastEntryStart = i+1; kitaev@213: continue; kitaev@213: } kitaev@213: // byteOne has to be consumed up to this line, if not jet, consume it kitaev@213: if (byteOne) { kitaev@213: // insert 1 we've read on previous step into the byte builder kitaev@213: bos.write(1); kitaev@213: // fall-through to consume current byte kitaev@213: byteOne = false; kitaev@213: } kitaev@213: if (b == (int) ':') { kitaev@213: assert value == null; kitaev@213: key = new String(bos.toByteArray()); kitaev@213: bos.reset(); kitaev@213: lastColon = i; kitaev@213: } else if (b == 1) { kitaev@213: byteOne = true; kitaev@213: } else { kitaev@213: bos.write(b); kitaev@213: } kitaev@213: } kitaev@213: _metadata.trimToSize(); kitaev@213: metadata.add(revisionNumber, lastEntryStart, _metadata); kitaev@213: if (da.isEmpty() || !byteOne) { kitaev@213: throw new HgDataStreamException(String.format("Metadata for revision %d is not closed properly", revisionNumber), null); kitaev@213: } kitaev@213: // da is in prepared state (i.e. we consumed all bytes up to metadata end). kitaev@213: } kitaev@213: } kitaev@213: }