# HG changeset patch # User Artem Tikhomirov # Date 1360856173 -3600 # Node ID a71a05ec11bc0b04bfd6d5137f1974b782836abb # Parent 946b131962521f9199e1fedbdc2487d3aaef5e46 Towards annotate/blame support: general outline of the functionality diff -r 946b13196252 -r a71a05ec11bc src/org/tmatesoft/hg/internal/AnnotateFacility.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/AnnotateFacility.java Thu Feb 14 16:36:13 2013 +0100 @@ -0,0 +1,196 @@ +/* + * 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 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.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgInvalidStateException; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.CancelledException; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@Experimental(reason="work in progress") +public class AnnotateFacility { + + public void annotate(HgDataFile df, int changestRevisionIndex, Inspector insp) { + Nodeid fileRev = df.getRepo().getManifest().getFileRevision(changestRevisionIndex, 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 { + ByteArrayChannel c1, c2; + df.content(soleParent, c1 = new ByteArrayChannel()); + df.content(fileRevIndex, c2 = new ByteArrayChannel()); + int parentChangesetRevIndex = df.getChangesetRevisionIndex(soleParent); + PatchGenerator pg = new PatchGenerator(); + pg.init(c1.toArray(), 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; + } + } + } + + @Callback + public interface Inspector { + void same(Block block); + void added(AddBlock block); + void changed(ChangeBlock block); + void deleted(DeleteBlock block); + } + + public interface Block { +// boolean isMergeRevision(); +// int fileRevisionIndex(); +// int originFileRevisionIndex(); +// String[] lines(); +// byte[] data(); + } + + public interface AddBlock extends Block { + int firstAddedLine(); + int totalAddedLines(); + String[] addedLines(); + } + public interface DeleteBlock extends Block { + int firstRemovedLine(); + int totalRemovedLines(); + String[] removedLines(); + } + public interface ChangeBlock extends AddBlock, DeleteBlock { + } + + static class BlameBlockInspector extends PatchGenerator.DeltaInspector { + private final Inspector insp; + + public BlameBlockInspector(Inspector inspector) { + assert inspector != null; + insp = inspector; + } + + @Override + protected void changed(int s1From, int s1To, int s2From, int s2To) { + insp.changed(new BlockImpl2(seq1, seq2, s1From, s1To-s1From, s2From, s2To - s2From)); + } + + @Override + protected void added(int s1InsertPoint, int s2From, int s2To) { + insp.added(new BlockImpl2(null, seq2, -1, -1, s2From, s2To - s2From)); + } + + @Override + protected void deleted(int s1From, int s1To) { + insp.deleted(new BlockImpl2(seq1, null, s1From, s1To - s1From, -1, -1)); + } + + @Override + protected void unchanged(int s1From, int s2From, int length) { + insp.same(new BlockImpl(seq2, s2From, length)); + } + } + + static class BlockImpl implements Block { + private final ChunkSequence seq; + private final int start; + private final int length; + + BlockImpl() { + // FIXME delete this cons + seq = null; + start = length = -1; + } + + BlockImpl(ChunkSequence s, int blockStart, int blockLength) { + seq = s; + start = blockStart; + length = blockLength; + } + + } + + static class BlockImpl2 implements ChangeBlock { + + private final ChunkSequence oldSeq; + private final ChunkSequence newSeq; + private final int s1Start; + private final int s1Len; + private final int s2Start; + private final int s2Len; + + public BlockImpl2(ChunkSequence s1, ChunkSequence s2, int s1Start, int s1Len, int s2Start, int s2Len) { + oldSeq = s1; + newSeq = s2; + this.s1Start = s1Start; + this.s1Len = s1Len; + this.s2Start = s2Start; + this.s2Len = s2Len; + } + + public int firstAddedLine() { + return s2Start; + } + + public int totalAddedLines() { + return s2Len; + } + + public String[] addedLines() { + return generateLines(totalAddedLines(), firstAddedLine()); + } + + public int firstRemovedLine() { + return s1Start; + } + + public int totalRemovedLines() { + return s1Len; + } + + public String[] removedLines() { + return generateLines(totalRemovedLines(), firstRemovedLine()); + } + + private String[] generateLines(int count, int startFrom) { + String[] rv = new String[count]; + for (int i = 0; i < count; i++) { + rv[i] = String.format("LINE %d", startFrom + i); + } + return rv; + } + } +} diff -r 946b13196252 -r a71a05ec11bc src/org/tmatesoft/hg/internal/PatchGenerator.java --- a/src/org/tmatesoft/hg/internal/PatchGenerator.java Wed Feb 13 19:42:22 2013 +0100 +++ b/src/org/tmatesoft/hg/internal/PatchGenerator.java Thu Feb 14 16:36:13 2013 +0100 @@ -158,7 +158,7 @@ } } - static class DeltaDumpInspector implements MatchInspector { + static class DeltaInspector implements MatchInspector { protected int changeStartS1, changeStartS2; protected ChunkSequence seq1, seq2; @@ -170,38 +170,79 @@ } public void match(int startSeq1, int startSeq2, int matchLength) { - reportDeltaElement(startSeq1, startSeq2); + reportDeltaElement(startSeq1, startSeq2, matchLength); changeStartS1 = startSeq1 + matchLength; changeStartS2 = startSeq2 + matchLength; } public void end() { if (changeStartS1 < seq1.chunkCount() || changeStartS2 < seq2.chunkCount()) { - reportDeltaElement(seq1.chunkCount(), seq2.chunkCount()); + reportDeltaElement(seq1.chunkCount(), seq2.chunkCount(), 0); } } - protected void reportDeltaElement(int matchStartSeq1, int matchStartSeq2) { + protected void reportDeltaElement(int matchStartSeq1, int matchStartSeq2, int matchLength) { if (changeStartS1 < matchStartSeq1) { if (changeStartS2 < matchStartSeq2) { - System.out.printf("changed [%d..%d) with [%d..%d)\n", changeStartS1, matchStartSeq1, changeStartS2, matchStartSeq2); + changed(changeStartS1, matchStartSeq1, changeStartS2, matchStartSeq2); } else { assert changeStartS2 == matchStartSeq2; - System.out.printf("deleted [%d..%d)\n", changeStartS1, matchStartSeq1); + deleted(changeStartS1, matchStartSeq1); } } else { assert changeStartS1 == matchStartSeq1; if(changeStartS2 < matchStartSeq2) { - System.out.printf("added [%d..%d)\n", changeStartS2, matchStartSeq2); + added(matchStartSeq1, changeStartS2, matchStartSeq2); } else { assert changeStartS2 == matchStartSeq2; System.out.printf("adjustent equal blocks %d, %d and %d,%d\n", changeStartS1, matchStartSeq1, changeStartS2, matchStartSeq2); } } + if (matchLength > 0) { + unchanged(matchStartSeq1, matchStartSeq2, matchLength); + } + } + + /** + * [s1From..s1To) replaced with [s2From..s2To) + */ + protected void changed(int s1From, int s1To, int s2From, int s2To) { + // NO-OP + } + + protected void deleted(int s1From, int s1To) { + // NO-OP + } + + protected void added(int s1InsertPoint, int s2From, int s2To) { + // NO-OP + } + + protected void unchanged(int s1From, int s2From, int length) { + // NO-OP } } - static class PatchFillInspector extends DeltaDumpInspector { + static class DeltaDumpInspector extends DeltaInspector { + + @Override + protected void changed(int s1From, int s1To, int s2From, int s2To) { + System.out.printf("changed [%d..%d) with [%d..%d)\n", s1From, s1To, s2From, s2To); + } + + @Override + protected void deleted(int s1From, int s1To) { + System.out.printf("deleted [%d..%d)\n", s1From, s1To); + } + + @Override + protected void added(int s1InsertPoint, int s2From, int s2To) { + System.out.printf("added [%d..%d) at %d\n", s2From, s2To, s1InsertPoint); + } + + } + + static class PatchFillInspector extends DeltaInspector { private final Patch deltaCollector; PatchFillInspector(Patch p) { @@ -210,18 +251,25 @@ } @Override - protected void reportDeltaElement(int matchStartSeq1, int matchStartSeq2) { - if (changeStartS1 < matchStartSeq1) { - int from = seq1.chunk(changeStartS1).getOffset(); - int to = seq1.chunk(matchStartSeq1).getOffset(); - byte[] data = seq2.data(changeStartS2, matchStartSeq2); - deltaCollector.add(from, to, data); - } else { - assert changeStartS1 == matchStartSeq1; - int insPoint = seq1.chunk(changeStartS1).getOffset(); - byte[] data = seq2.data(changeStartS2, matchStartSeq2); - deltaCollector.add(insPoint, insPoint, data); - } + protected void changed(int s1From, int s1To, int s2From, int s2To) { + int from = seq1.chunk(s1From).getOffset(); + int to = seq1.chunk(s1To).getOffset(); + byte[] data = seq2.data(s2From, s2To); + deltaCollector.add(from, to, data); + } + + @Override + protected void deleted(int s1From, int s1To) { + int from = seq1.chunk(s1From).getOffset(); + int to = seq1.chunk(s1To).getOffset(); + deltaCollector.add(from, to, new byte[0]); + } + + @Override + protected void added(int s1InsertPoint, int s2From, int s2To) { + int insPoint = seq1.chunk(s1InsertPoint).getOffset(); + byte[] data = seq2.data(s2From, s2To); + deltaCollector.add(insPoint, insPoint, data); } } @@ -257,7 +305,11 @@ return rv; } - private static class ChunkSequence { + /* + * TODO shall be parameterized (template?) and refacctored to facilitate matching non lines only + * (sequence diff algorithm above doesn't care about sequence nature) + */ + static final class ChunkSequence { private final byte[] input; private ArrayList lines; diff -r 946b13196252 -r a71a05ec11bc test/org/tmatesoft/hg/test/TestBlame.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/test/org/tmatesoft/hg/test/TestBlame.java Thu Feb 14 16:36:13 2013 +0100 @@ -0,0 +1,91 @@ +/* + * 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 org.junit.Test; +import org.tmatesoft.hg.internal.AnnotateFacility; +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.IntMap; +import org.tmatesoft.hg.internal.AnnotateFacility.Block; +import org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgLookup; +import org.tmatesoft.hg.repo.HgRepository; + +/** + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class TestBlame { + + + @Test + public void testSingleParentBlame() throws Exception { + HgRepository repo = new HgLookup().detectFromWorkingDir(); + HgDataFile df = repo.getFileNode("src/org/tmatesoft/hg/internal/PatchGenerator.java"); + final IntMap linesOld= new IntMap(100); + final IntMap linesNew = new IntMap(100); + new AnnotateFacility().annotate(df, 539, new AnnotateFacility.Inspector() { + + public void same(Block block) { + // TODO Auto-generated method stub + + } + + public void deleted(DeleteBlock block) { + String[] lines = block.removedLines(); + assert lines.length == block.totalRemovedLines(); + for (int i = 0, ln = block.firstRemovedLine(); i < lines.length; i++, ln++) { + linesOld.put(ln, String.format("%3d:---:%s", ln, lines[i])); + } + } + + public void changed(ChangeBlock block) { + deleted(block); + added(block); + } + + public void added(AddBlock block) { + String[] addedLines = block.addedLines(); + assert addedLines.length == block.totalAddedLines(); + for (int i = 0, ln = block.firstAddedLine(), x = addedLines.length; i < x; i++, ln++) { + linesNew.put(ln, String.format("%3d:+++:%s", ln, addedLines[i])); + } + } + }); + + System.out.println("Changes to old revision:"); + for (int i = linesOld.firstKey(), x = linesOld.lastKey(); i < x; i++) { + if (linesOld.containsKey(i)) { + System.out.println(linesOld.get(i)); + } + } + + System.out.println("Changes in the new revision:"); + for (int i = linesNew.firstKey(), x = linesNew.lastKey(); i < x; i++) { + if (linesNew.containsKey(i)) { + System.out.println(linesNew.get(i)); + } + } + } + + public static void main(String[] args) throws Exception { + new TestBlame().testSingleParentBlame(); + } +}