Mercurial > jhg
view test/org/tmatesoft/hg/test/TestBlame.java @ 573:e49f9d9513fa
Partial blame when start/end revisions are in the middle of a single filename history
author | Artem Tikhomirov <tikhomirov.artem@gmail.com> |
---|---|
date | Fri, 12 Apr 2013 19:50:21 +0200 |
parents | 36853bb80a35 |
children | 707b5c7c6fa4 |
line wrap: on
line source
/* * 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.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 static org.tmatesoft.hg.repo.HgRepository.NO_REVISION; import static org.tmatesoft.hg.repo.HgRepository.TIP; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.tmatesoft.hg.core.HgAnnotateCommand; import org.tmatesoft.hg.core.HgAnnotateCommand.LineInfo; import org.tmatesoft.hg.core.HgCallbackTargetException; import org.tmatesoft.hg.core.HgIterateDirection; import org.tmatesoft.hg.core.HgRepoFacade; import org.tmatesoft.hg.internal.FileAnnotation; import org.tmatesoft.hg.internal.FileAnnotation.LineDescriptor; import org.tmatesoft.hg.internal.FileAnnotation.LineInspector; import org.tmatesoft.hg.internal.IntVector; import org.tmatesoft.hg.repo.HgBlameFacility; import org.tmatesoft.hg.repo.HgBlameFacility.AddBlock; import org.tmatesoft.hg.repo.HgBlameFacility.Block; import org.tmatesoft.hg.repo.HgBlameFacility.BlockData; import org.tmatesoft.hg.repo.HgBlameFacility.ChangeBlock; import org.tmatesoft.hg.repo.HgBlameFacility.DeleteBlock; import org.tmatesoft.hg.repo.HgBlameFacility.EqualBlock; import org.tmatesoft.hg.repo.HgDataFile; import org.tmatesoft.hg.repo.HgLookup; import org.tmatesoft.hg.repo.HgRepository; import org.tmatesoft.hg.util.Path; /** * * @author Artem Tikhomirov * @author TMate Software Ltd. */ public class TestBlame { @Rule public ErrorCollectorExt errorCollector = new ErrorCollectorExt(); @Test public void testSingleParentBlame() 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); ByteArrayOutputStream bos = new ByteArrayOutputStream(); new HgBlameFacility(df).annotateSingleRevision(checkChangeset, new DiffOutInspector(new PrintStream(bos))); LineGrepOutputParser gp = new LineGrepOutputParser("^@@.+"); ExecHelper eh = new ExecHelper(gp, null); eh.run("hg", "diff", "-c", String.valueOf(checkChangeset), "-U", "0", fname); // String[] apiResult = splitLines(bos.toString()); String[] expected = splitLines(gp.result()); Assert.assertArrayEquals(expected, apiResult); } @Test public void testFileLineAnnotate1() throws Exception { HgRepository repo = new HgLookup().detectFromWorkingDir(); final String fname = "src/org/tmatesoft/hg/internal/PatchGenerator.java"; HgDataFile df = repo.getFileNode(fname); AnnotateRunner ar = new AnnotateRunner(df.getPath(), null); for (int cs : new int[] { 539, 541 /*, TIP */}) { ar.run(cs, false); FileAnnotateInspector fa = new FileAnnotateInspector(); FileAnnotation.annotate(df, cs, fa); doAnnotateLineCheck(cs, ar.getLines(), Arrays.asList(fa.lineRevisions), Arrays.asList(fa.lines)); } } @Test public void testFileLineAnnotate2() throws Exception { HgRepository repo = Configuration.get().find("test-annotate"); HgDataFile df = repo.getFileNode("file1"); AnnotateRunner ar = new AnnotateRunner(df.getPath(), repo.getWorkingDir()); for (int cs : new int[] { 4, 6 /*, 8 see below*/, TIP}) { ar.run(cs, false); FileAnnotateInspector fa = new FileAnnotateInspector(); FileAnnotation.annotate(df, cs, fa); doAnnotateLineCheck(cs, ar.getLines(), Arrays.asList(fa.lineRevisions), Arrays.asList(fa.lines)); } /*`hg annotate -r 8` and HgBlameFacility give different result * for "r0, line 5" line, which was deleted in rev2 and restored back in * rev4 (both in default branch), while branch with r3 and r6 kept the line intact. * HgBlame reports rev4 for the line, `hg annotate` gives original, rev0. * However `hg annotate -r 4` shows rev4 for the line, too. The aforementioned rev0 for * the merge rev8 results from the iteration order and is implementation specific * (i.e. one can't tell which one is right). Mercurial walks from parents to children, * and traces equal lines, wile HgBlameFacility walks from child to parents and records * changes (additions). Seems it processes branch with rev3 and rev6 first * (printout in context.py, annotate and annotate.pair reveals that), and the line 0_5 * comes as unchanged through this branch, and later processing rev2 and rev4 doesn't * change that. */ } @Test public void testComplexHistoryAnnotate() throws Exception { HgRepository repo = Configuration.get().find("test-annotate"); HgDataFile df = repo.getFileNode("file1"); HgBlameFacility af = new HgBlameFacility(df); ByteArrayOutputStream bos = new ByteArrayOutputStream(); DiffOutInspector dump = new DiffOutInspector(new PrintStream(bos)); af.annotate(TIP, dump, HgIterateDirection.OldToNew); LinkedList<String> apiResult = new LinkedList<String>(Arrays.asList(splitLines(bos.toString()))); /* * FIXME this is an ugly hack to deal with the way `hg diff -c <mergeRev>` describes the change * and our merge handling approach. For merged revision m, and lines changed both in p1 and p2 * we report lines from p2 as pure additions, regardless of intersecting p1 changes (which * are reported as deletions, if no sufficient changed lines in m found) * So, here we try to combine deletion that follows a change (based on identical insertionPoint) * into a single change * To fix, need to find better approach to find out reference info (i.e. `hg diff -c` is flawed in this case, * as it uses first parent only). */ Pattern fix = Pattern.compile("@@ -(\\d+),(\\d+) \\+(\\d+),(\\d+) @@"); int v1, v2, v3, v4; v1 = v2 = v3 = v4 = -1; for (ListIterator<String> it = apiResult.listIterator(); it.hasNext();) { String n = it.next(); Matcher m = fix.matcher(n); if (m.find()) { int d1 = Integer.parseInt(m.group(1)); int d2 = Integer.parseInt(m.group(2)); int d3 = Integer.parseInt(m.group(3)); int d4 = Integer.parseInt(m.group(4)); if (v1 == d1 && d4 == 0) { it.previous(); // shift to current element it.previous(); // to real previous it.remove(); it.next(); it.set(String.format("@@ -%d,%d +%d,%d @@", v1, v2+d2, v3, v4)); } v1 = d1; v2 = d2; v3 = d3; v4 = d4; } } LineGrepOutputParser gp = new LineGrepOutputParser("^@@.+"); ExecHelper eh = new ExecHelper(gp, repo.getWorkingDir()); for (int cs : dump.getReportedTargetRevisions()) { gp.reset(); eh.run("hg", "diff", "-c", String.valueOf(cs), "-U", "0", df.getPath().toString()); for (String expected : splitLines(gp.result())) { if (!apiResult.remove(expected)) { errorCollector.fail(String.format("Expected diff output '%s' for changes in revision %d", expected, cs)); } } } errorCollector.assertTrue(String.format("Annotate API reported excessive diff: %s ", apiResult.toString()), apiResult.isEmpty()); } @Test public void testPartialHistoryFollow() throws Exception { HgRepository repo = Configuration.get().find("test-annotate2"); HgDataFile df = repo.getFileNode("file1b.txt"); // rev3: file1 -> file1a, rev7: file1a -> file1b, tip: rev10 HgBlameFacility bf = new HgBlameFacility(df); DiffOutInspector insp = new DiffOutInspector(new PrintStream(new OutputStream() { @Override public void write(int b) throws IOException { // NULL OutputStream } })); // rev6 changes rev4, rev4 changes rev3. Plus, anything changed // earlier than rev2 shall be reported as new from change3 int[] change_2_8_new2old = new int[] {4, 6, 3, 4, -1, 3}; int[] change_2_8_old2new = new int[] {-1, 3, 3, 4, 4, 6 }; bf.annotate(2, 8, insp, NewToOld); Assert.assertArrayEquals(change_2_8_new2old, insp.getReportedRevisionPairs()); insp.reset(); bf.annotate(2, 8, insp, OldToNew); Assert.assertArrayEquals(change_2_8_old2new, insp.getReportedRevisionPairs()); // same as 2 to 8, with addition of rev9 changes rev7 (rev6 to rev7 didn't change content, only name) int[] change_3_9_new2old = new int[] {7, 9, 4, 6, 3, 4, -1, 3 }; int[] change_3_9_old2new = new int[] {-1, 3, 3, 4, 4, 6, 7, 9 }; insp.reset(); bf.annotate(3, 9, insp, NewToOld); Assert.assertArrayEquals(change_3_9_new2old, insp.getReportedRevisionPairs()); insp.reset(); bf.annotate(3, 9, insp, OldToNew); Assert.assertArrayEquals(change_3_9_old2new, insp.getReportedRevisionPairs()); } @Test public void testAnnotateCmdFollowNoFollow() throws Exception { HgRepoFacade hgRepoFacade = new HgRepoFacade(); HgRepository repo = Configuration.get().find("test-annotate2"); hgRepoFacade.init(repo); HgAnnotateCommand cmd = hgRepoFacade.createAnnotateCommand(); final Path fname = Path.create("file1b.txt"); final int changeset = TIP; AnnotateInspector ai = new AnnotateInspector(); cmd.changeset(changeset); // follow cmd.file(fname); cmd.execute(ai); AnnotateRunner ar = new AnnotateRunner(fname, repo.getWorkingDir()); ar.run(changeset, true); doAnnotateLineCheck(changeset, ar.getLines(), ai.changesets, ai.lines); // no follow cmd.file(fname, false); ai = new AnnotateInspector(); cmd.execute(ai); ar.run(changeset, false); doAnnotateLineCheck(changeset, ar.getLines(), ai.changesets, ai.lines); } private void doAnnotateLineCheck(int cs, String[] hgAnnotateLines, List<Integer> cmdChangesets, List<String> cmdLines) { assertTrue("[sanity]", hgAnnotateLines.length > 0); assertEquals("Number of lines reported by native annotate and our impl", hgAnnotateLines.length, cmdLines.size()); for (int i = 0; i < cmdChangesets.size(); i++) { int hgAnnotateRevIndex = Integer.parseInt(hgAnnotateLines[i].substring(0, hgAnnotateLines[i].indexOf(':')).trim()); errorCollector.assertEquals(String.format("Revision mismatch for line %d (annotating rev: %d)", i+1, cs), hgAnnotateRevIndex, cmdChangesets.get(i)); String hgAnnotateLine = hgAnnotateLines[i].substring(hgAnnotateLines[i].indexOf(':') + 1); String apiLine = cmdLines.get(i).trim(); errorCollector.assertEquals(hgAnnotateLine.trim(), apiLine); } } // FIXME 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 // while original diff lib does. Would be nice to behave as close to original, as possible. private static String[] splitLines(CharSequence seq) { int lineCount = 0; for (int i = 0, x = seq.length(); i < x; i++) { if (seq.charAt(i) == '\n') { lineCount++; } } if (seq.length() > 0 && seq.charAt(seq.length()-1) != '\n') { lineCount++; } String[] rv = new String[lineCount]; int lineStart = 0, lineEnd = 0, ix = 0; do { while (lineEnd < seq.length() && seq.charAt(lineEnd) != '\n') lineEnd++; if (lineEnd == lineStart) { continue; } CharSequence line = seq.subSequence(lineStart, lineEnd); rv[ix++] = line.toString(); lineStart = ++lineEnd; } while (lineStart < seq.length()); assert ix == lineCount; return rv; } private void aaa() 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); HgBlameFacility af = new HgBlameFacility(df); DiffOutInspector dump = new DiffOutInspector(System.out); System.out.println("541 -> 543"); af.annotateSingleRevision(543, dump); System.out.println("539 -> 541"); af.annotateSingleRevision(541, dump); System.out.println("536 -> 539"); af.annotateSingleRevision(checkChangeset, dump); System.out.println("531 -> 536"); af.annotateSingleRevision(536, dump); System.out.println(" -1 -> 531"); af.annotateSingleRevision(531, dump); FileAnnotateInspector fai = new FileAnnotateInspector(); FileAnnotation.annotate(df, 541, fai); for (int i = 0; i < fai.lineRevisions.length; i++) { System.out.printf("%3d: LINE %d\n", fai.lineRevisions[i], i+1); } } private void bbb() throws Exception { HgRepository repo = new HgLookup().detectFromWorkingDir(); final String fname = "src/org/tmatesoft/hg/repo/HgManifest.java"; final int checkChangeset = 415; HgDataFile df = repo.getFileNode(fname); HgBlameFacility af = new HgBlameFacility(df); DiffOutInspector dump = new DiffOutInspector(System.out); // System.out.println("413 -> 415"); // af.diff(df, 413, 415, dump); // System.out.println("408 -> 415"); // af.diff(df, 408, 415, dump); // System.out.println("Combined (with merge):"); // dump.needRevisions(true); // af.annotateChange(df, checkChangeset, dump); dump.needRevisions(true); af.annotate(checkChangeset, dump, HgIterateDirection.OldToNew); } private void ccc() throws Throwable { HgRepository repo = new HgLookup().detect("/home/artem/hg/hgtest-annotate-merge/"); HgDataFile df = repo.getFileNode("file.txt"); HgBlameFacility af = new HgBlameFacility(df); DiffOutInspector dump = new DiffOutInspector(System.out); dump.needRevisions(true); af.annotate(8, dump, HgIterateDirection.NewToOld); // af.annotateSingleRevision(df, 113, dump); // System.out.println(); // af.annotate(df, TIP, new LineDumpInspector(true), HgIterateDirection.NewToOld); // System.out.println(); // af.annotate(df, TIP, new LineDumpInspector(false), HgIterateDirection.NewToOld); // System.out.println(); /* OutputParser.Stub op = new OutputParser.Stub(); eh = new ExecHelper(op, repo.getWorkingDir()); for (int cs : new int[] { 24, 46, 49, 52, 59, 62, 64, TIP}) { doLineAnnotateTest(df, cs, op); } errorCollector.verify(); */ FileAnnotateInspector fa = new FileAnnotateInspector(); FileAnnotation.annotate(df, 8, fa); for (int i = 0; i < fa.lineRevisions.length; i++) { System.out.printf("%d: %s", fa.lineRevisions[i], fa.line(i) == null ? "null\n" : fa.line(i)); } } public static void main(String[] args) throws Throwable { // 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"))); TestBlame tt = new TestBlame(); tt.ccc(); } private static class DiffOutInspector implements HgBlameFacility.Inspector { private final PrintStream out; private boolean dumpRevs; private IntVector reportedRevisionPairs = new IntVector(); DiffOutInspector(PrintStream ps) { out = ps; } // Note, true makes output incompatible with 'hg diff' public void needRevisions(boolean dumpRevs) { this.dumpRevs = dumpRevs; } private void printRevs(Block b) { if (dumpRevs) { out.printf("[%3d -> %3d] ", b.originChangesetIndex(), b.targetChangesetIndex()); } reportedRevisionPairs.add(b.originChangesetIndex(), b.targetChangesetIndex()); } int[] getReportedTargetRevisions() { LinkedHashSet<Integer> rv = new LinkedHashSet<Integer>(); for (int i = 1; i < reportedRevisionPairs.size(); i += 2) { rv.add(reportedRevisionPairs.get(i)); } int[] x = new int[rv.size()]; int i = 0; for (int v : rv) { x[i++] = v; } return x; } int[] getReportedRevisionPairs() { return reportedRevisionPairs.toArray(); } void reset() { reportedRevisionPairs.clear(); } public void same(EqualBlock block) { // nothing } public void deleted(DeleteBlock block) { printRevs(block); out.printf("@@ -%d,%d +%d,0 @@\n", block.firstRemovedLine() + 1, block.totalRemovedLines(), block.removedAt()); } public void changed(ChangeBlock block) { printRevs(block); out.printf("@@ -%d,%d +%d,%d @@\n", block.firstRemovedLine() + 1, block.totalRemovedLines(), block.firstAddedLine() + 1, block.totalAddedLines()); } public void added(AddBlock block) { printRevs(block); out.printf("@@ -%d,0 +%d,%d @@\n", block.insertedAt(), block.firstAddedLine() + 1, block.totalAddedLines()); } } public static class LineGrepOutputParser implements OutputParser { private final Pattern pattern; private final StringBuilder result = new StringBuilder(); public LineGrepOutputParser(String regexp) { pattern = Pattern.compile(regexp); } public void reset() { result.setLength(0); } public CharSequence result() { return result; } public void parse(CharSequence seq) { int lineStart = 0, lineEnd = 0; do { while (lineEnd < seq.length() && seq.charAt(lineEnd) != '\n') lineEnd++; if (lineEnd == lineStart) { continue; } CharSequence line = seq.subSequence(lineStart, lineEnd); if (pattern.matcher(line).matches()) { result.append(line); result.append('\n'); } lineStart = ++lineEnd; } while (lineStart < seq.length()); } } private static class FileAnnotateInspector implements LineInspector { private Integer[] lineRevisions; private String[] lines; FileAnnotateInspector() { } public void line(int lineNumber, int changesetRevIndex, BlockData lineContent, LineDescriptor ld) { if (lineRevisions == null) { lineRevisions = new Integer[ld.totalLines()]; Arrays.fill(lineRevisions, NO_REVISION); lines = new String[ld.totalLines()]; } lineRevisions[lineNumber] = changesetRevIndex; lines[lineNumber] = new String(lineContent.asArray()); } public String line(int i) { return lines[i]; } } private static class LineDumpInspector implements HgBlameFacility.Inspector { private final boolean lineByLine; public LineDumpInspector(boolean lineByLine) { this.lineByLine = lineByLine; } public void same(EqualBlock block) { } public void added(AddBlock block) { BlockData lines = block.addedLines(); printBlock(lines, block.targetChangesetIndex(), block.firstAddedLine(), block.totalAddedLines(), "+++"); } public void changed(ChangeBlock block) { deleted(block); added(block); } public void deleted(DeleteBlock block) { BlockData lines = block.removedLines(); assert lines.elementCount() == block.totalRemovedLines(); printBlock(lines, block.originChangesetIndex(), block.firstRemovedLine(), block.totalRemovedLines(), "---"); } private void printBlock(BlockData lines, int cset, int first, int length, String marker) { assert lines.elementCount() == length; if (lineByLine) { for (int i = 0, ln = first; i < length; i++, ln++) { String line = new String(lines.elementAt(i).asArray()); System.out.printf("%3d:%3d:%s:%s", cset, ln, marker, line); } } else { String content = new String(lines.asArray()); System.out.printf("%3d:%s:[%d..%d):\n%s", cset, marker, first, first+length, content); } } } static class AnnotateInspector implements HgAnnotateCommand.Inspector { private int lineNumber = 1; public final ArrayList<String> lines = new ArrayList<String>(); public final ArrayList<Integer> changesets = new ArrayList<Integer>(); public void next(LineInfo lineInfo) throws HgCallbackTargetException { Assert.assertEquals(lineInfo.getLineNumber(), lineNumber); lineNumber++; lines.add(new String(lineInfo.getContent())); changesets.add(lineInfo.getChangesetIndex()); } } private static class AnnotateRunner { private final ExecHelper eh; private final OutputParser.Stub op; private final Path file; public AnnotateRunner(Path filePath, File repoDir) { file = filePath; op = new OutputParser.Stub(); eh = new ExecHelper(op, repoDir); } public void run(int cset, boolean follow) throws Exception { op.reset(); ArrayList<String> args = new ArrayList<String>(); args.add("hg"); args.add("annotate"); args.add("-r"); args.add(cset == TIP ? "tip" : String.valueOf(cset)); if (!follow) { args.add("--no-follow"); } args.add(file.toString()); eh.run(args); } public String[] getLines() { return splitLines(op.result()); } } }