Mercurial > jhg
comparison src/org/tmatesoft/hg/core/HgCloneCommand.java @ 530:0f6fa88e2162
Towards commit command: refactor clone, extract pieces to reuse. Describe a defect discovered when bundle has few patches with 0,0 parents
| author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
|---|---|
| date | Wed, 23 Jan 2013 17:46:12 +0100 |
| parents | a41d955dc360 |
| children | 95c2f43008bd |
comparison
equal
deleted
inserted
replaced
| 529:95bdcf75e71e | 530:0f6fa88e2162 |
|---|---|
| 1 /* | 1 /* |
| 2 * Copyright (c) 2011-2012 TMate Software Ltd | 2 * Copyright (c) 2011-2013 TMate Software Ltd |
| 3 * | 3 * |
| 4 * This program is free software; you can redistribute it and/or modify | 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 | 5 * it under the terms of the GNU General Public License as published by |
| 6 * the Free Software Foundation; version 2 of the License. | 6 * the Free Software Foundation; version 2 of the License. |
| 7 * | 7 * |
| 17 package org.tmatesoft.hg.core; | 17 package org.tmatesoft.hg.core; |
| 18 | 18 |
| 19 import static org.tmatesoft.hg.core.Nodeid.NULL; | 19 import static org.tmatesoft.hg.core.Nodeid.NULL; |
| 20 import static org.tmatesoft.hg.internal.RequiresFile.*; | 20 import static org.tmatesoft.hg.internal.RequiresFile.*; |
| 21 | 21 |
| 22 import java.io.ByteArrayOutputStream; | |
| 23 import java.io.File; | 22 import java.io.File; |
| 24 import java.io.FileOutputStream; | 23 import java.io.FileOutputStream; |
| 25 import java.io.IOException; | 24 import java.io.IOException; |
| 26 import java.nio.ByteBuffer; | |
| 27 import java.util.ArrayList; | 25 import java.util.ArrayList; |
| 28 import java.util.Collections; | 26 import java.util.Collections; |
| 29 import java.util.LinkedList; | 27 import java.util.LinkedList; |
| 30 import java.util.TreeMap; | 28 import java.util.TreeMap; |
| 31 import java.util.zip.DeflaterOutputStream; | |
| 32 | 29 |
| 33 import org.tmatesoft.hg.internal.ByteArrayDataAccess; | 30 import org.tmatesoft.hg.internal.ByteArrayDataAccess; |
| 34 import org.tmatesoft.hg.internal.DataAccess; | 31 import org.tmatesoft.hg.internal.DataAccess; |
| 35 import org.tmatesoft.hg.internal.DigestHelper; | 32 import org.tmatesoft.hg.internal.DigestHelper; |
| 36 import org.tmatesoft.hg.internal.Lifecycle; | 33 import org.tmatesoft.hg.internal.Lifecycle; |
| 37 import org.tmatesoft.hg.internal.RepoInitializer; | 34 import org.tmatesoft.hg.internal.RepoInitializer; |
| 35 import org.tmatesoft.hg.internal.RevlogCompressor; | |
| 36 import org.tmatesoft.hg.internal.RevlogStreamWriter; | |
| 38 import org.tmatesoft.hg.repo.HgBundle; | 37 import org.tmatesoft.hg.repo.HgBundle; |
| 39 import org.tmatesoft.hg.repo.HgBundle.GroupElement; | 38 import org.tmatesoft.hg.repo.HgBundle.GroupElement; |
| 40 import org.tmatesoft.hg.repo.HgInvalidControlFileException; | 39 import org.tmatesoft.hg.repo.HgInvalidControlFileException; |
| 41 import org.tmatesoft.hg.repo.HgInvalidFileException; | 40 import org.tmatesoft.hg.repo.HgInvalidFileException; |
| 42 import org.tmatesoft.hg.repo.HgInvalidStateException; | 41 import org.tmatesoft.hg.repo.HgInvalidStateException; |
| 97 } | 96 } |
| 98 if (destination.exists()) { | 97 if (destination.exists()) { |
| 99 if (!destination.isDirectory()) { | 98 if (!destination.isDirectory()) { |
| 100 throw new HgBadArgumentException(String.format("%s is not a directory", destination), null); | 99 throw new HgBadArgumentException(String.format("%s is not a directory", destination), null); |
| 101 } else if (destination.list().length > 0) { | 100 } else if (destination.list().length > 0) { |
| 102 throw new HgBadArgumentException(String.format("% shall be empty", destination), null); | 101 throw new HgBadArgumentException(String.format("%s shall be empty", destination), null); |
| 103 } | 102 } |
| 104 } else { | 103 } else { |
| 105 destination.mkdirs(); | 104 destination.mkdirs(); |
| 106 } | 105 } |
| 107 ProgressSupport progress = getProgressSupport(null); | 106 ProgressSupport progress = getProgressSupport(null); |
| 144 private String filename; // human-readable name of the file being written, for log/exception purposes | 143 private String filename; // human-readable name of the file being written, for log/exception purposes |
| 145 | 144 |
| 146 private final TreeMap<Nodeid, Integer> changelogIndexes = new TreeMap<Nodeid, Integer>(); | 145 private final TreeMap<Nodeid, Integer> changelogIndexes = new TreeMap<Nodeid, Integer>(); |
| 147 private boolean collectChangelogIndexes = false; | 146 private boolean collectChangelogIndexes = false; |
| 148 | 147 |
| 149 private int base = -1; | |
| 150 private long offset = 0; | |
| 151 private DataAccess prevRevContent; | 148 private DataAccess prevRevContent; |
| 152 private final DigestHelper dh = new DigestHelper(); | 149 private final DigestHelper dh = new DigestHelper(); |
| 153 private final ArrayList<Nodeid> revisionSequence = new ArrayList<Nodeid>(); // last visited nodes first | 150 private final ArrayList<Nodeid> revisionSequence = new ArrayList<Nodeid>(); // last visited nodes first |
| 154 | 151 |
| 155 private final LinkedList<String> fncacheFiles = new LinkedList<String>(); | 152 private final LinkedList<String> fncacheFiles = new LinkedList<String>(); |
| 179 fncacheFile.close(); | 176 fncacheFile.close(); |
| 180 } | 177 } |
| 181 | 178 |
| 182 public void changelogStart() { | 179 public void changelogStart() { |
| 183 try { | 180 try { |
| 184 base = -1; | 181 revlogHeader.offset(0).baseRevision(-1); |
| 185 offset = 0; | |
| 186 revisionSequence.clear(); | 182 revisionSequence.clear(); |
| 187 indexFile = new FileOutputStream(new File(hgDir, filename = "store/00changelog.i")); | 183 indexFile = new FileOutputStream(new File(hgDir, filename = "store/00changelog.i")); |
| 188 collectChangelogIndexes = true; | 184 collectChangelogIndexes = true; |
| 189 } catch (IOException ex) { | 185 } catch (IOException ex) { |
| 190 throw new HgInvalidControlFileException("Failed to write changelog", ex, new File(filename)); | 186 throw new HgInvalidControlFileException("Failed to write changelog", ex, new File(filename)); |
| 209 stopIfCancelled(); | 205 stopIfCancelled(); |
| 210 } | 206 } |
| 211 | 207 |
| 212 public void manifestStart() { | 208 public void manifestStart() { |
| 213 try { | 209 try { |
| 214 base = -1; | 210 revlogHeader.offset(0).baseRevision(-1); |
| 215 offset = 0; | |
| 216 revisionSequence.clear(); | 211 revisionSequence.clear(); |
| 217 indexFile = new FileOutputStream(new File(hgDir, filename = "store/00manifest.i")); | 212 indexFile = new FileOutputStream(new File(hgDir, filename = "store/00manifest.i")); |
| 218 } catch (IOException ex) { | 213 } catch (IOException ex) { |
| 219 throw new HgInvalidControlFileException("Failed to write manifest", ex, new File(filename)); | 214 throw new HgInvalidControlFileException("Failed to write manifest", ex, new File(filename)); |
| 220 } | 215 } |
| 237 stopIfCancelled(); | 232 stopIfCancelled(); |
| 238 } | 233 } |
| 239 | 234 |
| 240 public void fileStart(String name) { | 235 public void fileStart(String name) { |
| 241 try { | 236 try { |
| 242 base = -1; | 237 revlogHeader.offset(0).baseRevision(-1); |
| 243 offset = 0; | |
| 244 revisionSequence.clear(); | 238 revisionSequence.clear(); |
| 245 fncacheFiles.add("data/" + name + ".i"); // TODO post-1.0 this is pure guess, | 239 fncacheFiles.add("data/" + name + ".i"); // TODO post-1.0 this is pure guess, |
| 246 // need to investigate more how filenames are kept in fncache | 240 // need to investigate more how filenames are kept in fncache |
| 247 File file = new File(hgDir, filename = storagePathHelper.rewrite(name).toString()); | 241 File file = new File(hgDir, filename = storagePathHelper.rewrite(name).toString()); |
| 248 file.getParentFile().mkdirs(); | 242 file.getParentFile().mkdirs(); |
| 282 } | 276 } |
| 283 } | 277 } |
| 284 String m = String.format("Can't find index of %s for file %s", p.shortNotation(), filename); | 278 String m = String.format("Can't find index of %s for file %s", p.shortNotation(), filename); |
| 285 throw new HgInvalidControlFileException(m, null, null).setRevision(p); | 279 throw new HgInvalidControlFileException(m, null, null).setRevision(p); |
| 286 } | 280 } |
| 281 | |
| 282 private RevlogStreamWriter.HeaderWriter revlogHeader = new RevlogStreamWriter.HeaderWriter(true); | |
| 283 private RevlogCompressor revlogDataZip = new RevlogCompressor(); | |
| 287 | 284 |
| 288 public boolean element(GroupElement ge) { | 285 public boolean element(GroupElement ge) { |
| 289 try { | 286 try { |
| 290 assert indexFile != null; | 287 assert indexFile != null; |
| 291 boolean writeComplete = false; | 288 boolean writeComplete = false; |
| 292 Nodeid p1 = ge.firstParent(); | 289 Nodeid p1 = ge.firstParent(); |
| 293 Nodeid p2 = ge.secondParent(); | 290 Nodeid p2 = ge.secondParent(); |
| 294 if (p1.isNull() && p2.isNull() /* or forced flag, does REVIDX_PUNCHED_FLAG indicate that? */) { | 291 if (p1.isNull() && p2.isNull() /* or forced flag, does REVIDX_PUNCHED_FLAG indicate that? */) { |
| 292 // FIXME NOTE, both parents isNull == true doesn't necessarily mean | |
| 293 // empty prevContent, see build.gradle sample below | |
| 295 prevRevContent = new ByteArrayDataAccess(new byte[0]); | 294 prevRevContent = new ByteArrayDataAccess(new byte[0]); |
| 296 writeComplete = true; | 295 writeComplete = true; |
| 297 } | 296 } |
| 298 byte[] content = ge.apply(prevRevContent.byteArray()); | 297 byte[] content = ge.apply(prevRevContent.byteArray()); |
| 299 byte[] calculated = dh.sha1(p1, p2, content).asBinary(); | 298 byte[] calculated = dh.sha1(p1, p2, content).asBinary(); |
| 300 final Nodeid node = ge.node(); | 299 final Nodeid node = ge.node(); |
| 301 if (!node.equalsTo(calculated)) { | 300 if (!node.equalsTo(calculated)) { |
| 302 // TODO post-1.0 custom exception ChecksumCalculationFailed? | 301 // TODO post-1.0 custom exception ChecksumCalculationFailed? |
| 303 throw new HgInvalidStateException(String.format("Checksum failed: expected %s, calculated %s. File %s", node, calculated, filename)); | 302 throw new HgInvalidStateException(String.format("Checksum failed: expected %s, calculated %s. File %s", node, calculated, filename)); |
| 304 } | 303 } |
| 305 final int link; | 304 revlogHeader.nodeid(node); |
| 306 if (collectChangelogIndexes) { | 305 if (collectChangelogIndexes) { |
| 307 changelogIndexes.put(node, revisionSequence.size()); | 306 changelogIndexes.put(node, revisionSequence.size()); |
| 308 link = revisionSequence.size(); | 307 revlogHeader.linkRevision(revisionSequence.size()); |
| 309 } else { | 308 } else { |
| 310 Integer csRev = changelogIndexes.get(ge.cset()); | 309 Integer csRev = changelogIndexes.get(ge.cset()); |
| 311 if (csRev == null) { | 310 if (csRev == null) { |
| 312 throw new HgInvalidStateException(String.format("Changelog doesn't contain revision %s of %s", ge.cset().shortNotation(), filename)); | 311 throw new HgInvalidStateException(String.format("Changelog doesn't contain revision %s of %s", ge.cset().shortNotation(), filename)); |
| 313 } | 312 } |
| 314 link = csRev.intValue(); | 313 revlogHeader.linkRevision(csRev.intValue()); |
| 315 } | 314 } |
| 316 final int p1Rev = knownRevision(p1), p2Rev = knownRevision(p2); | 315 revlogHeader.parents(knownRevision(p1), knownRevision(p2)); |
| 317 byte[] patchContent = ge.rawDataByteArray(); | 316 byte[] patchContent = ge.rawDataByteArray(); |
| 318 writeComplete = writeComplete || patchContent.length >= (/* 3/4 of actual */content.length - (content.length >>> 2)); | 317 writeComplete = writeComplete || patchContent.length >= (/* 3/4 of actual */content.length - (content.length >>> 2)); |
| 319 if (writeComplete) { | 318 if (writeComplete) { |
| 320 base = revisionSequence.size(); | 319 revlogHeader.baseRevision(revisionSequence.size()); |
| 321 } | 320 } |
| 322 final byte[] sourceData = writeComplete ? content : patchContent; | 321 final byte[] sourceData = writeComplete ? content : patchContent; |
| 323 final byte[] data; | 322 revlogDataZip.reset(sourceData); |
| 324 ByteArrayOutputStream bos = new ByteArrayOutputStream(content.length); | 323 final int compressedLen; |
| 325 DeflaterOutputStream dos = new DeflaterOutputStream(bos); | 324 final boolean useUncompressedData = revlogDataZip.getCompressedLengthEstimate() >= (sourceData.length - (sourceData.length >>> 2)); |
| 326 dos.write(sourceData); | 325 if (useUncompressedData) { |
| 327 dos.close(); | |
| 328 final byte[] compressedData = bos.toByteArray(); | |
| 329 dos = null; | |
| 330 bos = null; | |
| 331 final Byte dataPrefix; | |
| 332 if (compressedData.length >= (sourceData.length - (sourceData.length >>> 2))) { | |
| 333 // compression wasn't too effective, | 326 // compression wasn't too effective, |
| 334 data = sourceData; | 327 compressedLen = sourceData.length + 1 /*1 byte for 'u' - uncompressed prefix byte*/; |
| 335 dataPrefix = 'u'; | |
| 336 } else { | 328 } else { |
| 337 data = compressedData; | 329 compressedLen= revlogDataZip.getCompressedLengthEstimate(); |
| 338 dataPrefix = null; | 330 } |
| 339 } | 331 |
| 340 | 332 revlogHeader.length(content.length, compressedLen); |
| 341 ByteBuffer header = ByteBuffer.allocate(64 /* REVLOGV1_RECORD_SIZE */); | 333 |
| 342 if (offset == 0) { | 334 revlogHeader.write(indexFile); |
| 343 final int INLINEDATA = 1 << 16; | 335 |
| 344 header.putInt(1 /* RevlogNG */ | INLINEDATA); | 336 if (useUncompressedData) { |
| 345 header.putInt(0); | 337 indexFile.write((byte) 'u'); |
| 338 indexFile.write(sourceData); | |
| 346 } else { | 339 } else { |
| 347 header.putLong(offset << 16); | 340 int actualCompressedLenWritten = revlogDataZip.writeCompressedData(indexFile); |
| 348 } | 341 if (actualCompressedLenWritten != compressedLen) { |
| 349 final int compressedLen = data.length + (dataPrefix == null ? 0 : 1); | 342 throw new HgInvalidStateException(String.format("Expected %d bytes of compressed data, but actually wrote %d in %s", compressedLen, actualCompressedLenWritten, filename)); |
| 350 header.putInt(compressedLen); | 343 } |
| 351 header.putInt(content.length); | 344 } |
| 352 header.putInt(base); | |
| 353 header.putInt(link); | |
| 354 header.putInt(p1Rev); | |
| 355 header.putInt(p2Rev); | |
| 356 header.put(node.toByteArray()); | |
| 357 // assume 12 bytes left are zeros | |
| 358 indexFile.write(header.array()); | |
| 359 if (dataPrefix != null) { | |
| 360 indexFile.write(dataPrefix.byteValue()); | |
| 361 } | |
| 362 indexFile.write(data); | |
| 363 // | 345 // |
| 364 offset += compressedLen; | |
| 365 revisionSequence.add(node); | 346 revisionSequence.add(node); |
| 366 prevRevContent.done(); | 347 prevRevContent.done(); |
| 367 prevRevContent = new ByteArrayDataAccess(content); | 348 prevRevContent = new ByteArrayDataAccess(content); |
| 368 } catch (IOException ex) { | 349 } catch (IOException ex) { |
| 369 String m = String.format("Failed to write revision %s of file %s", ge.node().shortNotation(), filename); | 350 String m = String.format("Failed to write revision %s of file %s", ge.node().shortNotation(), filename); |
| 370 throw new HgInvalidControlFileException(m, ex, new File(filename)); | 351 throw new HgInvalidControlFileException(m, ex, new File(filename)); |
| 371 } | 352 } |
| 372 return cancelException == null; | 353 return cancelException == null; |
| 373 } | 354 } |
| 355 /* | |
| 356 $ hg debugindex build.gradle | |
| 357 rev offset length base linkrev nodeid p1 p2 | |
| 358 0 0 857 0 454 b2a1b20d1933 000000000000 000000000000 | |
| 359 1 857 319 0 455 5324c8f2b550 b2a1b20d1933 000000000000 | |
| 360 2 1176 533 0 460 4011d52141cd 5324c8f2b550 000000000000 | |
| 361 3 1709 85 0 463 d0be58845306 4011d52141cd 000000000000 | |
| 362 4 1794 105 0 464 3ddd456244a0 d0be58845306 000000000000 | |
| 363 5 1899 160 0 466 a3f374fbf33a 3ddd456244a0 000000000000 | |
| 364 6 2059 133 0 468 0227d28e0db6 a3f374fbf33a 000000000000 | |
| 365 | |
| 366 once we get a bundle for this repository and look into it for the same file: | |
| 367 | |
| 368 $hg debugbundle -a /tmp/hg-bundle-4418325145435980614.tmp | |
| 369 format: id, p1, p2, cset, delta base, len(delta) | |
| 370 | |
| 371 build.gradle | |
| 372 62a101b7994c6c5b0423ba6c802f8c64d24ef784 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 6ec4af642ba8024edd636af15e672c97cc3294e4 0000000000000000000000000000000000000000 1368 | |
| 373 b2a1b20d1933d0605aab6780ee52fe5ab3073832 0000000000000000000000000000000000000000 0000000000000000000000000000000000000000 7dcc920e2d57d5850ee9f08ac863251460565bd3 62a101b7994c6c5b0423ba6c802f8c64d24ef784 2373 | |
| 374 5324c8f2b5503a4d1ead3ac40a9851c27572166b b2a1b20d1933d0605aab6780ee52fe5ab3073832 0000000000000000000000000000000000000000 7b883bf03b14ccea8ee74db0a34f9f66ca644a3c b2a1b20d1933d0605aab6780ee52fe5ab3073832 579 | |
| 375 4011d52141cd717c92cbf350a93522d2f3ee415e 5324c8f2b5503a4d1ead3ac40a9851c27572166b 0000000000000000000000000000000000000000 55e9588b84b83aa96fe76a06ee8bf067c5d3c90e 5324c8f2b5503a4d1ead3ac40a9851c27572166b 1147 | |
| 376 d0be588453068787dcb3ee05f8edfe47fdd5ae78 4011d52141cd717c92cbf350a93522d2f3ee415e 0000000000000000000000000000000000000000 ad0322a4af204547c400e1846b2b83d446ab8da5 4011d52141cd717c92cbf350a93522d2f3ee415e 85 | |
| 377 3ddd456244a08f81779163d9faf922a6dcd9e53e d0be588453068787dcb3ee05f8edfe47fdd5ae78 0000000000000000000000000000000000000000 3ace1fc95d0a1a941b6427c60b6e624f96dd71ad d0be588453068787dcb3ee05f8edfe47fdd5ae78 151 | |
| 378 a3f374fbf33aba1cc3b4f472db022b5185880f5d 3ddd456244a08f81779163d9faf922a6dcd9e53e 0000000000000000000000000000000000000000 3ca4ae7bdd3890b8ed89bfea1b42af593e04b373 3ddd456244a08f81779163d9faf922a6dcd9e53e 195 | |
| 379 0227d28e0db69afebee34cd5a4151889fb6271da a3f374fbf33aba1cc3b4f472db022b5185880f5d 0000000000000000000000000000000000000000 31bd09da0dcfe48e1fc662143f91ff402238aa84 a3f374fbf33aba1cc3b4f472db022b5185880f5d 145 | |
| 380 | |
| 381 but there's no delta base information in the bundle file, it's merely a hard-coded convention (always patches previous version, see | |
| 382 (a) changegroup.py#builddeltaheader(): # do nothing with basenode, it is implicitly the previous one in HG10 | |
| 383 (b) revlog.py#group(): prev, curr = revs[r], revs[r + 1] | |
| 384 for c in bundler.revchunk(self, curr, prev): | |
| 385 ) | |
| 386 | |
| 387 | |
| 388 It's unclear where the first chunk (identified 62a101b7...) comes from (by the way, there's no such changeset as 6ec4af... as specified in the chunk, while 7dcc920e.. IS changeset 454) | |
| 389 | |
| 390 EXPLANATION: | |
| 391 if cloned repository comes from svnkit repo (where's the gradle branch): | |
| 392 $hg debugindex build.gradle | |
| 393 rev offset length base linkrev nodeid p1 p2 | |
| 394 0 0 590 0 213 62a101b7994c 000000000000 000000000000 | |
| 395 1 590 872 0 452 b2a1b20d1933 000000000000 000000000000 | |
| 396 2 1462 319 0 453 5324c8f2b550 b2a1b20d1933 000000000000 | |
| 397 3 1781 533 0 459 4011d52141cd 5324c8f2b550 000000000000 | |
| 398 4 2314 85 0 462 d0be58845306 4011d52141cd 000000000000 | |
| 399 5 2399 105 0 466 3ddd456244a0 d0be58845306 000000000000 | |
| 400 6 2504 160 0 468 a3f374fbf33a 3ddd456244a0 000000000000 | |
| 401 7 2664 133 0 470 0227d28e0db6 a3f374fbf33a 000000000000 | |
| 402 | |
| 403 and the aforementioned bundle was result of hg incoming svnkit!!! | |
| 404 */ | |
| 374 | 405 |
| 375 public void start(int count, Callback callback, Object token) { | 406 public void start(int count, Callback callback, Object token) { |
| 376 progressSupport.start(count); | 407 progressSupport.start(count); |
| 377 lifecycleCallback = callback; | 408 lifecycleCallback = callback; |
| 378 } | 409 } |
