Mercurial > jhg
comparison hg4j/src/main/java/org/tmatesoft/hg/repo/HgDataFile.java @ 213:6ec4af642ba8 gradle
Project uses Gradle for build - actual changes
| author | Alexander Kitaev <kitaev@gmail.com> |
|---|---|
| date | Tue, 10 May 2011 10:52:53 +0200 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 212:edb2e2829352 | 213:6ec4af642ba8 |
|---|---|
| 1 /* | |
| 2 * Copyright (c) 2010-2011 TMate Software Ltd | |
| 3 * | |
| 4 * This program is free software; you can redistribute it and/or modify | |
| 5 * it under the terms of the GNU General Public License as published by | |
| 6 * the Free Software Foundation; version 2 of the License. | |
| 7 * | |
| 8 * This program is distributed in the hope that it will be useful, | |
| 9 * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| 11 * GNU General Public License for more details. | |
| 12 * | |
| 13 * For information on how to redistribute this software under | |
| 14 * the terms of a license other than GNU General Public License | |
| 15 * contact TMate Software at support@hg4j.com | |
| 16 */ | |
| 17 package org.tmatesoft.hg.repo; | |
| 18 | |
| 19 import static org.tmatesoft.hg.repo.HgInternals.wrongLocalRevision; | |
| 20 import static org.tmatesoft.hg.repo.HgRepository.*; | |
| 21 | |
| 22 import java.io.ByteArrayOutputStream; | |
| 23 import java.io.IOException; | |
| 24 import java.nio.ByteBuffer; | |
| 25 import java.util.ArrayList; | |
| 26 import java.util.Collection; | |
| 27 import java.util.TreeMap; | |
| 28 | |
| 29 import org.tmatesoft.hg.core.HgDataStreamException; | |
| 30 import org.tmatesoft.hg.core.HgException; | |
| 31 import org.tmatesoft.hg.core.Nodeid; | |
| 32 import org.tmatesoft.hg.internal.DataAccess; | |
| 33 import org.tmatesoft.hg.internal.FilterByteChannel; | |
| 34 import org.tmatesoft.hg.internal.RevlogStream; | |
| 35 import org.tmatesoft.hg.util.ByteChannel; | |
| 36 import org.tmatesoft.hg.util.CancelledException; | |
| 37 import org.tmatesoft.hg.util.Path; | |
| 38 | |
| 39 | |
| 40 | |
| 41 /** | |
| 42 * ? name:HgFileNode? | |
| 43 * | |
| 44 * @author Artem Tikhomirov | |
| 45 * @author TMate Software Ltd. | |
| 46 */ | |
| 47 public class HgDataFile extends Revlog { | |
| 48 | |
| 49 // absolute from repo root? | |
| 50 // slashes, unix-style? | |
| 51 // repo location agnostic, just to give info to user, not to access real storage | |
| 52 private final Path path; | |
| 53 private Metadata metadata; // get initialized on first access to file content. | |
| 54 | |
| 55 /*package-local*/HgDataFile(HgRepository hgRepo, Path filePath, RevlogStream content) { | |
| 56 super(hgRepo, content); | |
| 57 path = filePath; | |
| 58 } | |
| 59 | |
| 60 /*package-local*/HgDataFile(HgRepository hgRepo, Path filePath) { | |
| 61 super(hgRepo); | |
| 62 path = filePath; | |
| 63 } | |
| 64 | |
| 65 // exists is not the best name possible. now it means no file with such name was ever known to the repo. | |
| 66 // it might be confused with files existed before but lately removed. | |
| 67 public boolean exists() { | |
| 68 return content != null; // XXX need better impl | |
| 69 } | |
| 70 | |
| 71 // human-readable (i.e. "COPYING", not "store/data/_c_o_p_y_i_n_g.i") | |
| 72 public Path getPath() { | |
| 73 return path; // hgRepo.backresolve(this) -> name? In this case, what about hashed long names? | |
| 74 } | |
| 75 | |
| 76 public int length(Nodeid nodeid) { | |
| 77 return content.dataLength(getLocalRevision(nodeid)); | |
| 78 } | |
| 79 | |
| 80 public void workingCopy(ByteChannel sink) throws IOException, CancelledException { | |
| 81 throw HgRepository.notImplemented(); | |
| 82 } | |
| 83 | |
| 84 // public void content(int revision, ByteChannel sink, boolean applyFilters) throws HgDataStreamException, IOException, CancelledException { | |
| 85 // byte[] content = content(revision); | |
| 86 // final CancelSupport cancelSupport = CancelSupport.Factory.get(sink); | |
| 87 // final ProgressSupport progressSupport = ProgressSupport.Factory.get(sink); | |
| 88 // ByteBuffer buf = ByteBuffer.allocate(512); | |
| 89 // int left = content.length; | |
| 90 // progressSupport.start(left); | |
| 91 // int offset = 0; | |
| 92 // cancelSupport.checkCancelled(); | |
| 93 // ByteChannel _sink = applyFilters ? new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath())) : sink; | |
| 94 // do { | |
| 95 // buf.put(content, offset, Math.min(left, buf.remaining())); | |
| 96 // buf.flip(); | |
| 97 // cancelSupport.checkCancelled(); | |
| 98 // // XXX I may not rely on returned number of bytes but track change in buf position instead. | |
| 99 // int consumed = _sink.write(buf); | |
| 100 // buf.compact(); | |
| 101 // offset += consumed; | |
| 102 // left -= consumed; | |
| 103 // progressSupport.worked(consumed); | |
| 104 // } while (left > 0); | |
| 105 // progressSupport.done(); // XXX shall specify whether #done() is invoked always or only if completed successfully. | |
| 106 // } | |
| 107 | |
| 108 /*XXX not sure distinct method contentWithFilters() is the best way to do, perhaps, callers shall add filters themselves?*/ | |
| 109 public void contentWithFilters(int revision, ByteChannel sink) throws HgDataStreamException, IOException, CancelledException { | |
| 110 content(revision, new FilterByteChannel(sink, getRepo().getFiltersFromRepoToWorkingDir(getPath()))); | |
| 111 } | |
| 112 | |
| 113 // for data files need to check heading of the file content for possible metadata | |
| 114 // @see http://mercurial.selenic.com/wiki/FileFormats#data.2BAC8- | |
| 115 public void content(int revision, ByteChannel sink) throws HgDataStreamException, IOException, CancelledException { | |
| 116 if (revision == TIP) { | |
| 117 revision = getLastRevision(); | |
| 118 } | |
| 119 if (revision == WORKING_COPY) { | |
| 120 workingCopy(sink); | |
| 121 return; | |
| 122 } | |
| 123 if (wrongLocalRevision(revision) || revision == BAD_REVISION) { | |
| 124 throw new IllegalArgumentException(String.valueOf(revision)); | |
| 125 } | |
| 126 if (sink == null) { | |
| 127 throw new IllegalArgumentException(); | |
| 128 } | |
| 129 if (metadata == null) { | |
| 130 metadata = new Metadata(); | |
| 131 } | |
| 132 ContentPipe insp; | |
| 133 if (metadata.none(revision)) { | |
| 134 insp = new ContentPipe(sink, 0); | |
| 135 } else if (metadata.known(revision)) { | |
| 136 insp = new ContentPipe(sink, metadata.dataOffset(revision)); | |
| 137 } else { | |
| 138 // do not know if there's metadata | |
| 139 insp = new MetadataContentPipe(sink, metadata); | |
| 140 } | |
| 141 insp.checkCancelled(); | |
| 142 super.content.iterate(revision, revision, true, insp); | |
| 143 try { | |
| 144 insp.checkFailed(); | |
| 145 } catch (HgDataStreamException ex) { | |
| 146 throw ex; | |
| 147 } catch (HgException ex) { | |
| 148 // shall not happen, unless we changed ContentPipe or its subclass | |
| 149 throw new HgDataStreamException(ex.getClass().getName(), ex); | |
| 150 } | |
| 151 } | |
| 152 | |
| 153 public void history(HgChangelog.Inspector inspector) { | |
| 154 history(0, getLastRevision(), inspector); | |
| 155 } | |
| 156 | |
| 157 public void history(int start, int end, HgChangelog.Inspector inspector) { | |
| 158 if (!exists()) { | |
| 159 throw new IllegalStateException("Can't get history of invalid repository file node"); | |
| 160 } | |
| 161 final int last = getLastRevision(); | |
| 162 if (start < 0 || start > last) { | |
| 163 throw new IllegalArgumentException(); | |
| 164 } | |
| 165 if (end == TIP) { | |
| 166 end = last; | |
| 167 } else if (end < start || end > last) { | |
| 168 throw new IllegalArgumentException(); | |
| 169 } | |
| 170 final int[] commitRevisions = new int[end - start + 1]; | |
| 171 RevlogStream.Inspector insp = new RevlogStream.Inspector() { | |
| 172 int count = 0; | |
| 173 | |
| 174 public void next(int revisionNumber, int actualLen, int baseRevision, int linkRevision, int parent1Revision, int parent2Revision, byte[] nodeid, DataAccess data) { | |
| 175 commitRevisions[count++] = linkRevision; | |
| 176 } | |
| 177 }; | |
| 178 content.iterate(start, end, false, insp); | |
| 179 getRepo().getChangelog().range(inspector, commitRevisions); | |
| 180 } | |
| 181 | |
| 182 // for a given local revision of the file, find out local revision in the changelog | |
| 183 public int getChangesetLocalRevision(int revision) { | |
| 184 return content.linkRevision(revision); | |
| 185 } | |
| 186 | |
| 187 public Nodeid getChangesetRevision(Nodeid nid) { | |
| 188 int changelogRevision = getChangesetLocalRevision(getLocalRevision(nid)); | |
| 189 return getRepo().getChangelog().getRevision(changelogRevision); | |
| 190 } | |
| 191 | |
| 192 public boolean isCopy() throws HgDataStreamException { | |
| 193 if (metadata == null || !metadata.checked(0)) { | |
| 194 // content() always initializes metadata. | |
| 195 // FIXME this is expensive way to find out metadata, distinct RevlogStream.Iterator would be better. | |
| 196 // Alternatively, may parameterize MetadataContentPipe to do prepare only. | |
| 197 // For reference, when throwing CancelledException, hg status -A --rev 3:80 takes 70 ms | |
| 198 // however, if we just consume buffer instead (buffer.position(buffer.limit()), same command takes ~320ms | |
| 199 // (compared to command-line counterpart of 190ms) | |
| 200 try { | |
| 201 content(0, new ByteChannel() { // No-op channel | |
| 202 public int write(ByteBuffer buffer) throws IOException, CancelledException { | |
| 203 // pretend we consumed whole buffer | |
| 204 // int rv = buffer.remaining(); | |
| 205 // buffer.position(buffer.limit()); | |
| 206 // return rv; | |
| 207 throw new CancelledException(); | |
| 208 } | |
| 209 }); | |
| 210 } catch (CancelledException ex) { | |
| 211 // it's ok, we did that | |
| 212 } catch (Exception ex) { | |
| 213 throw new HgDataStreamException("Can't initialize metadata", ex); | |
| 214 } | |
| 215 } | |
| 216 if (!metadata.known(0)) { | |
| 217 return false; | |
| 218 } | |
| 219 return metadata.find(0, "copy") != null; | |
| 220 } | |
| 221 | |
| 222 public Path getCopySourceName() throws HgDataStreamException { | |
| 223 if (isCopy()) { | |
| 224 return Path.create(metadata.find(0, "copy")); | |
| 225 } | |
| 226 throw new UnsupportedOperationException(); // XXX REVISIT, think over if Exception is good (clients would check isCopy() anyway, perhaps null is sufficient?) | |
| 227 } | |
| 228 | |
| 229 public Nodeid getCopySourceRevision() throws HgDataStreamException { | |
| 230 if (isCopy()) { | |
| 231 return Nodeid.fromAscii(metadata.find(0, "copyrev")); // XXX reuse/cache Nodeid | |
| 232 } | |
| 233 throw new UnsupportedOperationException(); | |
| 234 } | |
| 235 | |
| 236 @Override | |
| 237 public String toString() { | |
| 238 StringBuilder sb = new StringBuilder(getClass().getSimpleName()); | |
| 239 sb.append('('); | |
| 240 sb.append(getPath()); | |
| 241 sb.append(')'); | |
| 242 return sb.toString(); | |
| 243 } | |
| 244 | |
| 245 private static final class MetadataEntry { | |
| 246 private final String entry; | |
| 247 private final int valueStart; | |
| 248 /*package-local*/MetadataEntry(String key, String value) { | |
| 249 entry = key + value; | |
| 250 valueStart = key.length(); | |
| 251 } | |
| 252 /*package-local*/boolean matchKey(String key) { | |
| 253 return key.length() == valueStart && entry.startsWith(key); | |
| 254 } | |
| 255 // uncomment once/if needed | |
| 256 // public String key() { | |
| 257 // return entry.substring(0, valueStart); | |
| 258 // } | |
| 259 public String value() { | |
| 260 return entry.substring(valueStart); | |
| 261 } | |
| 262 } | |
| 263 | |
| 264 private static class Metadata { | |
| 265 // XXX sparse array needed | |
| 266 private final TreeMap<Integer, Integer> offsets = new TreeMap<Integer, Integer>(); | |
| 267 private final TreeMap<Integer, MetadataEntry[]> entries = new TreeMap<Integer, MetadataEntry[]>(); | |
| 268 | |
| 269 private final Integer NONE = new Integer(-1); // do not duplicate -1 integers at least within single file (don't want statics) | |
| 270 | |
| 271 // true when there's metadata for given revision | |
| 272 boolean known(int revision) { | |
| 273 Integer i = offsets.get(revision); | |
| 274 return i != null && NONE != i; | |
| 275 } | |
| 276 | |
| 277 // true when revision has been checked for metadata presence. | |
| 278 public boolean checked(int revision) { | |
| 279 return offsets.containsKey(revision); | |
| 280 } | |
| 281 | |
| 282 // true when revision has been checked and found not having any metadata | |
| 283 boolean none(int revision) { | |
| 284 Integer i = offsets.get(revision); | |
| 285 return i == NONE; | |
| 286 } | |
| 287 | |
| 288 // mark revision as having no metadata. | |
| 289 void recordNone(int revision) { | |
| 290 Integer i = offsets.get(revision); | |
| 291 if (i == NONE) { | |
| 292 return; // already there | |
| 293 } | |
| 294 if (i != null) { | |
| 295 throw new IllegalStateException(String.format("Trying to override Metadata state for revision %d (known offset: %d)", revision, i)); | |
| 296 } | |
| 297 offsets.put(revision, NONE); | |
| 298 } | |
| 299 | |
| 300 // since this is internal class, callers are supposed to ensure arg correctness (i.e. ask known() before) | |
| 301 int dataOffset(int revision) { | |
| 302 return offsets.get(revision); | |
| 303 } | |
| 304 void add(int revision, int dataOffset, Collection<MetadataEntry> e) { | |
| 305 assert !offsets.containsKey(revision); | |
| 306 offsets.put(revision, dataOffset); | |
| 307 entries.put(revision, e.toArray(new MetadataEntry[e.size()])); | |
| 308 } | |
| 309 String find(int revision, String key) { | |
| 310 for (MetadataEntry me : entries.get(revision)) { | |
| 311 if (me.matchKey(key)) { | |
| 312 return me.value(); | |
| 313 } | |
| 314 } | |
| 315 return null; | |
| 316 } | |
| 317 } | |
| 318 | |
| 319 private static class MetadataContentPipe extends ContentPipe { | |
| 320 | |
| 321 private final Metadata metadata; | |
| 322 | |
| 323 public MetadataContentPipe(ByteChannel sink, Metadata _metadata) { | |
| 324 super(sink, 0); | |
| 325 metadata = _metadata; | |
| 326 } | |
| 327 | |
| 328 @Override | |
| 329 protected void prepare(int revisionNumber, DataAccess da) throws HgException, IOException { | |
| 330 final int daLength = da.length(); | |
| 331 if (daLength < 4 || da.readByte() != 1 || da.readByte() != 10) { | |
| 332 metadata.recordNone(revisionNumber); | |
| 333 da.reset(); | |
| 334 return; | |
| 335 } | |
| 336 int lastEntryStart = 2; | |
| 337 int lastColon = -1; | |
| 338 ArrayList<MetadataEntry> _metadata = new ArrayList<MetadataEntry>(); | |
| 339 // XXX in fact, need smth like ByteArrayBuilder, similar to StringBuilder, | |
| 340 // which can't be used here because we can't convert bytes to chars as we read them | |
| 341 // (there might be multi-byte encoding), and we need to collect all bytes before converting to string | |
| 342 ByteArrayOutputStream bos = new ByteArrayOutputStream(); | |
| 343 String key = null, value = null; | |
| 344 boolean byteOne = false; | |
| 345 for (int i = 2; i < daLength; i++) { | |
| 346 byte b = da.readByte(); | |
| 347 if (b == '\n') { | |
| 348 if (byteOne) { // i.e. \n follows 1 | |
| 349 lastEntryStart = i+1; | |
| 350 // XXX is it possible to have here incomplete key/value (i.e. if last pair didn't end with \n) | |
| 351 break; | |
| 352 } | |
| 353 if (key == null || lastColon == -1 || i <= lastColon) { | |
| 354 throw new IllegalStateException(); // FIXME log instead and record null key in the metadata. Ex just to fail fast during dev | |
| 355 } | |
| 356 value = new String(bos.toByteArray()).trim(); | |
| 357 bos.reset(); | |
| 358 _metadata.add(new MetadataEntry(key, value)); | |
| 359 key = value = null; | |
| 360 lastColon = -1; | |
| 361 lastEntryStart = i+1; | |
| 362 continue; | |
| 363 } | |
| 364 // byteOne has to be consumed up to this line, if not jet, consume it | |
| 365 if (byteOne) { | |
| 366 // insert 1 we've read on previous step into the byte builder | |
| 367 bos.write(1); | |
| 368 // fall-through to consume current byte | |
| 369 byteOne = false; | |
| 370 } | |
| 371 if (b == (int) ':') { | |
| 372 assert value == null; | |
| 373 key = new String(bos.toByteArray()); | |
| 374 bos.reset(); | |
| 375 lastColon = i; | |
| 376 } else if (b == 1) { | |
| 377 byteOne = true; | |
| 378 } else { | |
| 379 bos.write(b); | |
| 380 } | |
| 381 } | |
| 382 _metadata.trimToSize(); | |
| 383 metadata.add(revisionNumber, lastEntryStart, _metadata); | |
| 384 if (da.isEmpty() || !byteOne) { | |
| 385 throw new HgDataStreamException(String.format("Metadata for revision %d is not closed properly", revisionNumber), null); | |
| 386 } | |
| 387 // da is in prepared state (i.e. we consumed all bytes up to metadata end). | |
| 388 } | |
| 389 } | |
| 390 } |
