comparison src/org/tmatesoft/hg/core/HgLogCommand.java @ 514:5dcb4581c8ef

Report renames when following file history tree with HgFileRenameHandlerMixin
author Artem Tikhomirov <tikhomirov.artem@gmail.com>
date Mon, 17 Dec 2012 19:06:07 +0100
parents 122e0600799f
children e6c8b9b654b2
comparison
equal deleted inserted replaced
513:a41d955dc360 514:5dcb4581c8ef
43 import org.tmatesoft.hg.repo.HgInvalidStateException; 43 import org.tmatesoft.hg.repo.HgInvalidStateException;
44 import org.tmatesoft.hg.repo.HgParentChildMap; 44 import org.tmatesoft.hg.repo.HgParentChildMap;
45 import org.tmatesoft.hg.repo.HgRepository; 45 import org.tmatesoft.hg.repo.HgRepository;
46 import org.tmatesoft.hg.repo.HgRuntimeException; 46 import org.tmatesoft.hg.repo.HgRuntimeException;
47 import org.tmatesoft.hg.repo.HgStatusCollector; 47 import org.tmatesoft.hg.repo.HgStatusCollector;
48 import org.tmatesoft.hg.util.Adaptable;
48 import org.tmatesoft.hg.util.CancelSupport; 49 import org.tmatesoft.hg.util.CancelSupport;
49 import org.tmatesoft.hg.util.CancelledException; 50 import org.tmatesoft.hg.util.CancelledException;
50 import org.tmatesoft.hg.util.Pair; 51 import org.tmatesoft.hg.util.Pair;
51 import org.tmatesoft.hg.util.Path; 52 import org.tmatesoft.hg.util.Path;
52 import org.tmatesoft.hg.util.ProgressSupport; 53 import org.tmatesoft.hg.util.ProgressSupport;
71 private Set<String> branches; 72 private Set<String> branches;
72 private int limit = 0, count = 0; 73 private int limit = 0, count = 0;
73 private int startRev = 0, endRev = TIP; 74 private int startRev = 0, endRev = TIP;
74 private Calendar date; 75 private Calendar date;
75 private Path file; 76 private Path file;
76 private boolean followHistory; // makes sense only when file != null 77 /*
78 * Whether to iterate file origins, if any.
79 * Makes sense only when file != null
80 */
81 private boolean followRenames;
82 /*
83 * Whether to track history of the selected file version (based on file revision
84 * in working dir parent), follow ancestors only.
85 * Note, 'hg log --follow' combines both #followHistory and #followAncestry
86 */
87 private boolean followAncestry;
77 private ChangesetTransformer csetTransform; 88 private ChangesetTransformer csetTransform;
78 private HgParentChildMap<HgChangelog> parentHelper; 89 private HgParentChildMap<HgChangelog> parentHelper;
79 90
80 public HgLogCommand(HgRepository hgRepo) { 91 public HgLogCommand(HgRepository hgRepo) {
81 repo = hgRepo; 92 repo = hgRepo;
182 * @param followCopyRename true to report changesets of the original file(-s), if copy/rename ever occured to the file. 193 * @param followCopyRename true to report changesets of the original file(-s), if copy/rename ever occured to the file.
183 */ 194 */
184 public HgLogCommand file(Path file, boolean followCopyRename) { 195 public HgLogCommand file(Path file, boolean followCopyRename) {
185 // multiple? Bad idea, would need to include extra method into Handler to tell start of next file 196 // multiple? Bad idea, would need to include extra method into Handler to tell start of next file
186 this.file = file; 197 this.file = file;
187 followHistory = followCopyRename; 198 followRenames = followAncestry = followCopyRename;
188 return this; 199 return this;
189 } 200 }
190 201
191 /** 202 /**
192 * Handy analog of {@link #file(Path, boolean)} when clients' paths come from filesystem and need conversion to repository's 203 * Handy analog of {@link #file(Path, boolean)} when clients' paths come from filesystem and need conversion to repository's
254 throw new HgPathNotFoundException(String.format("File %s not found in the repository", file), file); 265 throw new HgPathNotFoundException(String.format("File %s not found in the repository", file), file);
255 } 266 }
256 // FIXME startRev and endRev ARE CHANGESET REVISIONS, not that of FILE!!! 267 // FIXME startRev and endRev ARE CHANGESET REVISIONS, not that of FILE!!!
257 fileNode.history(startRev, endRev, this); 268 fileNode.history(startRev, endRev, this);
258 csetTransform.checkFailure(); 269 csetTransform.checkFailure();
270 final HgFileRenameHandlerMixin withCopyHandler = Adaptable.Factory.getAdapter(handler, HgFileRenameHandlerMixin.class, null);
259 if (fileNode.isCopy()) { 271 if (fileNode.isCopy()) {
260 // even if we do not follow history, report file rename 272 // even if we do not follow history, report file rename
261 do { 273 do {
262 if (handler instanceof HgChangesetHandler.WithCopyHistory) { 274 if (withCopyHandler != null) {
263 HgFileRevision src = new HgFileRevision(repo, fileNode.getCopySourceRevision(), null, fileNode.getCopySourceName()); 275 HgFileRevision src = new HgFileRevision(repo, fileNode.getCopySourceRevision(), null, fileNode.getCopySourceName());
264 HgFileRevision dst = new HgFileRevision(repo, fileNode.getRevision(0), null, fileNode.getPath(), src.getPath()); 276 HgFileRevision dst = new HgFileRevision(repo, fileNode.getRevision(0), null, fileNode.getPath(), src.getPath());
265 ((HgChangesetHandler.WithCopyHistory) handler).copy(src, dst); 277 withCopyHandler.copy(src, dst);
266 } 278 }
267 if (limit > 0 && count >= limit) { 279 if (limit > 0 && count >= limit) {
268 // if limit reach, follow is useless. 280 // if limit reach, follow is useless.
269 break; 281 break;
270 } 282 }
271 if (followHistory) { 283 if (followRenames) {
272 fileNode = repo.getFileNode(fileNode.getCopySourceName()); 284 fileNode = repo.getFileNode(fileNode.getCopySourceName());
273 fileNode.history(this); 285 fileNode.history(this);
274 csetTransform.checkFailure(); 286 csetTransform.checkFailure();
275 } 287 }
276 } while (followHistory && fileNode.isCopy()); 288 } while (followRenames && fileNode.isCopy());
277 } 289 }
278 } 290 }
279 } catch (HgRuntimeException ex) { 291 } catch (HgRuntimeException ex) {
280 throw new HgLibraryFailureException(ex); 292 throw new HgLibraryFailureException(ex);
281 } finally { 293 } finally {
304 if (file == null) { 316 if (file == null) {
305 throw new IllegalArgumentException("History tree is supported for files only (at least now), please specify file"); 317 throw new IllegalArgumentException("History tree is supported for files only (at least now), please specify file");
306 } 318 }
307 final ProgressSupport progressHelper = getProgressSupport(handler); 319 final ProgressSupport progressHelper = getProgressSupport(handler);
308 final CancelSupport cancelHelper = getCancelSupport(handler, true); 320 final CancelSupport cancelHelper = getCancelSupport(handler, true);
321 final HgFileRenameHandlerMixin renameHandler = Adaptable.Factory.getAdapter(handler, HgFileRenameHandlerMixin.class, null);
309 322
310 // builds tree of nodes according to parents in file's revlog 323 // builds tree of nodes according to parents in file's revlog
311 final TreeBuildInspector treeBuildInspector = new TreeBuildInspector(followHistory); 324 final TreeBuildInspector treeBuildInspector = new TreeBuildInspector(followRenames);
312 // we iterate separate histories of each filename, need to connect 325 // we iterate separate histories of each filename, need to connect
313 // last node of historyA with first node of historyB (A renamed to B case) 326 // last node of historyA with first node of historyB (A renamed to B case)
314 // to make overall history smooth. 327 // to make overall history smooth.
315 HistoryNode lastFromPrevIteration = null; 328 HistoryNode lastFromPrevIteration = null;
329 HgFileRevision copiedFrom = null, copiedTo = null;
330 boolean shallReportRenameAfter1Step = false;
316 331
317 final int CACHE_CSET_IN_ADVANCE_THRESHOLD = 100; /* XXX is it really worth it? */ 332 final int CACHE_CSET_IN_ADVANCE_THRESHOLD = 100; /* XXX is it really worth it? */
318 ElementImpl ei = null; 333 ElementImpl ei = null;
319 334
320 // renamed files in the queue are placed with respect to #iterateDirection 335 // renamed files in the queue are placed with respect to #iterateDirection
321 // i.e. if we iterate from new to old, recent filenames come first 336 // i.e. if we iterate from new to old, recent filenames come first
322 LinkedList<Pair<HgDataFile, Nodeid>> fileRenamesQueue = buildFileRenamesQueue(); 337 List<Pair<HgDataFile, Nodeid>> fileRenamesQueue = buildFileRenamesQueue();
323 progressHelper.start(4 * fileRenamesQueue.size()); 338 progressHelper.start(4 * fileRenamesQueue.size());
324 do { 339 for (int namesIndex = 0, renamesQueueSize = fileRenamesQueue.size(); namesIndex < renamesQueueSize; namesIndex++) {
325 340
326 Pair<HgDataFile, Nodeid> renameInfo = fileRenamesQueue.removeFirst(); 341 final Pair<HgDataFile, Nodeid> renameInfo = fileRenamesQueue.get(namesIndex);
327 cancelHelper.checkCancelled(); 342 cancelHelper.checkCancelled();
328 HgDataFile fileNode = renameInfo.first(); 343 final List<HistoryNode> changeHistory = treeBuildInspector.go(renameInfo.first(), renameInfo.second());
329 Nodeid fileLastRevToVisit = null;
330 if (followHistory) {
331 fileLastRevToVisit = renameInfo.second();
332 if (fileLastRevToVisit == null) {
333 // it's either first or last item in the queue, depending on iteration order
334 assert fileRenamesQueue.isEmpty() || /*awful way to find out it's first iteration*/ lastFromPrevIteration == null;
335 // TODO subject to dedicated method either in HgRepository (getWorkingCopyParentRevisionIndex)
336 // or in the HgDataFile (getWorkingCopyOriginRevision)
337 Nodeid wdParentChangeset = repo.getWorkingCopyParents().first();
338 if (!wdParentChangeset.isNull()) {
339 int wdParentRevIndex = repo.getChangelog().getRevisionIndex(wdParentChangeset);
340 fileLastRevToVisit = repo.getManifest().getFileRevision(wdParentRevIndex, fileNode.getPath());
341 }
342 // else fall-through, assume lastRevision() is ok here
343 }
344 }
345 int fileLastRevIndexToVisit = fileLastRevToVisit == null ? fileNode.getLastRevision() : fileNode.getRevisionIndex(fileLastRevToVisit);
346 final List<HistoryNode> changeHistory = treeBuildInspector.go(fileNode, fileLastRevIndexToVisit);
347 assert changeHistory.size() > 0; 344 assert changeHistory.size() > 0;
348 progressHelper.worked(1); 345 progressHelper.worked(1);
349 cancelHelper.checkCancelled(); 346 cancelHelper.checkCancelled();
350 final ProgressSupport ph2; 347 final ProgressSupport ph2;
351 if (ei == null) { 348 if (ei == null) {
368 if (lastFromPrevIteration != null) { 365 if (lastFromPrevIteration != null) {
369 if (iterateDirection == IterateDirection.FromOldToNew) { 366 if (iterateDirection == IterateDirection.FromOldToNew) {
370 // forward, from old to new: 367 // forward, from old to new:
371 // A(0..n) -> B(0..m). First, report A(0)..A(n-1) 368 // A(0..n) -> B(0..m). First, report A(0)..A(n-1)
372 // then A(n).bind(B(0)) 369 // then A(n).bind(B(0))
373 HistoryNode oldestOfTheNextChunk = changeHistory.get(0); 370 HistoryNode oldestOfTheNextChunk = changeHistory.get(0); // B(0)
374 lastFromPrevIteration.bindChild(oldestOfTheNextChunk); 371 lastFromPrevIteration.bindChild(oldestOfTheNextChunk); // lastFromPrevIteration is A(n)
375 changeHistory.add(0, lastFromPrevIteration); 372 changeHistory.add(0, lastFromPrevIteration);
373 if (renameHandler != null) { // shall report renames
374 assert namesIndex > 0;
375 HgDataFile lastIterationFileNode = fileRenamesQueue.get(namesIndex-1).first(); // A
376 copiedFrom = new HgFileRevision(lastIterationFileNode, lastFromPrevIteration.fileRevision, null);
377 copiedTo = new HgFileRevision(renameInfo.first(), oldestOfTheNextChunk.fileRevision, copiedFrom.getPath());
378 shallReportRenameAfter1Step = true; // report rename after A(n)
379 }
376 } else { 380 } else {
377 assert iterateDirection == IterateDirection.FromNewToOld; 381 assert iterateDirection == IterateDirection.FromNewToOld;
378 // A renamed to B. A(0..n) -> B(0..m). 382 // A renamed to B. A(0..n) -> B(0..m).
379 // First, report B(m), B(m-1)...B(1), then A(n).bind(B(0)) 383 // First, report B(m), B(m-1)...B(1), then A(n).bind(B(0)), report B(0), A(n)...
380 HistoryNode newestOfNextChunk = changeHistory.get(changeHistory.size() - 1); // A(n) 384 HistoryNode newestOfNextChunk = changeHistory.get(changeHistory.size() - 1); // A(n)
381 newestOfNextChunk.bindChild(lastFromPrevIteration); 385 newestOfNextChunk.bindChild(lastFromPrevIteration);
382 changeHistory.add(lastFromPrevIteration); 386 changeHistory.add(lastFromPrevIteration);
383 } 387 if (renameHandler != null) {
384 } 388 assert namesIndex > 0;
385 if (!fileRenamesQueue.isEmpty()) { 389 // renameInfo points to chunk of name A now, and lastFromPrevIteration (from namesIndex-1) is B
390 copiedFrom = new HgFileRevision(renameInfo.first(), newestOfNextChunk.fileRevision, null);
391 HgDataFile lastIterationFileNode = fileRenamesQueue.get(namesIndex-1).first(); // B
392 copiedTo = new HgFileRevision(lastIterationFileNode, lastFromPrevIteration.fileRevision, copiedFrom.getPath());
393 shallReportRenameAfter1Step = true; // report rename after B(0)
394 }
395 }
396 }
397 if (namesIndex + 1 < renamesQueueSize) {
398 // there's at least one more name we are going to look at, save
399 // one element for later binding
400 //
386 if (iterateDirection == IterateDirection.FromOldToNew) { 401 if (iterateDirection == IterateDirection.FromOldToNew) {
387 // save newest, and exclude it from this iteration (postpone for next) 402 // save newest, and exclude it from this iteration (postpone for next)
388 lastFromPrevIteration = changeHistory.remove(changeHistory.size()-1); 403 lastFromPrevIteration = changeHistory.remove(changeHistory.size()-1);
389 } else { 404 } else {
390 assert iterateDirection == IterateDirection.FromNewToOld; 405 assert iterateDirection == IterateDirection.FromNewToOld;
391 // save oldest, and exclude it from thi iteration (postpone for next) 406 // save oldest, and exclude it from this iteration (postpone for next)
392 lastFromPrevIteration = changeHistory.remove(0); 407 lastFromPrevIteration = changeHistory.remove(0);
393 } 408 }
394 } else { 409 } else {
395 lastFromPrevIteration = null; // just for the sake of no references to old items 410 lastFromPrevIteration = null; // just for the sake of no references to old items
396 } 411 }
405 while(it.hasNext()) { 420 while(it.hasNext()) {
406 HistoryNode n = it.next(); 421 HistoryNode n = it.next();
407 handler.treeElement(ei.init(n)); 422 handler.treeElement(ei.init(n));
408 ph2.worked(1); 423 ph2.worked(1);
409 cancelHelper.checkCancelled(); 424 cancelHelper.checkCancelled();
410 } 425 if (shallReportRenameAfter1Step) {
411 } while (!fileRenamesQueue.isEmpty()); 426 assert renameHandler != null;
427 assert copiedFrom != null;
428 assert copiedTo != null;
429 renameHandler.copy(copiedFrom, copiedTo);
430 shallReportRenameAfter1Step = false;
431 copiedFrom = copiedTo = null;
432 }
433 }
434 } // for fileRenamesQueue;
412 progressHelper.done(); 435 progressHelper.done();
413 } 436 }
414 437
415 private IterateDirection iterateDirection = IterateDirection.FromOldToNew; 438 private IterateDirection iterateDirection = IterateDirection.FromOldToNew;
416 439
434 457
435 /** 458 /**
436 * Follows file renames and build a list of all corresponding file nodes and revisions they were 459 * Follows file renames and build a list of all corresponding file nodes and revisions they were
437 * copied/renamed/branched at (IOW, their latest revision to look at). 460 * copied/renamed/branched at (IOW, their latest revision to look at).
438 * 461 *
439 * If {@link #followHistory} is <code>false</code>, the list contains one element only, 462 * If {@link #followRenames} is <code>false</code>, the list contains one element only,
440 * file node with the name of the file as it was specified by the user. 463 * file node with the name of the file as it was specified by the user.
441 * 464 *
442 * For the most recent file revision is null. 465 * For the most recent file revision depends on {@link #followAncestry}, and is file revision from working copy parent
466 * in it's true. <code>null</code> indicates file's TIP revision shall be used.
443 * 467 *
444 * TODO may use HgFileRevision (after some refactoring to accept HgDataFile and Nodeid) instead of Pair 468 * TODO may use HgFileRevision (after some refactoring to accept HgDataFile and Nodeid) instead of Pair
445 * and possibly reuse this functionality 469 * and possibly reuse this functionality
446 * 470 *
447 * @return list of file renames, ordered with respect to {@link #iterateDirection} 471 * @return list of file renames, ordered with respect to {@link #iterateDirection}
448 */ 472 */
449 private LinkedList<Pair<HgDataFile, Nodeid>> buildFileRenamesQueue() { 473 private List<Pair<HgDataFile, Nodeid>> buildFileRenamesQueue() {
450 LinkedList<Pair<HgDataFile, Nodeid>> rv = new LinkedList<Pair<HgDataFile, Nodeid>>(); 474 LinkedList<Pair<HgDataFile, Nodeid>> rv = new LinkedList<Pair<HgDataFile, Nodeid>>();
451 if (!followHistory) { 475 Nodeid startRev = null;
452 rv.add(new Pair<HgDataFile, Nodeid>(repo.getFileNode(file), null)); 476 HgDataFile fileNode = repo.getFileNode(file);
477 if (followAncestry) {
478 // TODO subject to dedicated method either in HgRepository (getWorkingCopyParentRevisionIndex)
479 // or in the HgDataFile (getWorkingCopyOriginRevision)
480 Nodeid wdParentChangeset = repo.getWorkingCopyParents().first();
481 if (!wdParentChangeset.isNull()) {
482 int wdParentRevIndex = repo.getChangelog().getRevisionIndex(wdParentChangeset);
483 startRev = repo.getManifest().getFileRevision(wdParentRevIndex, fileNode.getPath());
484 }
485 // else fall-through, assume null (eventually, lastRevision()) is ok here
486 }
487 rv.add(new Pair<HgDataFile, Nodeid>(fileNode, startRev));
488 if (!followRenames) {
453 return rv; 489 return rv;
454 } 490 }
455 Path fp = file; 491 while (fileNode.isCopy()) {
456 Nodeid copyRev = null; 492 Path fp = fileNode.getCopySourceName();
457 boolean isCopy; 493 Nodeid copyRev = fileNode.getCopySourceRevision();
458 do { 494 fileNode = repo.getFileNode(fp);
459 HgDataFile fileNode = repo.getFileNode(fp);
460 Pair<HgDataFile, Nodeid> p = new Pair<HgDataFile, Nodeid>(fileNode, copyRev); 495 Pair<HgDataFile, Nodeid> p = new Pair<HgDataFile, Nodeid>(fileNode, copyRev);
461 if (iterateDirection == IterateDirection.FromOldToNew) { 496 if (iterateDirection == IterateDirection.FromOldToNew) {
462 rv.addFirst(p); 497 rv.addFirst(p);
463 } else { 498 } else {
464 assert iterateDirection == IterateDirection.FromNewToOld; 499 assert iterateDirection == IterateDirection.FromNewToOld;
465 rv.addLast(p); 500 rv.addLast(p);
466 } 501 }
467 if (isCopy = fileNode.isCopy()) { 502 };
468 fp = fileNode.getCopySourceName();
469 copyRev = fileNode.getCopySourceRevision();
470 }
471 } while (isCopy);
472 return rv; 503 return rv;
473 } 504 }
474 505
475 private static class TreeBuildInspector implements HgChangelog.ParentInspector, HgChangelog.RevisionInspector { 506 private static class TreeBuildInspector implements HgChangelog.ParentInspector, HgChangelog.RevisionInspector {
476 private final boolean followAncestry; 507 private final boolean followAncestry;
504 * lastRevisionIndex would be included. 535 * lastRevisionIndex would be included.
505 * 536 *
506 * @return list of history elements, from oldest to newest. In case {@link #followAncestry} is <code>true</code>, the list 537 * @return list of history elements, from oldest to newest. In case {@link #followAncestry} is <code>true</code>, the list
507 * is modifiable (to further augment with last/first elements of renamed file histories) 538 * is modifiable (to further augment with last/first elements of renamed file histories)
508 */ 539 */
509 List<HistoryNode> go(HgDataFile fileNode, int lastRevisionIndex) throws HgInvalidControlFileException { 540 List<HistoryNode> go(HgDataFile fileNode, Nodeid fileLastRevisionToVisit) throws HgInvalidControlFileException {
510 resultHistory = null; 541 resultHistory = null;
511 completeHistory = new HistoryNode[lastRevisionIndex+1]; 542 int fileLastRevIndexToVisit = fileLastRevisionToVisit == null ? fileNode.getLastRevision() : fileNode.getRevisionIndex(fileLastRevisionToVisit);
543 completeHistory = new HistoryNode[fileLastRevIndexToVisit+1];
512 commitRevisions = new int[completeHistory.length]; 544 commitRevisions = new int[completeHistory.length];
513 fileNode.indexWalk(0, lastRevisionIndex, this); 545 fileNode.indexWalk(0, fileLastRevIndexToVisit, this);
514 if (!followAncestry) { 546 if (!followAncestry) {
515 // in case when ancestor not followed, it's safe to return unmodifiable list 547 // in case when ancestor not followed, it's safe to return unmodifiable list
516 resultHistory = Arrays.asList(completeHistory); 548 resultHistory = Arrays.asList(completeHistory);
517 completeHistory = null; 549 completeHistory = null;
518 // keep commitRevisions initialized, no need to recalculate them 550 // keep commitRevisions initialized, no need to recalculate them
537 // strippedHistory: only those HistoryNodes from completeHistory that are on the same 569 // strippedHistory: only those HistoryNodes from completeHistory that are on the same
538 // line of descendant, in order from older to newer 570 // line of descendant, in order from older to newer
539 LinkedList<HistoryNode> strippedHistoryList = new LinkedList<HistoryNode>(); 571 LinkedList<HistoryNode> strippedHistoryList = new LinkedList<HistoryNode>();
540 LinkedList<HistoryNode> queue = new LinkedList<HistoryNode>(); 572 LinkedList<HistoryNode> queue = new LinkedList<HistoryNode>();
541 // look for ancestors of the selected history node 573 // look for ancestors of the selected history node
542 queue.add(completeHistory[lastRevisionIndex]); 574 queue.add(completeHistory[fileLastRevIndexToVisit]);
543 do { 575 do {
544 HistoryNode withFileChange = queue.removeFirst(); 576 HistoryNode withFileChange = queue.removeFirst();
545 if (strippedHistoryList.contains(withFileChange)) { 577 if (strippedHistoryList.contains(withFileChange)) {
546 // fork point for the change that was later merged (and we traced 578 // fork point for the change that was later merged (and we traced
547 // both lines of development by now. 579 // both lines of development by now.