Mercurial > jhg
changeset 691:72fc7774b87e
Fix file.isCopy() for blame/annotate. Refactor status and blame to use newly introduced FileHistory helper that builds file rename history
author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
---|---|
date | Fri, 02 Aug 2013 23:07:23 +0200 |
parents | b286222158be |
children | e970b333f284 |
files | src/org/tmatesoft/hg/core/HgChangesetFileSneaker.java src/org/tmatesoft/hg/internal/FileHistory.java src/org/tmatesoft/hg/internal/FileRenameHistory.java src/org/tmatesoft/hg/internal/FileRevisionHistoryChunk.java src/org/tmatesoft/hg/repo/HgStatusCollector.java test-data/test-repos.jar test/org/tmatesoft/hg/test/TestFileRenameUtils.java test/org/tmatesoft/hg/test/TestHistory.java |
diffstat | 8 files changed, 409 insertions(+), 147 deletions(-) [+] |
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/core/HgChangesetFileSneaker.java Thu Aug 01 21:45:47 2013 +0200 +++ b/src/org/tmatesoft/hg/core/HgChangesetFileSneaker.java Fri Aug 02 23:07:23 2013 +0200 @@ -152,7 +152,7 @@ if (csetIndex > csetFileEnds) { return new Outcome(Outcome.Kind.Success, String.format("%s: last known changeset for the file %s is %d. Follow renames is possible towards older changesets only", phaseMsg, file, csetFileEnds)); } - // XXX code is similar to that in HgStatusCollector#getOriginIfCopy. Why it's different in lastOrigin processing then? + // @see FileRenameHistory, with similar code, which doesn't trace alternative paths // traceback stack keeps record of all files with isCopy(fileRev) == true we've tried to follow, so that we can try earlier file // revisions in case followed fileRev didn't succeed ArrayDeque<Pair<HgDataFile, Integer>> traceback = new ArrayDeque<Pair<HgDataFile, Integer>>();
--- a/src/org/tmatesoft/hg/internal/FileHistory.java Thu Aug 01 21:45:47 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/FileHistory.java Fri Aug 02 23:07:23 2013 +0200 @@ -17,14 +17,15 @@ package org.tmatesoft.hg.internal; import static org.tmatesoft.hg.core.HgIterateDirection.NewToOld; +import static org.tmatesoft.hg.core.HgIterateDirection.OldToNew; import java.util.Collections; import java.util.LinkedList; import org.tmatesoft.hg.core.HgIterateDirection; import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.FileRenameHistory.Chunk; import org.tmatesoft.hg.repo.HgDataFile; -import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.repo.HgRuntimeException; /** @@ -58,42 +59,28 @@ } public void build() throws HgRuntimeException { - assert fileCompleteHistory.isEmpty(); - HgDataFile currentFile = df; - final int changelogRevIndexEnd = csetTo; - final int changelogRevIndexStart = csetFrom; - int fileLastClogRevIndex = changelogRevIndexEnd; - FileRevisionHistoryChunk nextChunk = null; fileCompleteHistory.clear(); // just in case, #build() is not expected to be called more than once - do { - FileRevisionHistoryChunk fileHistory = new FileRevisionHistoryChunk(currentFile); - fileHistory.init(fileLastClogRevIndex); - fileHistory.linkTo(nextChunk); - fileCompleteHistory.addFirst(fileHistory); // to get the list in old-to-new order - nextChunk = fileHistory; - if (fileHistory.changeset(0) > changelogRevIndexStart && currentFile.isCopy()) { - // fileHistory.changeset(0) is the earliest revision we know about so far, - // once we get to revisions earlier than the requested start, stop digging. - // The reason there's NO == (i.e. not >=) because: - // (easy): once it's equal, we've reached our intended start - // (hard): if changelogRevIndexStart happens to be exact start of one of renames in the - // chain of renames (test-annotate2 repository, file1->file1a->file1b, i.e. points - // to the very start of file1a or file1 history), presence of == would get us to the next - // chunk and hence changed parents of present chunk's first element. Our annotate alg - // relies on parents only (i.e. knows nothing about 'last iteration element') to find out - // what to compare, and hence won't report all lines of 'last iteration element' (which is the - // first revision of the renamed file) as "added in this revision", leaving gaps in annotate - HgRepository repo = currentFile.getRepo(); - Nodeid originLastRev = currentFile.getCopySourceRevision(); - currentFile = repo.getFileNode(currentFile.getCopySourceName()); - fileLastClogRevIndex = currentFile.getChangesetRevisionIndex(currentFile.getRevisionIndex(originLastRev)); - // XXX perhaps, shall fail with meaningful exception if new file doesn't exist (.i/.d not found for whatever reason) - // or source revision is missing? - } else { - fileHistory.chopAtChangeset(changelogRevIndexStart); - currentFile = null; // stop iterating + Nodeid fileRev = df.getRepo().getManifest().getFileRevision(csetTo, df.getPath()); + int fileRevIndex = df.getRevisionIndex(fileRev); + FileRenameHistory frh = new FileRenameHistory(csetFrom, csetTo); + if (frh.isOutOfRange(df, fileRevIndex)) { + return; + } + frh.build(df, fileRevIndex); + FileRevisionHistoryChunk prevChunk = null; + for (Chunk c : frh.iterate(OldToNew)) { + FileRevisionHistoryChunk fileHistory = new FileRevisionHistoryChunk(c.file(), c.firstCset(), c.lastCset(), c.firstFileRev(), c.lastFileRev()); + fileHistory.init(); + if (fileHistory.revisionCount() == 0) { + // no revisions on our cset range of interest + continue; } - } while (currentFile != null && fileLastClogRevIndex > changelogRevIndexStart); + if (prevChunk != null) { + prevChunk.linkTo(fileHistory); + } + fileCompleteHistory.addLast(fileHistory); // to get the list in old-to-new order + prevChunk = fileHistory; + } // fileCompleteHistory is in (origin, intermediate target, ultimate target) order }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/FileRenameHistory.java Fri Aug 02 23:07:23 2013 +0200 @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2013 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.internal; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.tmatesoft.hg.core.HgFileRevision; +import org.tmatesoft.hg.core.HgIterateDirection; +import org.tmatesoft.hg.repo.HgDataFile; + +/** + * Traces file renames. Quite similar to HgChangesetFileSneaker, although the latter tries different paths + * to find origin names, while this class traces first renames found only. + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public final class FileRenameHistory { + + private final int csetFrom; + private final int csetTo; + private final List<Chunk> history; + + public FileRenameHistory(int csetStartIndex, int csetEndIndex) { + csetFrom = csetStartIndex; + csetTo = csetEndIndex; + history = new ArrayList<Chunk>(3); + } + + public int startChangeset() { + return csetFrom; + } + + public int endChangeset() { + return csetTo; + } + + public boolean isOutOfRange(HgDataFile df, int fileRev) { + return df.getChangesetRevisionIndex(fileRev) < csetFrom || df.getChangesetRevisionIndex(0) > csetTo; + } + + public void build(HgDataFile df, int fileRev) { + assert !isOutOfRange(df, fileRev); + LinkedList<Chunk> chunks = new LinkedList<Chunk>(); + int chunkStart = 0, chunkEnd = fileRev; + int csetChunkEnd = -1, csetChunkStart = -1; + while (fileRev >= 0) { + int cset = df.getChangesetRevisionIndex(fileRev); + if (csetChunkEnd == -1) { + csetChunkEnd = cset; + } + if (cset <= csetFrom) { + chunkStart = fileRev; + csetChunkStart = csetFrom; + break; + } + if (cset > csetTo) { + chunkEnd = --fileRev; + csetChunkEnd = -1; + continue; + } + csetChunkStart = cset; + if (df.isCopy(fileRev)) { + chunks.addFirst(new Chunk(df, fileRev, chunkEnd, csetChunkStart, csetChunkEnd)); + HgFileRevision origin = df.getCopySource(fileRev); + df = df.getRepo().getFileNode(origin.getPath()); + fileRev = chunkEnd = df.getRevisionIndex(origin.getRevision()); + chunkStart = 0; + csetChunkEnd = cset - 1; // if df is copy, cset can't be 0 + csetChunkStart = -1; + } else { + fileRev--; + } + } + assert chunkStart >= 0; + assert chunkEnd >= 0; // can be negative only if df.cset(0) > csetTo + assert csetChunkEnd >= 0; + assert csetChunkStart >= 0; + chunks.addFirst(new Chunk(df, chunkStart, chunkEnd, csetChunkStart, csetChunkEnd)); + + history.clear(); + history.addAll(chunks); + } + + public Iterable<Chunk> iterate(HgIterateDirection order) { + if (order == HgIterateDirection.NewToOld) { + return ReverseIterator.reversed(history); + } + assert order == HgIterateDirection.OldToNew; + return Collections.unmodifiableList(history); + } + + public int chunks() { + return history.size(); + } + + public Chunk chunkAt(int cset) { + if (cset < csetFrom || cset > csetTo) { + return null; + } + for (Chunk c : history) { + if (c.firstCset() > cset) { + break; + } + if (cset <= c.lastCset()) { + return c; + } + } + return null; + } + + + /** + * file has changes [firstFileRev..lastFileRev] that have occurred somewhere in [firstCset..lastCset] + */ + public static class Chunk { + private final HgDataFile df; + private final int fileRevFrom; + private final int fileRevTo; + private final int csetFrom; + private final int csetTo; + Chunk(HgDataFile file, int fileRevStart, int fileRevEnd, int csetStart, int csetEnd) { + df = file; + fileRevFrom = fileRevStart; + fileRevTo = fileRevEnd; + csetFrom = csetStart; + csetTo = csetEnd; + } + public HgDataFile file() { + return df; + } + public int firstFileRev() { + return fileRevFrom; + } + public int lastFileRev() { + return fileRevTo; + } + public int firstCset() { + return csetFrom; + } + public int lastCset() { + return csetTo; + } + } +}
--- a/src/org/tmatesoft/hg/internal/FileRevisionHistoryChunk.java Thu Aug 01 21:45:47 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/FileRevisionHistoryChunk.java Fri Aug 02 23:07:23 2013 +0200 @@ -25,7 +25,6 @@ import java.util.LinkedList; import org.tmatesoft.hg.core.HgIterateDirection; -import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.repo.HgDataFile; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.repo.HgRuntimeException; @@ -41,16 +40,22 @@ private final HgDataFile df; // change ancestry, sequence of file revisions private IntVector fileRevsToVisit; - // parent pairs of complete file history + // parent pairs of complete file history, index offset by fileRevFrom private IntVector fileParentRevs; - // map file revision to changelog revision (sparse array, only file revisions to visit are set) + // map file revision to changelog revision (sparse array, only file revisions to visit are set), index offset by fileRevFrom private int[] file2changelog; private int originChangelogRev = BAD_REVISION, originFileRev = BAD_REVISION; - private int csetRangeStart = NO_REVISION, csetRangeEnd = BAD_REVISION; + private final int csetFrom, csetTo; + private final int fileRevFrom, fileRevTo; - public FileRevisionHistoryChunk(HgDataFile file) { + public FileRevisionHistoryChunk(HgDataFile file, int csetStart, int csetEnd, int fileStart, int fileEnd) { + assert fileEnd >= fileStart; df = file; + csetFrom = csetStart; + csetTo = csetEnd; + fileRevFrom = fileStart; + fileRevTo = fileEnd; } /** @@ -64,52 +69,57 @@ * @return changeset this file history chunk was chopped at, or {@link HgRepository#NO_REVISION} if none specified */ public int getStartChangeset() { - return csetRangeStart; + return csetFrom; } /** * @return changeset this file history chunk ends at */ public int getEndChangeset() { - return csetRangeEnd; + return csetTo; } - public void init(int changelogRevisionIndex) throws HgRuntimeException { - csetRangeEnd = changelogRevisionIndex; - // XXX df.indexWalk(0, fileRevIndex, ) might be more effective - Nodeid fileRev = df.getRepo().getManifest().getFileRevision(changelogRevisionIndex, df.getPath()); - int fileRevIndex = df.getRevisionIndex(fileRev); + public void init() throws HgRuntimeException { int[] fileRevParents = new int[2]; - fileParentRevs = new IntVector((fileRevIndex+1) * 2, 0); - fileParentRevs.add(NO_REVISION, NO_REVISION); // parents of fileRevIndex == 0 - for (int i = 1; i <= fileRevIndex; i++) { + final int totalFileRevs = fileRevTo - fileRevFrom + 1; + fileParentRevs = new IntVector(totalFileRevs * 2, 0); + // pretend parents of fileRevStart are not set, regardless of actual state as we are not going to visit them anyway + fileParentRevs.add(NO_REVISION, NO_REVISION); + // XXX df.indexWalk(fileRevStart, fileRevEnd, ) might be more effective + for (int i = fileRevFrom+1; i <= fileRevTo; i++) { df.parents(i, fileRevParents, null, null); fileParentRevs.add(fileRevParents[0], fileRevParents[1]); } // fileRevsToVisit keep file change ancestry from new to old - fileRevsToVisit = new IntVector(fileRevIndex + 1, 0); + fileRevsToVisit = new IntVector(totalFileRevs, 0); // keep map of file revision to changelog revision - file2changelog = new int[fileRevIndex+1]; + file2changelog = new int[totalFileRevs]; // only elements worth visit would get mapped, so there would be unfilled areas in the file2changelog, // prevent from error (make it explicit) by bad value Arrays.fill(file2changelog, BAD_REVISION); LinkedList<Integer> queue = new LinkedList<Integer>(); - BitSet seen = new BitSet(fileRevIndex + 1); - queue.add(fileRevIndex); + BitSet seen = new BitSet(totalFileRevs); + queue.add(fileRevTo); do { - int x = queue.removeFirst(); - if (seen.get(x)) { + int fileRev = queue.removeFirst(); + int offFileRev = fileRev - fileRevFrom; + if (seen.get(offFileRev)) { continue; } - seen.set(x); - fileRevsToVisit.add(x); - file2changelog[x] = df.getChangesetRevisionIndex(x); - int p1 = fileParentRevs.get(2*x); - int p2 = fileParentRevs.get(2*x + 1); - if (p1 != NO_REVISION) { + seen.set(offFileRev); + int csetRev = df.getChangesetRevisionIndex(fileRev); + if (csetRev < csetFrom || csetRev > csetTo) { + continue; + } + fileRevsToVisit.add(fileRev); + + file2changelog[offFileRev] = csetRev; + int p1 = fileParentRevs.get(2*offFileRev); + int p2 = fileParentRevs.get(2*offFileRev + 1); + if (p1 != NO_REVISION && p1 >= fileRevFrom) { queue.addLast(p1); } - if (p2 != NO_REVISION) { + if (p2 != NO_REVISION && p2 >= fileRevFrom) { queue.addLast(p2); } } while (!queue.isEmpty()); @@ -117,37 +127,13 @@ fileRevsToVisit.sort(false); } - public void linkTo(FileRevisionHistoryChunk target) { - // assume that target.init() has been called already - if (target == null) { + public void linkTo(FileRevisionHistoryChunk next) { + // assume that init() has been called already + if (next == null) { return; } - target.originFileRev = fileRevsToVisit.get(0); // files to visit are new to old - target.originChangelogRev = changeset(target.originFileRev); - } - - /** - * Mark revision closest(ceil) to specified as the very first one (no parents) - */ - public void chopAtChangeset(int firstChangelogRevOfInterest) { - csetRangeStart = firstChangelogRevOfInterest; - if (firstChangelogRevOfInterest == 0) { - return; // nothing to do - } - int i = 0, x = fileRevsToVisit.size(), fileRev = BAD_REVISION; - // fileRevsToVisit is new to old, greater numbers to smaller - while (i < x && changeset(fileRev = fileRevsToVisit.get(i)) >= firstChangelogRevOfInterest) { - i++; - } - assert fileRev != BAD_REVISION; // there's at least 1 revision in fileRevsToVisit - if (i == x && changeset(fileRev) != firstChangelogRevOfInterest) { - assert false : "Requested changeset shall belong to the chunk"; - return; - } - fileRevsToVisit.trimTo(i); // no need to iterate more - // pretend fileRev got no parents - fileParentRevs.set(fileRev * 2, NO_REVISION); - fileParentRevs.set(fileRev, NO_REVISION); + next.originFileRev = fileRevsToVisit.get(0); // files to visit are new to old + next.originChangelogRev = changeset(next.originFileRev); } public int[] fileRevisions(HgIterateDirection iterateOrder) { @@ -172,30 +158,32 @@ } public int changeset(int fileRevIndex) { - return file2changelog[fileRevIndex]; + return file2changelog[fileRevIndex - fileRevFrom]; } public void fillFileParents(int fileRevIndex, int[] fileParents) { - if (fileRevIndex == 0 && originFileRev != BAD_REVISION) { + if (fileRevIndex == fileRevFrom && originFileRev != BAD_REVISION) { // this chunk continues another file assert originFileRev != NO_REVISION; fileParents[0] = originFileRev; fileParents[1] = NO_REVISION; return; } - fileParents[0] = fileParentRevs.get(fileRevIndex * 2); - fileParents[1] = fileParentRevs.get(fileRevIndex * 2 + 1); + int x = fileRevIndex - fileRevFrom; + fileParents[0] = fileParentRevs.get(x * 2); + fileParents[1] = fileParentRevs.get(x * 2 + 1); } public void fillCsetParents(int fileRevIndex, int[] csetParents) { - if (fileRevIndex == 0 && originFileRev != BAD_REVISION) { - assert originFileRev != NO_REVISION; + if (fileRevIndex == fileRevFrom && originFileRev != BAD_REVISION) { + assert originChangelogRev != NO_REVISION; csetParents[0] = originChangelogRev; csetParents[1] = NO_REVISION; // I wonder if possible to start a copy with two parents? return; } - int fp1 = fileParentRevs.get(fileRevIndex * 2); - int fp2 = fileParentRevs.get(fileRevIndex * 2 + 1); + int x = fileRevIndex - fileRevFrom; + int fp1 = fileParentRevs.get(x * 2); + int fp2 = fileParentRevs.get(x * 2 + 1); csetParents[0] = fp1 == NO_REVISION ? NO_REVISION : changeset(fp1); csetParents[1] = fp2 == NO_REVISION ? NO_REVISION : changeset(fp2); }
--- a/src/org/tmatesoft/hg/repo/HgStatusCollector.java Thu Aug 01 21:45:47 2013 +0200 +++ b/src/org/tmatesoft/hg/repo/HgStatusCollector.java Fri Aug 02 23:07:23 2013 +0200 @@ -26,8 +26,9 @@ import java.util.Map; import java.util.TreeSet; -import org.tmatesoft.hg.core.HgFileRevision; import org.tmatesoft.hg.core.Nodeid; +import org.tmatesoft.hg.internal.FileRenameHistory; +import org.tmatesoft.hg.internal.FileRenameHistory.Chunk; import org.tmatesoft.hg.internal.IntMap; import org.tmatesoft.hg.internal.ManifestRevision; import org.tmatesoft.hg.internal.Pool; @@ -376,45 +377,21 @@ assert fnameRev != null; assert !Nodeid.NULL.equals(fnameRev); int fileRevIndex = fnameRev == null ? 0 : df.getRevisionIndex(fnameRev); - Path lastOriginFound = null; - while(fileRevIndex >=0) { - if (!df.isCopy(fileRevIndex)) { - fileRevIndex--; - continue; - } - int csetRevIndex = df.getChangesetRevisionIndex(fileRevIndex); - if (csetRevIndex <= originalChangesetIndex) { - // we've walked past originalChangelogRevIndex and no chances we'll find origin - // if we get here, it means either fname's origin is not from the base revision - // or the last found rename is still valid - return lastOriginFound; - } - HgFileRevision origin = df.getCopySource(fileRevIndex); - // prepare for the next step, df(copyFromFileRev) would point to copy origin and its revision - df = hgRepo.getFileNode(origin.getPath()); - int copyFromFileRevIndex = df.getRevisionIndex(origin.getRevision()); - if (originals.contains(origin.getPath())) { - int copyFromCsetIndex = df.getChangesetRevisionIndex(copyFromFileRevIndex); - if (copyFromCsetIndex <= originalChangesetIndex) { - // copy/rename source was known prior to rev1 - // (both r1Files.contains is true and original was created earlier than rev1) - // without r1Files.contains changelogRevision <= rev1 won't suffice as the file - // might get removed somewhere in between (changelogRevision < R < rev1) - return origin.getPath(); - } - // copy/rename happened in [copyFromCsetIndex..target], let's see if - // origin wasn't renamed once more in [originalChangesetIndex..copyFromCsetIndex] - lastOriginFound = origin.getPath(); - // FALL-THROUGH - } else { - // clear last known origin if the file was renamed once again to something we don't have in base - lastOriginFound = null; - } - // try more steps away - // copyFromFileRev or one of its predecessors might be copies as well - fileRevIndex = copyFromFileRevIndex; // df is already origin file + FileRenameHistory frh = new FileRenameHistory(originalChangesetIndex, df.getChangesetRevisionIndex(fileRevIndex)); + if (frh.isOutOfRange(df, fileRevIndex)) { + return null; } - return lastOriginFound; + frh.build(df, fileRevIndex); + Chunk c = frh.chunkAt(originalChangesetIndex); + if (c == null) { + // file rename history doesn't go deep up to changeset of interest + return null; + } + Path nameAtOrigin = c.file().getPath(); + if (originals.contains(nameAtOrigin)) { + return nameAtOrigin; + } + return null; } // XXX for r1..r2 status, only modified, added, removed (and perhaps, clean) make sense
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/org/tmatesoft/hg/test/TestFileRenameUtils.java Fri Aug 02 23:07:23 2013 +0200 @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2013 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.test; + +import static org.tmatesoft.hg.core.HgIterateDirection.NewToOld; +import static org.tmatesoft.hg.core.HgIterateDirection.OldToNew; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.tmatesoft.hg.core.HgChangesetFileSneaker; +import org.tmatesoft.hg.core.HgException; +import org.tmatesoft.hg.core.HgIterateDirection; +import org.tmatesoft.hg.internal.FileRenameHistory; +import org.tmatesoft.hg.internal.FileRenameHistory.Chunk; +import org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgRepository; + +/** + * TODO add tests for {@link HgChangesetFileSneaker} + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class TestFileRenameUtils { + + + @Rule + public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); + + @Test + public void testFileRenameHistory() throws HgException { + HgRepository repo = Configuration.get().find("log-renames"); + // history of files from their TIP perspective + // series of (fname, fileRefStart, fileRevEnd, csetStart, csetEnd), new to old + Object[] historyA = new Object[] {"a", 2, 2, 5, 5, "c", 0, 1, 2, 4, "b", 0, 1, 0, 1}; + Object[] historyB = new Object[] {"b", 2, 3, 3, 4, "a", 0, 1, 0, 2 }; + Object[] historyC = new Object[] {"c", 0, 1, 2, 4, "b", 0, 1, 0, 1 }; + Object[] historyD = new Object[] {"d", 1, 1, 5, 5, "b", 2, 3, 3, 4, "a", 0, 1, 0, 2}; + + FileRenameHistory frh = new FileRenameHistory(0, 5); + for (Object[] history : new Object[][] {historyA, historyB, historyC, historyD}) { + String fname = history[0].toString(); + HgDataFile df = repo.getFileNode(fname); + Assert.assertFalse(frh.isOutOfRange(df, df.getLastRevision())); + frh.build(df, df.getLastRevision()); + int recordIndex = 0; + errorCollector.assertEquals(history.length / 5, frh.chunks()); + for (Chunk c : frh.iterate(HgIterateDirection.NewToOld)) { + compareChunk(fname, c, history, recordIndex++); + } + errorCollector.assertEquals("Shall compare full history", history.length, recordIndex * 5); + } + // + HgDataFile df = repo.getFileNode("d"); + Assert.assertFalse(frh.isOutOfRange(df, 0)); + frh.build(df, 0); + errorCollector.assertEquals(1, frh.chunks()); + Chunk c = frh.iterate(NewToOld).iterator().next(); + compareChunk("abandoned d(0)", c, new Object[] { "d", 0, 0, 4, 4 }, 0); + // + df = repo.getFileNode("a"); + Assert.assertFalse(frh.isOutOfRange(df, 0)); + frh.build(df, 0); + errorCollector.assertEquals(1, frh.chunks()); + c = frh.iterate(NewToOld).iterator().next(); + compareChunk("a(0) and boundary checks", c, new Object[] { "a", 0, 0, 0, 0 }, 0); + // + repo = Configuration.get().find("test-annotate"); // need a long file history + df = repo.getFileNode("file1"); + Assert.assertTrue("[sanity]", repo.getChangelog().getLastRevision() >=9); + Assert.assertTrue("[sanity]", df.exists() && df.getLastRevision() >= 9); + frh = new FileRenameHistory(0, 9); + frh.build(df, 9); + errorCollector.assertEquals(1, frh.chunks()); + c = frh.iterate(NewToOld).iterator().next(); + compareChunk("regular file, no renames", c, new Object[] { "file1", 0, 9, 0, 9 }, 0); + // restricted range + frh = new FileRenameHistory(3, 6); + Assert.assertFalse(frh.isOutOfRange(df, 9)); + frh.build(df, 9); // start from out of range revision + errorCollector.assertEquals(1, frh.chunks()); + c = frh.iterate(OldToNew).iterator().next(); + compareChunk("regular file, no renames, in range 3..6", c, new Object[] { "file1", 3, 6, 3, 6 }, 0); + } + + private void compareChunk(String msg, Chunk chunk, Object[] expected, int recordOffset) { + int off = recordOffset * 5; + errorCollector.assertEquals(msg, expected[off], chunk.file().getPath().toString()); + errorCollector.assertEquals(msg, expected[off+1], chunk.firstFileRev()); + errorCollector.assertEquals(msg, expected[off+2], chunk.lastFileRev()); + errorCollector.assertEquals(msg, expected[off+3], chunk.firstCset()); + errorCollector.assertEquals(msg, expected[off+4], chunk.lastCset()); + } + +}
--- a/test/org/tmatesoft/hg/test/TestHistory.java Thu Aug 01 21:45:47 2013 +0200 +++ b/test/org/tmatesoft/hg/test/TestHistory.java Fri Aug 02 23:07:23 2013 +0200 @@ -21,6 +21,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.tmatesoft.hg.core.HgIterateDirection.NewToOld; +import static org.tmatesoft.hg.core.HgIterateDirection.OldToNew; import java.util.ArrayList; import java.util.Collections; @@ -69,7 +70,7 @@ private LogOutputParser changelogParser; public static void main(String[] args) throws Throwable { - TestHistory th = new TestHistory(); + TestHistory th = new TestHistory(new HgLookup().detectFromWorkingDir()); th.testCompleteLog(); th.testFollowHistory(); th.errorCollector.verify(); @@ -81,19 +82,22 @@ th.errorCollector.verify(); } - public TestHistory() throws Exception { - this(new HgLookup().detectFromWorkingDir()); -// this(new HgLookup().detect("\\temp\\hg\\hello")); + public TestHistory() { + eh = new ExecHelper(changelogParser = new LogOutputParser(true), null); } private TestHistory(HgRepository hgRepo) { + this(); repo = hgRepo; - eh = new ExecHelper(changelogParser = new LogOutputParser(true), repo.getWorkingDir()); - + eh.cwd(repo.getWorkingDir()); } - + @Test public void testCompleteLog() throws Exception { + if (repo == null) { + repo = Configuration.get().own(); + eh.cwd(repo.getWorkingDir()); + } changelogParser.reset(); eh.run("hg", "log", "--debug"); List<HgChangeset> r = new HgLogCommand(repo).execute(); @@ -105,6 +109,10 @@ @Test public void testFollowHistory() throws Exception { + if (repo == null) { + repo = Configuration.get().own(); + eh.cwd(repo.getWorkingDir()); + } final Path f = Path.create("cmdline/org/tmatesoft/hg/console/Remote.java"); assertTrue(repo.getFileNode(f).exists()); changelogParser.reset(); @@ -409,6 +417,37 @@ report("HgChangesetHandler+RenameHandler with followRenames = false, new2old iteration order", h1.getChanges(), false); report("HgChangesetTreeHandler+RenameHandler with followRenames = false, new2old iteration order", h2.getResult(), false); } + + @Test + public void testFollowMultipleRenames() throws Exception { + repo = Configuration.get().find("log-renames"); + String fname = "a"; + eh.run("hg", "log", "--debug", "--follow", fname, "--cwd", repo.getLocation()); + HgLogCommand cmd = new HgLogCommand(repo); + cmd.file(fname, true, true); + CollectWithRenameHandler h1; + // + cmd.order(OldToNew).execute(h1 = new CollectWithRenameHandler()); + errorCollector.assertEquals(2, h1.rh.renames.size()); + report("Follow a->c->b, old2new:", h1.getChanges(), true); + // + cmd.order(NewToOld).execute(h1 = new CollectWithRenameHandler()); + errorCollector.assertEquals(2, h1.rh.renames.size()); + report("Follow a->c->b, new2old:", h1.getChanges(), false); + // + // + TreeCollectHandler h2 = new TreeCollectHandler(false); + RenameCollector rh = new RenameCollector(h2); + cmd.order(OldToNew).execute(h2); + errorCollector.assertEquals(2, rh.renames.size()); + report("Tree. Follow a->c->b, old2new:", h2.getResult(), true); + // + h2 = new TreeCollectHandler(false); + rh = new RenameCollector(h2); + cmd.order(NewToOld).execute(h2); + errorCollector.assertEquals(2, rh.renames.size()); + report("Tree. Follow a->c->b, new2old:", h2.getResult(), false); + } private void assertRename(String fnameFrom, String fnameTo, Pair<HgFileRevision, HgFileRevision> rename) { errorCollector.assertEquals(fnameFrom, rename.first().getPath().toString());