# HG changeset patch # User Artem Tikhomirov # Date 1361213931 -3600 # Node ID ab21ac7dd83327cfd4941da83eb5dd53b863b9c0 # Parent 66fc86e8c0dd523916a8c837fdbbf27029654e28 Line-by-line annotation API and support code in place diff -r 66fc86e8c0dd -r ab21ac7dd833 src/org/tmatesoft/hg/internal/AnnotateFacility.java --- a/src/org/tmatesoft/hg/internal/AnnotateFacility.java Mon Feb 18 19:58:10 2013 +0100 +++ b/src/org/tmatesoft/hg/internal/AnnotateFacility.java Mon Feb 18 19:58:51 2013 +0100 @@ -17,11 +17,13 @@ package org.tmatesoft.hg.internal; import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION; +import static org.tmatesoft.hg.repo.HgRepository.TIP; import org.tmatesoft.hg.core.Nodeid; 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; /** @@ -31,7 +33,27 @@ */ @Experimental(reason="work in progress") public class AnnotateFacility { - + + /** + * Annotate file revision, line by line. + */ + public void annotate(HgDataFile df, int changesetRevisionIndex, LineInspector insp) { + if (!df.exists()) { + return; + } + Nodeid fileRev = df.getRepo().getManifest().getFileRevision(changesetRevisionIndex, df.getPath()); + int fileRevIndex = df.getRevisionIndex(fileRev); + int[] fileRevParents = new int[2]; + FileAnnotation fa = new FileAnnotation(insp); + do { + // also covers changesetRevisionIndex == TIP, #implAnnotateChange doesn't tolerate constants + changesetRevisionIndex = df.getChangesetRevisionIndex(fileRevIndex); + df.parents(fileRevIndex, fileRevParents, null, null); + implAnnotateChange(df, changesetRevisionIndex, fileRevIndex, fileRevParents, fa); + fileRevIndex = fileRevParents[0]; + } while (fileRevIndex != NO_REVISION); + } + /** * Annotates changes of the file against its parent(s) */ @@ -41,22 +63,29 @@ int fileRevIndex = df.getRevisionIndex(fileRev); int[] fileRevParents = new int[2]; df.parents(fileRevIndex, fileRevParents, null, null); + if (changesetRevisionIndex == TIP) { + changesetRevisionIndex = df.getChangesetRevisionIndex(fileRevIndex); + } + implAnnotateChange(df, changesetRevisionIndex, fileRevIndex, fileRevParents, insp); + } + + private void implAnnotateChange(HgDataFile df, int csetRevIndex, int fileRevIndex, int[] fileParentRevs, BlockInspector insp) { try { - if (fileRevParents[0] != NO_REVISION && fileRevParents[1] != NO_REVISION) { + if (fileParentRevs[0] != NO_REVISION && fileParentRevs[1] != NO_REVISION) { // merge - } else if (fileRevParents[0] == fileRevParents[1]) { + } else if (fileParentRevs[0] == fileParentRevs[1]) { // may be equal iff both are unset - assert fileRevParents[0] == NO_REVISION; + assert fileParentRevs[0] == NO_REVISION; // everything added ByteArrayChannel c; df.content(fileRevIndex, c = new ByteArrayChannel()); - BlameBlockInspector bbi = new BlameBlockInspector(insp, NO_REVISION, changesetRevisionIndex); + BlameBlockInspector bbi = new BlameBlockInspector(insp, NO_REVISION, csetRevIndex); 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]; + int soleParent = fileParentRevs[0] == NO_REVISION ? fileParentRevs[1] : fileParentRevs[0]; assert soleParent != NO_REVISION; ByteArrayChannel c1, c2; df.content(soleParent, c1 = new ByteArrayChannel()); @@ -64,7 +93,7 @@ int parentChangesetRevIndex = df.getChangesetRevisionIndex(soleParent); PatchGenerator pg = new PatchGenerator(); pg.init(LineSequence.newlines(c1.toArray()), LineSequence.newlines(c2.toArray())); - pg.findMatchingBlocks(new BlameBlockInspector(insp, parentChangesetRevIndex, changesetRevisionIndex)); + pg.findMatchingBlocks(new BlameBlockInspector(insp, parentChangesetRevIndex, csetRevIndex)); } } catch (CancelledException ex) { // TODO likely it was bad idea to throw cancelled exception from content() @@ -123,6 +152,9 @@ @Callback public interface LineInspector { + /** + * Not necessarily invoked sequentially by line numbers + */ void line(int lineNumber, int changesetRevIndex, LineDescriptor ld); } diff -r 66fc86e8c0dd -r ab21ac7dd833 src/org/tmatesoft/hg/internal/FileAnnotation.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/FileAnnotation.java Mon Feb 18 19:58:51 2013 +0100 @@ -0,0 +1,195 @@ +/* + * 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.LinkedList; + +import org.tmatesoft.hg.internal.AnnotateFacility.AddBlock; +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.AnnotateFacility.LineInspector; + + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class FileAnnotation implements AnnotateFacility.BlockInspectorEx { + // blocks deleted in the target, as reported at the previous step + private LinkedList deleted = new LinkedList(); + // blocks deleted in the origin, to become deletions in target at the next step + private LinkedList newDeleted = new LinkedList(); + // keeps of equal blocks, origin to target, from previous step + // 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); + // equal blocks of the current iteration, to be recalculated before next step + // to track line number (current target to ultimate target) mapping + private IntVector newIdentical = new IntVector(20*3, 2*3); + + private boolean[] knownLines; + private final LineInspector delegate; + + public FileAnnotation(AnnotateFacility.LineInspector lineInspector) { + delegate = lineInspector; + } + + public void start(int originLineCount, int targetLineCount) { + if (knownLines == null) { + knownLines = new boolean[targetLineCount]; + } + } + +// 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 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 && !knownLines[lnInFinal]) { + delegate.line(lnInFinal, block.targetChangesetIndex(), new LineDescriptor()); + knownLines[lnInFinal] = true; + } + } + } + + public void changed(ChangeBlock block) { + deleted(block); + added(block); + } + + public void deleted(DeleteBlock block) { + newDeleted.add(block); + } + + // line - index in the target + 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; + } + + private final class LineDescriptor implements AnnotateFacility.LineDescriptor { + LineDescriptor() { + } + + public int totalLines() { + return FileAnnotation.this.knownLines.length; + } + } + } \ No newline at end of file diff -r 66fc86e8c0dd -r ab21ac7dd833 test/org/tmatesoft/hg/test/FileAnnotation.java --- a/test/org/tmatesoft/hg/test/FileAnnotation.java Mon Feb 18 19:58:10 2013 +0100 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,196 +0,0 @@ -/* - * 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 java.util.LinkedList; - -import org.tmatesoft.hg.internal.AnnotateFacility; -import org.tmatesoft.hg.internal.AnnotateFacility.LineInspector; -import org.tmatesoft.hg.internal.IntVector; -import org.tmatesoft.hg.internal.AnnotateFacility.AddBlock; -import org.tmatesoft.hg.internal.AnnotateFacility.ChangeBlock; -import org.tmatesoft.hg.internal.AnnotateFacility.DeleteBlock; -import org.tmatesoft.hg.internal.AnnotateFacility.EqualBlock; - -/** - * - * @author Artem Tikhomirov - * @author TMate Software Ltd. - */ -public class FileAnnotation implements AnnotateFacility.BlockInspectorEx { - // blocks deleted in the target, as reported at the previous step - private LinkedList deleted = new LinkedList(); - // blocks deleted in the origin, to become deletions in target at the next step - private LinkedList newDeleted = new LinkedList(); - // keeps of equal blocks, origin to target, from previous step - // 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); - // equal blocks of the current iteration, to be recalculated before next step - // to track line number (current target to ultimate target) mapping - private IntVector newIdentical = new IntVector(20*3, 2*3); - - private boolean[] knownLines; - private final LineInspector delegate; - - public FileAnnotation(AnnotateFacility.LineInspector lineInspector) { - delegate = lineInspector; - } - - public void start(int originLineCount, int targetLineCount) { - if (knownLines == null) { - knownLines = new boolean[targetLineCount]; - } - } - -// 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 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 && !knownLines[lnInFinal]) { - delegate.line(lnInFinal, block.targetChangesetIndex(), new LineDescriptor()); - knownLines[lnInFinal] = true; - } - } - } - - public void changed(ChangeBlock block) { - deleted(block); - added(block); - } - - public void deleted(DeleteBlock block) { - newDeleted.add(block); - } - - // line - index in the target - 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; - } - - private final class LineDescriptor implements AnnotateFacility.LineDescriptor { - LineDescriptor() { - } - - public int totalLines() { - return FileAnnotation.this.knownLines.length; - } - } - } \ No newline at end of file diff -r 66fc86e8c0dd -r ab21ac7dd833 test/org/tmatesoft/hg/test/OutputParser.java --- a/test/org/tmatesoft/hg/test/OutputParser.java Mon Feb 18 19:58:10 2013 +0100 +++ b/test/org/tmatesoft/hg/test/OutputParser.java Mon Feb 18 19:58:51 2013 +0100 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2012 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 @@ -50,6 +50,9 @@ public CharSequence result() { return result; } + public void reset() { + result = null; + } public Iterable lines() { return lines(Pattern.compile("(.+)$", Pattern.MULTILINE), 1); diff -r 66fc86e8c0dd -r ab21ac7dd833 test/org/tmatesoft/hg/test/TestBlame.java --- a/test/org/tmatesoft/hg/test/TestBlame.java Mon Feb 18 19:58:10 2013 +0100 +++ b/test/org/tmatesoft/hg/test/TestBlame.java Mon Feb 18 19:58:51 2013 +0100 @@ -19,6 +19,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION; +import static org.tmatesoft.hg.repo.HgRepository.TIP; import java.io.ByteArrayOutputStream; import java.io.PrintStream; @@ -67,25 +68,26 @@ public void testFileAnnotate() throws Exception { HgRepository repo = new HgLookup().detectFromWorkingDir(); final String fname = "src/org/tmatesoft/hg/internal/PatchGenerator.java"; - final int[] checkChangesets = new int[] { 539 , 536, 531 }; HgDataFile df = repo.getFileNode(fname); - AnnotateFacility af = new AnnotateFacility(); - FileAnnotateInspector fa = new FileAnnotateInspector(); - for (int cs : checkChangesets) { - af.annotateChange(df, cs, new FileAnnotation(fa)); - } - OutputParser.Stub op = new OutputParser.Stub(); ExecHelper eh = new ExecHelper(op, null); - eh.run("hg", "annotate", "-r", String.valueOf(checkChangesets[0]), fname); - - String[] hgAnnotateLines = splitLines(op.result()); - assertTrue("[sanity]", hgAnnotateLines.length > 0); - assertEquals("Number of lines reported by native annotate and our impl", hgAnnotateLines.length, fa.lineRevisions.length); - for (int i = 0; i < fa.lineRevisions.length; i++) { - int hgAnnotateRevIndex = Integer.parseInt(hgAnnotateLines[i].substring(0, hgAnnotateLines[i].indexOf(':'))); - assertEquals(String.format("Revision mismatch for line %d", i+1), hgAnnotateRevIndex, fa.lineRevisions[i]); + for (int startChangeset : new int[] { 539, 541/*, TIP */}) { + FileAnnotateInspector fa = new FileAnnotateInspector(); + new AnnotateFacility().annotate(df, startChangeset, fa); + + + op.reset(); + eh.run("hg", "annotate", "-r", startChangeset == TIP ? "tip" : String.valueOf(startChangeset), fname); + + String[] hgAnnotateLines = splitLines(op.result()); + assertTrue("[sanity]", hgAnnotateLines.length > 0); + assertEquals("Number of lines reported by native annotate and our impl", hgAnnotateLines.length, fa.lineRevisions.length); + + for (int i = 0; i < fa.lineRevisions.length; i++) { + int hgAnnotateRevIndex = Integer.parseInt(hgAnnotateLines[i].substring(0, hgAnnotateLines[i].indexOf(':'))); + assertEquals(String.format("Revision mismatch for line %d", i+1), hgAnnotateRevIndex, fa.lineRevisions[i]); + } } }