Mercurial > hg4j
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 } |