Mercurial > jhg
changeset 545:15b406c7cd9d
First round of annotate file is functional
author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
---|---|
date | Fri, 15 Feb 2013 22:15:13 +0100 |
parents | 7f5998a9619d |
children | cd78e8b9d7bc |
files | src/org/tmatesoft/hg/internal/AnnotateFacility.java src/org/tmatesoft/hg/internal/IntVector.java src/org/tmatesoft/hg/internal/PatchGenerator.java test/org/tmatesoft/hg/test/TestBlame.java |
diffstat | 4 files changed, 361 insertions(+), 56 deletions(-) [+] |
line wrap: on
line diff
--- a/src/org/tmatesoft/hg/internal/AnnotateFacility.java Fri Feb 15 16:48:54 2013 +0100 +++ b/src/org/tmatesoft/hg/internal/AnnotateFacility.java Fri Feb 15 22:15:13 2013 +0100 @@ -19,11 +19,9 @@ import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION; import org.tmatesoft.hg.core.Nodeid; -import org.tmatesoft.hg.internal.PatchGenerator.ChunkSequence; import org.tmatesoft.hg.internal.PatchGenerator.LineSequence; import org.tmatesoft.hg.repo.HgDataFile; import org.tmatesoft.hg.repo.HgInvalidStateException; -import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.util.CancelledException; /** @@ -37,48 +35,64 @@ /** * Annotates changes of the file against its parent(s) */ - public void annotateChange(HgDataFile df, int changestRevisionIndex, Inspector insp) { - Nodeid fileRev = df.getRepo().getManifest().getFileRevision(changestRevisionIndex, df.getPath()); + public void annotateChange(HgDataFile df, int changesetRevisionIndex, Inspector insp) { + // TODO detect if file is text/binary (e.g. looking for chars < ' ' and not \t\r\n\f + Nodeid fileRev = df.getRepo().getManifest().getFileRevision(changesetRevisionIndex, df.getPath()); int fileRevIndex = df.getRevisionIndex(fileRev); int[] fileRevParents = new int[2]; df.parents(fileRevIndex, fileRevParents, null, null); - if (fileRevParents[0] != NO_REVISION && fileRevParents[1] != NO_REVISION) { - // merge - } else if (fileRevParents[0] == fileRevParents[1]) { - // may be equal iff both are unset - assert fileRevParents[0] == NO_REVISION; - // everything added - insp.added(null); - } else { - int soleParent = fileRevParents[0] == NO_REVISION ? fileRevParents[1] : fileRevParents[0]; - assert soleParent != NO_REVISION; - try { + try { + if (fileRevParents[0] != NO_REVISION && fileRevParents[1] != NO_REVISION) { + // merge + } else if (fileRevParents[0] == fileRevParents[1]) { + // may be equal iff both are unset + assert fileRevParents[0] == NO_REVISION; + // everything added + ByteArrayChannel c; + df.content(fileRevIndex, c = new ByteArrayChannel()); + BlameBlockInspector bbi = new BlameBlockInspector(insp, NO_REVISION, changesetRevisionIndex); + LineSequence cls = LineSequence.newlines(c.toArray()); + bbi.begin(LineSequence.newlines(new byte[0]), cls); + bbi.match(0, cls.chunkCount()-1, 0); + bbi.end(); + } else { + int soleParent = fileRevParents[0] == NO_REVISION ? fileRevParents[1] : fileRevParents[0]; + assert soleParent != NO_REVISION; ByteArrayChannel c1, c2; df.content(soleParent, c1 = new ByteArrayChannel()); df.content(fileRevIndex, c2 = new ByteArrayChannel()); int parentChangesetRevIndex = df.getChangesetRevisionIndex(soleParent); PatchGenerator<LineSequence> pg = new PatchGenerator<LineSequence>(); pg.init(LineSequence.newlines(c1.toArray()), LineSequence.newlines(c2.toArray())); - pg.findMatchingBlocks(new BlameBlockInspector(insp)); - } catch (CancelledException ex) { - // TODO likely it was bad idea to throw cancelled exception from content() - // deprecate and provide alternative? - HgInvalidStateException ise = new HgInvalidStateException("ByteArrayChannel never throws CancelledException"); - ise.initCause(ex); - throw ise; + pg.findMatchingBlocks(new BlameBlockInspector(insp, parentChangesetRevIndex, changesetRevisionIndex)); } + } catch (CancelledException ex) { + // TODO likely it was bad idea to throw cancelled exception from content() + // deprecate and provide alternative? + HgInvalidStateException ise = new HgInvalidStateException("ByteArrayChannel never throws CancelledException"); + ise.initCause(ex); + throw ise; } } @Callback public interface Inspector { - void same(Block block); + void same(EqualBlock block); void added(AddBlock block); void changed(ChangeBlock block); void deleted(DeleteBlock block); } + @Callback + public interface InspectorEx extends Inspector { // XXX better name + // XXX perhaps, shall pass object instead of separate values for future extension? + void start(int originLineCount, int targetLineCount); + void done(); + } + public interface Block { + int originChangesetIndex(); + int targetChangesetIndex(); // boolean isMergeRevision(); // int fileRevisionIndex(); // int originFileRevisionIndex(); @@ -86,6 +100,12 @@ // byte[] data(); } + public interface EqualBlock extends Block { + int originStart(); + int targetStart(); + int length(); + } + public interface AddBlock extends Block { int insertedAt(); // line index in the old file int firstAddedLine(); @@ -103,56 +123,115 @@ static class BlameBlockInspector extends PatchGenerator.DeltaInspector<LineSequence> { private final Inspector insp; + private final int csetP1; + private final int csetTarget; - public BlameBlockInspector(Inspector inspector) { + public BlameBlockInspector(Inspector inspector, int parentCset1, int targetCset) { assert inspector != null; insp = inspector; + csetP1 = parentCset1; + csetTarget = targetCset; + } + + @Override + public void begin(LineSequence s1, LineSequence s2) { + super.begin(s1, s2); + if (insp instanceof InspectorEx) { + ((InspectorEx) insp).start(s1.chunkCount() - 1, s2.chunkCount() - 1); + } + } + + @Override + public void end() { + super.end(); + if(insp instanceof InspectorEx) { + ((InspectorEx) insp).done(); + } } @Override protected void changed(int s1From, int s1To, int s2From, int s2To) { - insp.changed(new BlockImpl2(seq1, seq2, s1From, s1To-s1From, s2From, s2To - s2From, s1From, s2From)); + BlockImpl2 block = new BlockImpl2(seq1, seq2, s1From, s1To-s1From, s2From, s2To - s2From, s1From, s2From); + block.setOriginAndTarget(csetP1, csetTarget); + insp.changed(block); } @Override protected void added(int s1InsertPoint, int s2From, int s2To) { - insp.added(new BlockImpl2(null, seq2, -1, -1, s2From, s2To - s2From, s1InsertPoint, -1)); + BlockImpl2 block = new BlockImpl2(null, seq2, -1, -1, s2From, s2To - s2From, s1InsertPoint, -1); + block.setOriginAndTarget(csetP1, csetTarget); + insp.added(block); } @Override protected void deleted(int s2DeletePoint, int s1From, int s1To) { - insp.deleted(new BlockImpl2(seq1, null, s1From, s1To - s1From, -1, -1, -1, s2DeletePoint)); + BlockImpl2 block = new BlockImpl2(seq1, null, s1From, s1To - s1From, -1, -1, -1, s2DeletePoint); + block.setOriginAndTarget(csetP1, csetTarget); + insp.deleted(block); } @Override protected void unchanged(int s1From, int s2From, int length) { - insp.same(new BlockImpl(seq2, s2From, length)); + BlockImpl1 block = new BlockImpl1(s1From, s2From, length); + block.setOriginAndTarget(csetP1, csetTarget); + insp.same(block); + } + } + + static class BlockImpl implements Block { + + private int originCset; + private int targetCset; + + void setOriginAndTarget(int originChangesetIndex, int targetChangesetIndex) { + // XXX perhaps, shall be part of Inspector API, rather than Block's + // as they don't change between blocks (although the moment about merged revisions) + // is not yet clear to me + originCset = originChangesetIndex; + targetCset = targetChangesetIndex; + } + + public int originChangesetIndex() { + return originCset; + } + + public int targetChangesetIndex() { + return targetCset; } } - static class BlockImpl implements Block { - private final ChunkSequence seq; - private final int start; + static class BlockImpl1 extends BlockImpl implements EqualBlock { + private final int start1, start2; private final int length; - BlockImpl() { - // FIXME delete this cons - seq = null; - start = length = -1; + BlockImpl1(int blockStartSeq1, int blockStartSeq2, int blockLength) { + start1 = blockStartSeq1; + start2 = blockStartSeq2; + length = blockLength; } - BlockImpl(ChunkSequence s, int blockStart, int blockLength) { - seq = s; - start = blockStart; - length = blockLength; + public int originStart() { + return start1; + } + + public int targetStart() { + return start2; + } + + public int length() { + return length; } + @Override + public String toString() { + return String.format("@@ [%d..%d) == [%d..%d) @@", start1, start1+length, start2, start2+length); + } } - static class BlockImpl2 implements ChangeBlock { + static class BlockImpl2 extends BlockImpl implements ChangeBlock { - private final ChunkSequence oldSeq; - private final ChunkSequence newSeq; + private final LineSequence oldSeq; + private final LineSequence newSeq; private final int s1Start; private final int s1Len; private final int s2Start; @@ -160,7 +239,7 @@ private final int s1InsertPoint; private final int s2DeletePoint; - public BlockImpl2(ChunkSequence s1, ChunkSequence s2, int s1Start, int s1Len, int s2Start, int s2Len, int s1InsertPoint, int s2DeletePoint) { + public BlockImpl2(LineSequence s1, LineSequence s2, int s1Start, int s1Len, int s2Start, int s2Len, int s1InsertPoint, int s2DeletePoint) { oldSeq = s1; newSeq = s2; this.s1Start = s1Start; @@ -210,5 +289,16 @@ } return rv; } + + @Override + public String toString() { + if (s2DeletePoint == -1) { + return String.format("@@ -%d,0 +%d,%d @@", insertedAt(), firstAddedLine(), totalAddedLines()); + } else if (s1InsertPoint == -1) { + // delete only + return String.format("@@ -%d,%d +%d,0 @@", firstRemovedLine(), totalRemovedLines(), removedAt()); + } + return String.format("@@ -%d,%d +%d,%d @@", firstRemovedLine(), totalRemovedLines(), firstAddedLine(), totalAddedLines()); + } } }
--- a/src/org/tmatesoft/hg/internal/IntVector.java Fri Feb 15 16:48:54 2013 +0100 +++ b/src/org/tmatesoft/hg/internal/IntVector.java Fri Feb 15 22:15:13 2013 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 TMate Software Ltd + * Copyright (c) 2011-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 @@ -54,10 +54,21 @@ return data[i]; } + public void set(int i, int v) { + if (i < 0 || i >= count) { + throw new IndexOutOfBoundsException(String.format("Index: %d, size: %d", i, count)); + } + data[i] = v; + } + public int size() { return count; } + public boolean isEmpty() { + return count == 0; + } + public void clear() { count = 0; }
--- a/src/org/tmatesoft/hg/internal/PatchGenerator.java Fri Feb 15 16:48:54 2013 +0100 +++ b/src/org/tmatesoft/hg/internal/PatchGenerator.java Fri Feb 15 22:15:13 2013 +0100 @@ -146,14 +146,21 @@ } static class MatchDumpInspector<T extends ChunkSequence<?>> implements MatchInspector<T> { + private int matchCount; + public void begin(T s1, T s2) { + matchCount = 0; } public void match(int startSeq1, int startSeq2, int matchLength) { - System.out.printf("match: from line #%d and line #%d of length %d\n", startSeq1, startSeq2, matchLength); + matchCount++; + System.out.printf("match #%d: from line #%d and line #%d of length %d\n", matchCount, startSeq1, startSeq2, matchLength); } public void end() { + if (matchCount == 0) { + System.out.println("NO MATCHES FOUND!"); + } } } @@ -174,8 +181,8 @@ } public void end() { - if (changeStartS1 < seq1.chunkCount() || changeStartS2 < seq2.chunkCount()) { - reportDeltaElement(seq1.chunkCount(), seq2.chunkCount(), 0); + if (changeStartS1 < seq1.chunkCount()-1 || changeStartS2 < seq2.chunkCount()-1) { + reportDeltaElement(seq1.chunkCount()-1, seq2.chunkCount()-1, 0); } } @@ -190,7 +197,7 @@ } else { assert changeStartS1 == matchStartSeq1; if(changeStartS2 < matchStartSeq2) { - added(matchStartSeq1, changeStartS2, matchStartSeq2); + added(changeStartS1, changeStartS2, matchStartSeq2); } else { assert changeStartS2 == matchStartSeq2; System.out.printf("adjustent equal blocks %d, %d and %d,%d\n", changeStartS1, matchStartSeq1, changeStartS2, matchStartSeq2); @@ -237,12 +244,18 @@ protected void added(int s1InsertPoint, int s2From, int s2To) { System.out.printf("added [%d..%d) at %d\n", s2From, s2To, s1InsertPoint); } - + + @Override + protected void unchanged(int s1From, int s2From, int length) { + System.out.printf("same [%d..%d) and [%d..%d)\n", s1From, s1From + length, s2From, s2From + length); + } } public static void main(String[] args) throws Exception { PatchGenerator<LineSequence> pg1 = new PatchGenerator<LineSequence>(); - pg1.init(LineSequence.newlines("hello".getBytes()), LineSequence.newlines("hello\nworld".getBytes())); +// pg1.init(LineSequence.newlines("hello\nabc".getBytes()), LineSequence.newlines("hello\nworld".getBytes())); +// pg1.init(LineSequence.newlines("".getBytes()), LineSequence.newlines("hello\nworld".getBytes())); + pg1.init(LineSequence.newlines("hello\nworld".getBytes()), LineSequence.newlines("".getBytes())); pg1.findMatchingBlocks(new MatchDumpInspector<LineSequence>()); pg1.findMatchingBlocks(new DeltaDumpInspector<LineSequence>()); if (Boolean.FALSE.booleanValue()) { @@ -293,6 +306,7 @@ return new LineSequence(array).splitByNewlines(); } + // sequence ends with fake, empty line chunk public LineSequence splitByNewlines() { lines = new ArrayList<ByteChain>(); int lastStart = 0; @@ -312,7 +326,7 @@ lines.add(new ByteChain(lastStart, input.length)); } // empty chunk to keep offset of input end - lines.add(new ByteChain(input.length, input.length)); + lines.add(new ByteChain(input.length)); return this; } @@ -339,6 +353,16 @@ private final int start, end; private final int hash; + /** + * construct a chunk with a sole purpose to keep + * offset of the data end + */ + ByteChain(int offset) { + start = end = offset; + // ensure this chunk doesn't match trailing chunk of another sequence + hash = System.identityHashCode(this); + } + ByteChain(int s, int e) { start = s; end = e;
--- a/test/org/tmatesoft/hg/test/TestBlame.java Fri Feb 15 16:48:54 2013 +0100 +++ b/test/org/tmatesoft/hg/test/TestBlame.java Fri Feb 15 22:15:13 2013 +0100 @@ -16,19 +16,23 @@ */ package org.tmatesoft.hg.test; +import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION; + import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.util.Arrays; +import java.util.LinkedList; import java.util.regex.Pattern; import org.junit.Assert; import org.junit.Test; import org.tmatesoft.hg.internal.AnnotateFacility; import org.tmatesoft.hg.internal.AnnotateFacility.AddBlock; -import org.tmatesoft.hg.internal.AnnotateFacility.Block; import org.tmatesoft.hg.internal.AnnotateFacility.ChangeBlock; import org.tmatesoft.hg.internal.AnnotateFacility.DeleteBlock; +import org.tmatesoft.hg.internal.AnnotateFacility.EqualBlock; import org.tmatesoft.hg.internal.IntMap; +import org.tmatesoft.hg.internal.IntVector; import org.tmatesoft.hg.repo.HgDataFile; import org.tmatesoft.hg.repo.HgLookup; import org.tmatesoft.hg.repo.HgRepository; @@ -58,6 +62,29 @@ Assert.assertArrayEquals(expected, apiResult); } + @Test + public void testFileAnnotate() throws Exception { + HgRepository repo = new HgLookup().detectFromWorkingDir(); + final String fname = "src/org/tmatesoft/hg/internal/PatchGenerator.java"; + final int checkChangeset = 539; + HgDataFile df = repo.getFileNode(fname); + AnnotateFacility af = new AnnotateFacility(); + System.out.println("536 -> 539"); + af.annotateChange(df, checkChangeset, new DiffOutInspector(System.out)); + System.out.println("531 -> 536"); + af.annotateChange(df, 536, new DiffOutInspector(System.out)); + System.out.println(" -1 -> 531"); + af.annotateChange(df, 531, new DiffOutInspector(System.out)); + + FileAnnotation fa = new FileAnnotation(); + af.annotateChange(df, checkChangeset, fa); + af.annotateChange(df, 536, fa); + af.annotateChange(df, 531, fa); + for (int i = 0; i < fa.lineRevisions.length; i++) { + System.out.printf("%3d: %d\n", fa.lineRevisions[i], i+1); + } + } + private static String[] splitLines(CharSequence seq) { int lineCount = 0; for (int i = 0, x = seq.length(); i < x; i++) { @@ -101,9 +128,11 @@ } public static void main(String[] args) throws Exception { - System.out.println(Arrays.equals(new String[0], splitLines(""))); - System.out.println(Arrays.equals(new String[] { "abc" }, splitLines("abc"))); - new TestBlame().testSingleParentBlame(); +// System.out.println(Arrays.equals(new String[0], splitLines(""))); +// System.out.println(Arrays.equals(new String[] { "abc" }, splitLines("abc"))); +// System.out.println(Arrays.equals(new String[] { "a", "bc" }, splitLines("a\nbc"))); +// System.out.println(Arrays.equals(new String[] { "a", "bc" }, splitLines("a\nbc\n"))); + new TestBlame().testFileAnnotate(); } static class DiffOutInspector implements AnnotateFacility.Inspector { @@ -113,7 +142,7 @@ out = ps; } - public void same(Block block) { + public void same(EqualBlock block) { // nothing } @@ -171,4 +200,155 @@ } while (lineStart < seq.length()); } } + + private static class FileAnnotation implements AnnotateFacility.InspectorEx { + private int[] lineRevisions; + private LinkedList<DeleteBlock> deleted = new LinkedList<DeleteBlock>(); + private LinkedList<DeleteBlock> newDeleted = new LinkedList<DeleteBlock>(); + // keeps <startSeq1, startSeq2, len> of equal blocks + // XXX smth like IntSliceVector to access triples (or slices of any size, in fact) + // with easy indexing, e.g. #get(sliceIndex, indexWithinSlice) + // and vect.get(7,2) instead of vect.get(7*SIZEOF_SLICE+2) + private IntVector identical = new IntVector(20*3, 2*3); + private IntVector newIdentical = new IntVector(20*3, 2*3); + + public FileAnnotation() { + } + + public void start(int originLineCount, int targetLineCount) { + if (lineRevisions == null) { + lineRevisions = new int [targetLineCount]; + Arrays.fill(lineRevisions, NO_REVISION); + } + } + +// private static void ppp(IntVector v) { +// for (int i = 0; i < v.size(); i+= 3) { +// int len = v.get(i+2); +// System.out.printf("[%d..%d) == [%d..%d); ", v.get(i), v.get(i) + len, v.get(i+1), v.get(i+1) + len); +// } +// System.out.println(); +// } + + public void done() { + if (identical.size() > 0) { + // update line numbers of the intermediate target to point to ultimate target's line numbers + IntVector v = new IntVector(identical.size(), 2*3); + for (int i = 0; i < newIdentical.size(); i+= 3) { + int originLine = newIdentical.get(i); + int targetLine = newIdentical.get(i+1); + int length = newIdentical.get(i+2); + int startTargetLine = -1, startOriginLine = -1, c = 0; + for (int j = 0; j < length; j++) { + int lnInFinal = mapLineIndex(targetLine + j); + if (lnInFinal == -1 || (startTargetLine != -1 && lnInFinal != startTargetLine + c)) { + // the line is not among "same" in ultimate origin + // or belongs to another/next "same" chunk + if (startOriginLine == -1) { + continue; + } + v.add(startOriginLine); + v.add(startTargetLine); + v.add(c); + c = 0; + startOriginLine = startTargetLine = -1; + // fall-through to check if it's not complete miss but a next chunk + } + if (lnInFinal != -1) { + if (startOriginLine == -1) { + startOriginLine = originLine + j; + startTargetLine = lnInFinal; + c = 1; + } else { + assert lnInFinal == startTargetLine + c; + c++; + } + } + } + if (startOriginLine != -1) { + assert c > 0; + v.add(startOriginLine); + v.add(startTargetLine); + v.add(c); + } + } + newIdentical.clear(); + identical = v; + } else { + IntVector li = newIdentical; + newIdentical = identical; + identical = li; + } + LinkedList<DeleteBlock> ld = newDeleted; + deleted.clear(); + newDeleted = deleted; + deleted = ld; + } + + public void same(EqualBlock block) { + newIdentical.add(block.originStart()); + newIdentical.add(block.targetStart()); + newIdentical.add(block.length()); + } + + public void added(AddBlock block) { + for (int i = 0, ln = block.firstAddedLine(), x = block.totalAddedLines(); i < x; i++, ln++) { + int lnInFinal = mapLineIndex(ln); + if (lnInFinal != -1 && historyUnknown(lnInFinal)) { + lineRevisions[lnInFinal] = block.targetChangesetIndex(); + } + } + } + + public void changed(ChangeBlock block) { + deleted(block); + added(block); + } + + public void deleted(DeleteBlock block) { + newDeleted.add(block); + } + + private boolean historyUnknown(int lineNumber) { + return lineRevisions[lineNumber] == NO_REVISION; + } + + private boolean isDeleted(int line) { + for (DeleteBlock b : deleted) { + if (b.firstRemovedLine() > line) { + break; + } + // line >= b.firstRemovedLine + if (b.firstRemovedLine() + b.totalRemovedLines() > line) { + return true; + } + } + return false; + } + + // map target lines to the lines of the revision being annotated (the one that came first) + private int mapLineIndex(int ln) { + if (isDeleted(ln)) { + return -1; + } + if (identical.isEmpty()) { + return ln; + } + for (int i = 0; i < identical.size(); i += 3) { + final int originStart = identical.get(i); + if (originStart > ln) { + assert false; + return -1; + } + // ln >= b.originStart + final int length = identical.get(i+2); + if (originStart + length > ln) { + int targetStart = identical.get(i+1); + return targetStart + (ln - originStart); + } + } + assert false; + return -1; + } + } }