Mercurial > jhg
changeset 680:58a6900f845d
Blame: alternative strategy to handle merge revisions: map(diff(p1->base->p2)) to understand merge intentions better
author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
---|---|
date | Sun, 21 Jul 2013 17:15:34 +0200 |
parents | 19f5167c2155 |
children | 4f93bbc73b64 |
files | src/org/tmatesoft/hg/core/HgAnnotateCommand.java src/org/tmatesoft/hg/internal/BlameHelper.java src/org/tmatesoft/hg/internal/DiffHelper.java src/org/tmatesoft/hg/internal/ForwardAnnotateInspector.java src/org/tmatesoft/hg/internal/IntSliceSeq.java src/org/tmatesoft/hg/internal/IntVector.java src/org/tmatesoft/hg/internal/diff/DiffRangeMap.java test-data/test-repos.jar test/org/tmatesoft/hg/test/TestBlame.java |
diffstat | 9 files changed, 477 insertions(+), 72 deletions(-) [+] |
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/core/HgAnnotateCommand.java Sat Jul 20 17:40:52 2013 +0200 +++ b/src/org/tmatesoft/hg/core/HgAnnotateCommand.java Sun Jul 21 17:15:34 2013 +0200 @@ -80,6 +80,17 @@ return this; } + + /** + * Select file to annotate, + * @param fileNode repository file to annotate + * @param followCopyRename true to follow copies/renames. + * @return <code>this</code> for convenience + */ + public HgAnnotateCommand file(HgDataFile fileNode, boolean followCopyRename) { + return file(fileNode.getPath(), followCopyRename); + } + // TODO [post-1.1] set encoding and provide String line content from LineInfo // TODO FWIW: diff algorithms: http://bramcohen.livejournal.com/73318.html
--- a/src/org/tmatesoft/hg/internal/BlameHelper.java Sat Jul 20 17:40:52 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/BlameHelper.java Sun Jul 21 17:15:34 2013 +0200 @@ -19,17 +19,27 @@ import static org.tmatesoft.hg.core.HgIterateDirection.OldToNew; import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.LinkedList; +import java.util.List; import java.util.ListIterator; import org.tmatesoft.hg.core.HgCallbackTargetException; +import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.internal.DiffHelper.LineSequence; import org.tmatesoft.hg.internal.DiffHelper.LineSequence.ByteChain; +import org.tmatesoft.hg.internal.diff.DiffRangeMap; +import org.tmatesoft.hg.internal.diff.DiffRangeMap.RangePair; import org.tmatesoft.hg.core.HgBlameInspector; import org.tmatesoft.hg.core.HgBlameInspector.*; +import org.tmatesoft.hg.repo.HgChangelog; import org.tmatesoft.hg.repo.HgDataFile; import org.tmatesoft.hg.repo.HgInvalidStateException; +import org.tmatesoft.hg.repo.HgParentChildMap; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgRevisionMap; import org.tmatesoft.hg.repo.HgRuntimeException; import org.tmatesoft.hg.util.Adaptable; import org.tmatesoft.hg.util.CancelledException; @@ -45,6 +55,7 @@ private final HgBlameInspector insp; private FileLinesCache linesCache; + private HgParentChildMap<HgChangelog> clogMap; public BlameHelper(HgBlameInspector inspector) { insp = inspector; @@ -100,14 +111,12 @@ int p2ClogIndex = fileParentClogRevs[1]; LineSequence p1Lines = linesCache.lines(p1ClogIndex, fileParentRevs[0]); LineSequence p2Lines = linesCache.lines(p2ClogIndex, fileParentRevs[1]); + MergeResolutionStrategy mergeResolver = createMergeStrategy(fileRevLines, p1Lines, p2Lines, csetRevIndex, fileParentClogRevs); + // DiffHelper<LineSequence> pg = new DiffHelper<LineSequence>(); - pg.init(p2Lines, fileRevLines); - EqualBlocksCollector p2MergeCommon = new EqualBlocksCollector(); - pg.findMatchingBlocks(p2MergeCommon); - // - pg.init(p1Lines); + pg.init(p1Lines, fileRevLines); BlameBlockInspector bbi = new BlameBlockInspector(targetFile, fileRevIndex, insp, p1ClogIndex, csetRevIndex); - bbi.setMergeParent2(new MergeStrategy1(p2MergeCommon.matches), p2ClogIndex); + bbi.setMergeParent2(mergeResolver, p2ClogIndex); pg.findMatchingBlocks(bbi); bbi.checkErrors(); } else if (fileParentClogRevs[0] == fileParentClogRevs[1]) { @@ -132,6 +141,66 @@ } } + private static final boolean useNewStrategy = Boolean.TRUE.booleanValue(); + + private MergeResolutionStrategy createMergeStrategy(LineSequence fileRevLines, LineSequence p1Lines, LineSequence p2Lines, int csetRevIndex, int[] fileParentClogRevs) { + DiffHelper<LineSequence> pg = new DiffHelper<LineSequence>(); + if (useNewStrategy) { + final ArrayList<RangePairSeq> allMatches = new ArrayList<RangePairSeq>(); + pg.init(p2Lines, fileRevLines); + pg.findAllMatchAlternatives(new DiffHelper.MatchInspector<LineSequence>() { + private RangePairSeq matches; + + public void begin(LineSequence s1, LineSequence s2) { + matches = new RangePairSeq(); + } + + public void match(int startSeq1, int startSeq2, int matchLength) { + matches.add(startSeq1, startSeq2, matchLength); + } + + public void end() { + if (matches.size() > 0) { + allMatches.add(matches); + } + } + + }); + // + LineSequence baseLines = getBaseRevisionLines(csetRevIndex, fileParentClogRevs); + pg.init(p1Lines, baseLines); + DiffRangeMap p1ToBase = new DiffRangeMap().fill(pg); + pg.init(baseLines, p2Lines); + DiffRangeMap baseToP2 = new DiffRangeMap().fill(pg); + return new MergeStrategy2(allMatches, p1ToBase, baseToP2); + } else { + pg.init(p2Lines, fileRevLines); + EqualBlocksCollector p2MergeCommon = new EqualBlocksCollector(); + pg.findMatchingBlocks(p2MergeCommon); + return new MergeStrategy1(p2MergeCommon.matches); + } + } + + private LineSequence getBaseRevisionLines(int clogRevIndex, int[] fileParentClogRevs) { + assert fileParentClogRevs[0] >= 0; + assert fileParentClogRevs[1] >= 0; + HgDataFile targetFile = linesCache.getFile(clogRevIndex); + final HgRepository repo = targetFile.getRepo(); + if (clogMap == null) { + // FIXME replace HgParentChildMap with revlog.indexWalk(AncestorIterator)) + clogMap = new HgParentChildMap<HgChangelog>(repo.getChangelog()); + clogMap.init(); + } + final HgRevisionMap<HgChangelog> m = clogMap.getRevisionMap(); + Nodeid ancestor = clogMap.ancestor(m.revision(fileParentClogRevs[0]), m.revision(fileParentClogRevs[1])); + final int ancestorRevIndex = m.revisionIndex(ancestor); + Nodeid fr = repo.getManifest().getFileRevision(ancestorRevIndex, targetFile.getPath()); + if (fr == null) { + return LineSequence.newlines(new byte[0]); + } + return linesCache.lines(ancestorRevIndex, targetFile.getRevisionIndex(fr)); + } + private static class FileLinesCache { private final LinkedList<Pair<Integer, LineSequence>> lruCache; private final int limit; @@ -281,7 +350,7 @@ } try { if (p2MergeCommon != null) { - IntSliceSeq mergeRanges = p2MergeCommon.combineAndMarkRangesWithSource(s2From, s2To - s2From, csetOrigin, csetMergeParent); + IntSliceSeq mergeRanges = p2MergeCommon.combineAndMarkRangesWithSource(s1From, s1To, s2From, s2To, csetOrigin, csetMergeParent); /* * Usecases, how it USED TO BE initially: @@ -345,7 +414,7 @@ } try { if (p2MergeCommon != null) { - IntSliceSeq mergeRanges = p2MergeCommon.combineAndMarkRangesWithSource(s2From, s2To - s2From, csetOrigin, csetMergeParent); + IntSliceSeq mergeRanges = p2MergeCommon.combineAndMarkRangesWithSource(s1InsertPoint, s2From, s2To, csetOrigin, csetMergeParent); int insPoint = s1InsertPoint; // track changes to insertion point for (IntTuple mergeRange : mergeRanges) { int rangeOrigin = mergeRange.at(0); @@ -617,18 +686,7 @@ return contentBlock.seq.data(from, from + length); } } - - - interface MergeResolutionStrategy { - /** - * breaks region [start..start+length) into ranges according to deduced (or simply guessed) - * matching of these lines to lines in source1 and source2 - * @return list of tuples (source, start, length), where source is one of the identifiers supplied - */ - public IntSliceSeq combineAndMarkRangesWithSource(int start, int length, int source1, int source2); - public int getLineInP2(int mergeLine); - } - + private static class EqualBlocksCollector implements DiffHelper.MatchInspector<LineSequence> { private final RangePairSeq matches = new RangePairSeq(); @@ -660,7 +718,20 @@ } } - + + interface MergeResolutionStrategy { + /** + * breaks region [start2..end2) into ranges according to deduced (or simply guessed) + * matching of [start1..end1) lines to lines in source1 and source2 + * @return list of tuples (source, start, length), where source is one of the identifiers supplied + */ + public IntSliceSeq combineAndMarkRangesWithSource(int start1, int end1, int start2, int end2, int source1, int source2); + public IntSliceSeq combineAndMarkRangesWithSource(int insPoint, int start, int end, int source1, int source2); + public int getLineInP2(int mergeLine); + } + + // report lines as merged from p2 solely based on whether target line belongs + // to a region that is equal to p2 region private static class MergeStrategy1 implements MergeResolutionStrategy { // equal ranges in p2 and merged revision private final RangePairSeq matches; @@ -677,7 +748,7 @@ * whether the range is from initial range (markerSource) or is a result of the intersection with target * (markerTarget) */ - public IntSliceSeq combineAndMarkRangesWithSource(int start, int length, int markerSource, int markerTarget) { + private IntSliceSeq doCombine(int start, int length, int markerSource, int markerTarget) { mergeRanges.clear(); assert mergeRanges.sliceSize() == 3; int sourceStart = start, targetStart = start, sourceEnd = start + length; @@ -716,7 +787,89 @@ public int getLineInP2(int mergeLine) { return matches.reverseMapLine(mergeLine); } + + public IntSliceSeq combineAndMarkRangesWithSource(int start1, int end1, int start2, int end2, int source1, int source2) { + return doCombine(start2, end2 - start2, source1, source2); + } + + public IntSliceSeq combineAndMarkRangesWithSource(int insPoint, int start, int end, int source1, int source2) { + return doCombine(start, end - start, source1, source2); + } } + + private static class MergeStrategy2 implements MergeResolutionStrategy { + // equal ranges in p2 and merged revision + private final List<RangePairSeq> matches; + private final IntSliceSeq mergeRanges; + private final DiffRangeMap p1ToBase; + private final DiffRangeMap baseToP2; + + public MergeStrategy2(List<RangePairSeq> p2EqualToM, DiffRangeMap p1ToBaseRanges, DiffRangeMap baseToP2Ranges) { + matches = p2EqualToM; + p1ToBase = p1ToBaseRanges; + baseToP2= baseToP2Ranges; + mergeRanges = new IntSliceSeq(3, 10, 10); + } + + + public IntSliceSeq combineAndMarkRangesWithSource(int insPoint, int start, int end, int source1, int source2) { + return combineAndMarkRangesWithSource(insPoint, insPoint, start, end, source1, source2); + } + + public IntSliceSeq combineAndMarkRangesWithSource(int start1, int end1, int start2, int end2, int source1, int source2) { + mergeRanges.clear(); + IntSliceSeq mergedLines = new IntSliceSeq(2, end2-start2, 0); + for (int i = start2; i < end2; i++) { + mergedLines.add(source1, 0); + } + // [s1Start..s1End) // range in p1 seen as changed in m + for (RangePair p1_b : p1ToBase.findInSource(start1, end1)) { + // there might be few ranges in (p1-base) that overlap with (p1-m) changes + for (RangePair b_p2 : baseToP2.findInSource(p1_b.start2(), p1_b.end2())) { + // regions in p2 that correspond to affected regions in base + for (int p2Line = b_p2.start2(); p2Line < b_p2.end2(); p2Line++) { + for (RangePairSeq eq : matches) { + if (eq.includesOriginLine(p2Line)) { + // this line in p2 is equal to some line in merge + int mergeLine = eq.mapLineIndex(p2Line); + if (mergeLine >= start2 && mergeLine < end2) { + mergedLines.set(mergeLine - start2, source2, p2Line); + } + } + } + } + } + } + int lineCount = 0, start = start2; + int lastSeenSource = source1; + for (IntTuple t : mergedLines) { + if (t.at(0) == lastSeenSource) { + lineCount++; + } else { + if (lineCount > 0) { + mergeRanges.add(lastSeenSource, start, lineCount); + start += lineCount; + } + lineCount = 1; + lastSeenSource = t.at(0); + } + } + if (lineCount > 0) { + mergeRanges.add(lastSeenSource, start, lineCount); + } + return mergeRanges; + } + + public int getLineInP2(int mergeLine) { + for (RangePairSeq eq : matches) { + if (eq.includesTargetLine(mergeLine)) { + return eq.reverseMapLine(mergeLine); + } + } + return -1; + } + } + private static class AnnotateRev implements RevisionDescriptor { public ContentBlock origin, target; @@ -789,7 +942,24 @@ } System.out.println(); MergeStrategy1 ms = new MergeStrategy1(bc.matches); - IntSliceSeq mr = ms.combineAndMarkRangesWithSource(0, 16, 508, 514); + IntSliceSeq mr = ms.doCombine(0, 16, 508, 514); + for (IntTuple t : mr) { + System.out.printf("%d:[%d..%d) ", t.at(0), t.at(1), t.at(1) + t.at(2)); + } + System.out.println(); + System.out.println(); + DiffRangeMap m1 = new DiffRangeMap(); // p1 -> base + m1.match(0, 0, 1); // =1..1 -> 1..1 + m1.match(7, 3, 0); // *2..7 -> 2..3 + DiffRangeMap m2 = new DiffRangeMap(); // base -> p2 + m2.match(0, 0, 1); // =1..1 -> 1..1 + m2.match(3, 3, 0); // *2..3 -> 2..3 + RangePairSeq eq1 = new RangePairSeq(); + eq1.add(0, 0, 3); + RangePairSeq eq2 = new RangePairSeq(); + eq2.add(0, 4, 3); + MergeStrategy2 ms2 = new MergeStrategy2(Arrays.asList(eq1, eq2), m1, m2); + mr = ms2.combineAndMarkRangesWithSource(5, 7, 5, 7, 33, 44); for (IntTuple t : mr) { System.out.printf("%d:[%d..%d) ", t.at(0), t.at(1), t.at(1) + t.at(2)); }
--- a/src/org/tmatesoft/hg/internal/DiffHelper.java Sat Jul 20 17:40:52 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/DiffHelper.java Sun Jul 21 17:15:34 2013 +0200 @@ -83,6 +83,50 @@ insp.end(); } + /** + * look up every line in s2 that match lines in s1 + * idea: pure additions in s2 are diff-ed against s1 again and again, to see if there are any matches + */ + void findAllMatchAlternatives(final MatchInspector<T> insp) { + assert seq1.chunkCount() > 0; + final IntSliceSeq insertions = new IntSliceSeq(2); + final boolean matchedAny[] = new boolean[] {false}; + DeltaInspector<T> myInsp = new DeltaInspector<T>() { + @Override + protected void unchanged(int s1From, int s2From, int length) { + matchedAny[0] = true; + insp.match(s1From, s2From, length); + } + @Override + protected void added(int s1InsertPoint, int s2From, int s2To) { + insertions.add(s2From, s2To); + } + }; + matchInspector = myInsp; + myInsp.begin(seq1, seq2); + IntSliceSeq s2RangesToCheck = new IntSliceSeq(2, 1, 0); + s2RangesToCheck.add(0, seq2.chunkCount()); + do { + IntSliceSeq nextCheck = new IntSliceSeq(2); + for (IntTuple t : s2RangesToCheck) { + int s2Start = t.at(0); + int s2End = t.at(1); + myInsp.changeStartS1 = 0; + myInsp.changeStartS2 = s2Start; + insp.begin(seq1, seq2); + matchedAny[0] = false; + findMatchingBlocks(0, seq1.chunkCount(), s2Start, s2End); + insp.end(); + myInsp.end(); + if (matchedAny[0]) { + nextCheck.addAll(insertions); + } + insertions.clear(); + } + s2RangesToCheck = nextCheck; + } while (s2RangesToCheck.size() > 0); + } + /** * implementation based on Python's difflib.py and SequenceMatcher */
--- a/src/org/tmatesoft/hg/internal/ForwardAnnotateInspector.java Sat Jul 20 17:40:52 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/ForwardAnnotateInspector.java Sun Jul 21 17:15:34 2013 +0200 @@ -17,14 +17,9 @@ package org.tmatesoft.hg.internal; import org.tmatesoft.hg.core.HgAnnotateCommand.Inspector; -import org.tmatesoft.hg.core.HgAnnotateCommand.LineInfo; import org.tmatesoft.hg.core.HgBlameInspector; import org.tmatesoft.hg.core.HgCallbackTargetException; -import org.tmatesoft.hg.core.HgDiffCommand; -import org.tmatesoft.hg.core.HgException; import org.tmatesoft.hg.core.HgIterateDirection; -import org.tmatesoft.hg.repo.HgLookup; -import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.util.CancelSupport; import org.tmatesoft.hg.util.CancelledException; import org.tmatesoft.hg.util.ProgressSupport; @@ -141,24 +136,4 @@ originPos += originBlockLen; } } - - - public static void main(String[] args) throws HgCallbackTargetException, CancelledException, HgException { - HgRepository repo = new HgLookup().detect("/home/artem/hg/junit-test-repos/test-annotate/"); - HgDiffCommand cmd = new HgDiffCommand(repo); - cmd.file(repo.getFileNode("file1")).order(HgIterateDirection.OldToNew); - final int cset = 8; - cmd.range(0, cset); - final ForwardAnnotateInspector c2 = new ForwardAnnotateInspector(); - cmd.executeAnnotate(c2); - for (IntTuple t : c2.all.get(cset)) { - System.out.printf("Block %d lines from revision %d (starts with line %d in the origin)\n", t.at(0), t.at(1), 1+t.at(2)); - } - c2.report(cset, new Inspector() { - - public void next(LineInfo lineInfo) throws HgCallbackTargetException { - System.out.printf("%3d:%3d: %s", lineInfo.getChangesetIndex(), lineInfo.getOriginLineNumber(), new String(lineInfo.getContent())); - } - }, ProgressSupport.Factory.get(null), CancelSupport.Factory.get(null)); - } } \ No newline at end of file
--- a/src/org/tmatesoft/hg/internal/IntSliceSeq.java Sat Jul 20 17:40:52 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/IntSliceSeq.java Sun Jul 21 17:15:34 2013 +0200 @@ -24,7 +24,7 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public final class IntSliceSeq implements Iterable<IntTuple> { +public final class IntSliceSeq implements Iterable<IntTuple>, Cloneable { private final IntVector slices; private final int slice; @@ -39,13 +39,13 @@ } public IntSliceSeq add(int... values) { - checkValues(values); + checkValuesAny(values); slices.add(values); return this; } public IntSliceSeq set(int sliceIndex, int... values) { - checkValues(values); + checkValuesExact(values); for (int i = 0, j = sliceIndex*slice; i < slice; i++,j++) { slices.set(j, values[i]); } @@ -63,6 +63,13 @@ return slices.get(sliceIndex*slice + valueIndex); } + public void addAll(IntSliceSeq other) { + if (other.slice != this.slice) { + throw new IllegalArgumentException(String.format("Tuple size doesn't match: %d and %d", slice, other.slice)); + } + slices.addAll(other.slices); + } + public int size() { return slices.size() / slice; } @@ -117,6 +124,15 @@ } return sb.toString(); } + + @Override + public IntSliceSeq clone() { + try { + return (IntSliceSeq) super.clone(); + } catch (CloneNotSupportedException ex) { + throw new Error(ex); + } + } private void checkArgRange(int rangeSize, int index) { if (index >= 0 && index < rangeSize) { @@ -124,9 +140,14 @@ } throw new IllegalArgumentException(String.valueOf(index)); } - private void checkValues(int[] values) { + private void checkValuesExact(int[] values) { if (values == null || values.length != slice) { throw new IllegalArgumentException(String.valueOf(values == null ? values : values.length)); } } + private void checkValuesAny(int[] values) { + if (values == null || values.length % slice != 0) { + throw new IllegalArgumentException(String.valueOf(values == null ? values : values.length)); + } + } }
--- a/src/org/tmatesoft/hg/internal/IntVector.java Sat Jul 20 17:40:52 2013 +0200 +++ b/src/org/tmatesoft/hg/internal/IntVector.java Sun Jul 21 17:15:34 2013 +0200 @@ -24,7 +24,7 @@ * @author Artem Tikhomirov * @author TMate Software Ltd. */ -public class IntVector { +public class IntVector implements Cloneable { private int[] data; private final int increment; @@ -57,7 +57,17 @@ data[count++] = v; } } - + + public void addAll(IntVector other) { + final int otherLen = other.count; + if (count + otherLen > data.length) { + grow(count + otherLen); + } + for (int i = 0; i < otherLen; i++) { + data[count++] = other.data[i]; + } + } + public int get(int i) { if (i < 0 || i >= count) { throw new IndexOutOfBoundsException(String.format("Index: %d, size: %d", i, count)); @@ -126,6 +136,15 @@ public String toString() { return String.format("%s[%d]", IntVector.class.getSimpleName(), size()); } + + @Override + public IntVector clone() { + try { + return (IntVector) super.clone(); + } catch (CloneNotSupportedException ex) { + throw new Error(ex); + } + } /** * Use only when this instance won't be used any longer
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/diff/DiffRangeMap.java Sun Jul 21 17:15:34 2013 +0200 @@ -0,0 +1,138 @@ +/* + * 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.diff; + +import java.util.ArrayList; + +import org.tmatesoft.hg.internal.DiffHelper; +import org.tmatesoft.hg.internal.IntSliceSeq; +import org.tmatesoft.hg.internal.IntTuple; +import org.tmatesoft.hg.internal.DiffHelper.ChunkSequence; + +/** + * Sequence of pairs of ranges (s1Start,s1End) - (s2Start, s2End) + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class DiffRangeMap extends DiffHelper.DeltaInspector<ChunkSequence<?>> { + private final IntSliceSeq ranges; + + public DiffRangeMap() { + ranges = new IntSliceSeq(5); + } + + /** + * handy method to fill this map from supplied DiffHelper + * <pre> + * DiffHelper<LineSequence> pg = ... + * pg.findMatchingBlocks(p1ToBase); // doesn't compile + * DiffHelper<?> dh = pg; + * dh.findMatchingBlocks(p1ToBase); // compiles ok! + * </pre> + */ + public DiffRangeMap fill(DiffHelper<?> dh) { + dh.findMatchingBlocks(this); + return this; + } + + @Override + protected void added(int s1InsertPoint, int s2From, int s2To) { + ranges.add(s1InsertPoint, s1InsertPoint, s2From, s2To, (int)'+'); + } + @Override + protected void changed(int s1From, int s1To, int s2From, int s2To) { + ranges.add(s1From, s1To, s2From, s2To, (int)'*'); + } + @Override + protected void deleted(int s2DeletePoint, int s1From, int s1To) { + ranges.add(s1From, s1To, s2DeletePoint, s2DeletePoint, (int)'-'); + } + @Override + protected void unchanged(int s1From, int s2From, int length) { + ranges.add(s1From, s1From + length, s2From, s2From + length, (int)'='); + } + + public Iterable<RangePair> findInSource(int sourceStart, int sourceEnd) { + ArrayList<RangePair> rv = new ArrayList<RangePair>(4); + for (IntTuple t : ranges) { + int srcRangeStart = t.at(0); + int srcRangeEnd = t.at(1); + if (srcRangeEnd <= sourceStart) { // srcRangeEnd exclusive + continue; + } + if (srcRangeStart >= sourceEnd) { + break; + } + rv.add(new RangePair(srcRangeStart, srcRangeEnd, t.at(2), t.at(3))); + } + return rv; + } + + public Iterable<RangePair> insertions() { + return rangesOfKind('+'); + } + + public Iterable<RangePair> same() { + return rangesOfKind('='); + } + + private Iterable<RangePair> rangesOfKind(int kind) { + ArrayList<RangePair> rv = new ArrayList<RangePair>(4); + for (IntTuple t : ranges) { + if (t.at(4) == kind) { + rv.add(new RangePair(t.at(0), t.at(1), t.at(2), t.at(3))); + } + } + return rv; + } + + public static final class RangePair { + private final int s1Start; + private final int s1End; + private final int s2Start; + private final int s2End; + + public RangePair(int s1Start, int s1End, int s2Start, int s2End) { + this.s1Start = s1Start; + this.s1End = s1End; + this.s2Start = s2Start; + this.s2End = s2End; + } + public int start1() { + return s1Start; + } + public int end1() { + return s1End; + } + public int length1() { + return s1End - s1Start; + } + public int start2() { + return s2Start; + } + public int end2() { + return s2End; + } + public int length2() { + return s2End - s2Start; + } + @Override + public String toString() { + return String.format("[%d..%d)->[%d..%d)", s1Start, s1End, s2Start, s2End); + } + } +}
--- a/test/org/tmatesoft/hg/test/TestBlame.java Sat Jul 20 17:40:52 2013 +0200 +++ b/test/org/tmatesoft/hg/test/TestBlame.java Sun Jul 21 17:15:34 2013 +0200 @@ -262,24 +262,6 @@ doAnnotateLineCheck(changeset, ar, ai); } - private void doAnnotateLineCheck(int cs, AnnotateRunner ar, AnnotateInspector hg4jResult) { - String[] hgAnnotateLines = ar.getLines(); - assertTrue("[sanity]", hgAnnotateLines.length > 0); - assertEquals("Number of lines reported by native annotate and our impl", hgAnnotateLines.length, hg4jResult.getLineCount()); - - for (int i = 0; i < hgAnnotateLines.length; i++) { - String[] hgLine = hgAnnotateLines[i].split(":"); - assertTrue(hgAnnotateLines[i], hgLine.length >= 3); - int hgAnnotateRevIndex = Integer.parseInt(hgLine[0].trim()); - int hgFirstAppLine = Integer.parseInt(hgLine[1].trim()); - String hgLineText = hgAnnotateLines[i].substring(hgLine[0].length() + hgLine[1].length() + 2).trim(); - errorCollector.assertEquals(String.format("Revision mismatch for line %d (annotating rev: %d)", i+1, cs), hgAnnotateRevIndex, hg4jResult.getChangeset(i)); - errorCollector.assertEquals(hgLineText, hg4jResult.getLine(i).trim()); - errorCollector.assertEquals(hgFirstAppLine, hg4jResult.getOriginLine(i)); - } - } - - @Test public void testDiffTwoRevisions() throws Exception { HgRepository repo = Configuration.get().find("test-annotate"); @@ -319,6 +301,32 @@ String[] expected = splitLines(gp.result()); Assert.assertArrayEquals(expected, apiResult); } + + @Test + public void testAnnotateMergeMapViaBase() throws Exception { + HgRepository repo = Configuration.get().find("test-annotate3"); + HgDataFile df1 = repo.getFileNode("file1"); + HgDataFile df4 = repo.getFileNode("file4"); + HgDataFile df5 = repo.getFileNode("file5"); + assertTrue("[sanity]", df1.exists() && df4.exists()); + // hg annotate handles merge in its own way, here we check + // how map(diff(p1->base->p2)) merge strategy works + final String file1AnnotateResult = "3:1:1\n3:2:2x\n3:3:3y\n2:4:z\n0:1:1\n1:2:2x\n4:3:3y\n"; + final String file4AnnotateResult = "3:1:1\n1:2:2x\n4:3:3y\n2:4:z\n0:1:1\n3:6:2x\n3:7:3y\n"; + final String file5AnnotateResult = "0:1:1\n1:2:2x\n4:3:3y\n2:4:z\n5:5:1\n5:6:2x\n5:7:3y\n"; + HgAnnotateCommand cmd = new HgAnnotateCommand(repo); + cmd.changeset(5); + AnnotateInspector insp = new AnnotateInspector(); + // file1 + cmd.file(df1, false).execute(insp); + doAnnotateLineCheck(5, splitLines(file1AnnotateResult), insp); + // file4 + cmd.file(df4, false).execute(insp = new AnnotateInspector()); + doAnnotateLineCheck(5, splitLines(file4AnnotateResult), insp); + // file5 + cmd.file(df5, false).execute(insp = new AnnotateInspector()); + doAnnotateLineCheck(5, splitLines(file5AnnotateResult), insp); +} // TODO HgWorkingCopyStatusCollector (and HgStatusCollector), with their ancestors (rev 59/69) have examples // of *incorrect* assignment of common lines (like "}") - our impl doesn't process common lines in any special way @@ -349,6 +357,25 @@ return rv; } + private void doAnnotateLineCheck(int cs, AnnotateRunner ar, AnnotateInspector hg4jResult) { + String[] hgAnnotateLines = ar.getLines(); + assertTrue("[sanity]", hgAnnotateLines.length > 0); + assertEquals("Number of lines reported by native annotate and our impl", hgAnnotateLines.length, hg4jResult.getLineCount()); + doAnnotateLineCheck(cs, hgAnnotateLines, hg4jResult); + } + + private void doAnnotateLineCheck(int cs, String[] expectedAnnotateLines, AnnotateInspector hg4jResult) { + for (int i = 0; i < expectedAnnotateLines.length; i++) { + String[] hgLine = expectedAnnotateLines[i].split(":"); + assertTrue(expectedAnnotateLines[i], hgLine.length >= 3); + int hgAnnotateRevIndex = Integer.parseInt(hgLine[0].trim()); + int hgFirstAppLine = Integer.parseInt(hgLine[1].trim()); + String hgLineText = expectedAnnotateLines[i].substring(hgLine[0].length() + hgLine[1].length() + 2).trim(); + errorCollector.assertEquals(String.format("Revision mismatch for line %d (annotating rev: %d)", i+1, cs), hgAnnotateRevIndex, hg4jResult.getChangeset(i)); + errorCollector.assertEquals("Line text", hgLineText, hg4jResult.getLine(i).trim()); + errorCollector.assertEquals("Line in origin", hgFirstAppLine, hg4jResult.getOriginLine(i)); + } + } private void ccc() throws Throwable { HgRepository repo = new HgLookup().detect("/home/artem/hg/hgtest-annotate-merge/");