tikhomirov@546: /* tikhomirov@546: * Copyright (c) 2013 TMate Software Ltd tikhomirov@546: * tikhomirov@546: * This program is free software; you can redistribute it and/or modify tikhomirov@546: * it under the terms of the GNU General Public License as published by tikhomirov@546: * the Free Software Foundation; version 2 of the License. tikhomirov@546: * tikhomirov@546: * This program is distributed in the hope that it will be useful, tikhomirov@546: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@546: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@546: * GNU General Public License for more details. tikhomirov@546: * tikhomirov@546: * For information on how to redistribute this software under tikhomirov@546: * the terms of a license other than GNU General Public License tikhomirov@546: * contact TMate Software at support@hg4j.com tikhomirov@546: */ tikhomirov@548: package org.tmatesoft.hg.internal; tikhomirov@546: tikhomirov@546: tikhomirov@555: import org.tmatesoft.hg.core.HgIterateDirection; tikhomirov@556: import org.tmatesoft.hg.repo.HgBlameFacility; tikhomirov@557: import org.tmatesoft.hg.repo.HgInvalidStateException; tikhomirov@557: import org.tmatesoft.hg.repo.HgBlameFacility.AddBlock; tikhomirov@557: import org.tmatesoft.hg.repo.HgBlameFacility.BlockData; tikhomirov@557: import org.tmatesoft.hg.repo.HgBlameFacility.ChangeBlock; tikhomirov@557: import org.tmatesoft.hg.repo.HgBlameFacility.DeleteBlock; tikhomirov@557: import org.tmatesoft.hg.repo.HgBlameFacility.EqualBlock; tikhomirov@557: import org.tmatesoft.hg.repo.HgBlameFacility.RevisionDescriptor; tikhomirov@555: import org.tmatesoft.hg.repo.HgDataFile; tikhomirov@546: tikhomirov@546: /** tikhomirov@555: * Produce output like 'hg annotate' does tikhomirov@546: * tikhomirov@546: * @author Artem Tikhomirov tikhomirov@546: * @author TMate Software Ltd. tikhomirov@546: */ tikhomirov@556: public class FileAnnotation implements HgBlameFacility.BlockInspector, RevisionDescriptor.Recipient { tikhomirov@546: tikhomirov@555: @Experimental(reason="The line-by-line inspector likely to become part of core/command API") tikhomirov@555: @Callback tikhomirov@555: public interface LineInspector { tikhomirov@555: /** tikhomirov@555: * Not necessarily invoked sequentially by line numbers tikhomirov@555: */ tikhomirov@557: void line(int lineNumber, int changesetRevIndex, BlockData lineContent, LineDescriptor ld); tikhomirov@555: } tikhomirov@555: tikhomirov@555: public interface LineDescriptor { tikhomirov@555: int totalLines(); tikhomirov@555: } tikhomirov@555: tikhomirov@555: /** tikhomirov@555: * Annotate file revision, line by line. tikhomirov@555: */ tikhomirov@555: public static void annotate(HgDataFile df, int changelogRevisionIndex, LineInspector insp) { tikhomirov@555: if (!df.exists()) { tikhomirov@555: return; tikhomirov@555: } tikhomirov@555: FileAnnotation fa = new FileAnnotation(insp); tikhomirov@556: HgBlameFacility af = new HgBlameFacility(); tikhomirov@555: af.annotate(df, changelogRevisionIndex, fa, HgIterateDirection.NewToOld); tikhomirov@555: } tikhomirov@555: tikhomirov@557: // keeps of equal blocks, origin to target, from some previous step tikhomirov@557: private RangeSeq activeEquals; tikhomirov@555: // equal blocks of the current iteration, to be recalculated before next step tikhomirov@555: // to track line number (current target to ultimate target) mapping tikhomirov@557: private RangeSeq intermediateEquals = new RangeSeq(); tikhomirov@555: tikhomirov@555: private boolean[] knownLines; tikhomirov@555: private final LineInspector delegate; tikhomirov@557: private RevisionDescriptor revisionDescriptor; tikhomirov@557: private BlockData lineContent; tikhomirov@557: tikhomirov@557: private IntMap mergedRanges = new IntMap(10); tikhomirov@557: private IntMap equalRanges = new IntMap(10); tikhomirov@557: private boolean activeEqualsComesFromMerge = false; tikhomirov@555: tikhomirov@555: public FileAnnotation(LineInspector lineInspector) { tikhomirov@555: delegate = lineInspector; tikhomirov@555: } tikhomirov@555: tikhomirov@555: public void start(RevisionDescriptor rd) { tikhomirov@557: revisionDescriptor = rd; tikhomirov@555: if (knownLines == null) { tikhomirov@557: lineContent = rd.target(); tikhomirov@557: knownLines = new boolean[lineContent.elementCount()]; tikhomirov@557: activeEquals = new RangeSeq(); tikhomirov@557: activeEquals.add(0, 0, knownLines.length); tikhomirov@557: equalRanges.put(rd.targetChangesetIndex(), activeEquals); tikhomirov@557: } else { tikhomirov@557: activeEquals = equalRanges.get(rd.targetChangesetIndex()); tikhomirov@557: if (activeEquals == null) { tikhomirov@557: // we didn't see this target revision as origin yet tikhomirov@557: // the only way this may happen is that this revision was a merge parent tikhomirov@557: activeEquals = mergedRanges.get(rd.targetChangesetIndex()); tikhomirov@557: activeEqualsComesFromMerge = true; tikhomirov@557: if (activeEquals == null) { tikhomirov@557: throw new HgInvalidStateException(String.format("Can't find previously visited revision %d (while in %d->%1$d diff)", rd.targetChangesetIndex(), rd.originChangesetIndex())); tikhomirov@557: } tikhomirov@557: } tikhomirov@555: } tikhomirov@555: } tikhomirov@555: tikhomirov@557: public void done(RevisionDescriptor rd) { tikhomirov@557: // update line numbers of the intermediate target to point to ultimate target's line numbers tikhomirov@557: RangeSeq v = intermediateEquals.intersect(activeEquals); tikhomirov@557: if (activeEqualsComesFromMerge) { tikhomirov@557: mergedRanges.put(rd.originChangesetIndex(), v); tikhomirov@557: } else { tikhomirov@557: equalRanges.put(rd.originChangesetIndex(), v); tikhomirov@557: } tikhomirov@560: if (rd.isMerge() && !mergedRanges.containsKey(rd.mergeChangesetIndex())) { tikhomirov@560: // seen merge, but no lines were merged from p2. tikhomirov@560: // Add empty range to avoid uncertainty when a parent of p2 pops in tikhomirov@560: mergedRanges.put(rd.mergeChangesetIndex(), new RangeSeq()); tikhomirov@560: } tikhomirov@557: intermediateEquals.clear(); tikhomirov@557: activeEquals = null; tikhomirov@557: activeEqualsComesFromMerge = false; tikhomirov@557: revisionDescriptor = null; tikhomirov@557: } tikhomirov@555: tikhomirov@557: public void same(EqualBlock block) { tikhomirov@557: intermediateEquals.add(block.originStart(), block.targetStart(), block.length()); tikhomirov@557: } tikhomirov@557: tikhomirov@557: public void added(AddBlock block) { tikhomirov@557: RangeSeq rs = null; tikhomirov@557: if (revisionDescriptor.isMerge() && block.originChangesetIndex() == revisionDescriptor.mergeChangesetIndex()) { tikhomirov@557: rs = mergedRanges.get(revisionDescriptor.mergeChangesetIndex()); tikhomirov@557: if (rs == null) { tikhomirov@557: mergedRanges.put(revisionDescriptor.mergeChangesetIndex(), rs = new RangeSeq()); tikhomirov@557: } tikhomirov@557: } tikhomirov@557: if (activeEquals.size() == 0) { tikhomirov@557: return; tikhomirov@557: } tikhomirov@557: for (int i = 0, ln = block.firstAddedLine(), x = block.totalAddedLines(); i < x; i++, ln++) { tikhomirov@557: int lnInFinal = activeEquals.mapLineIndex(ln); tikhomirov@557: if (lnInFinal != -1/* && !knownLines[lnInFinal]*/) { tikhomirov@557: if (rs != null) { tikhomirov@557: rs.add(block.insertedAt() + i, lnInFinal, 1); tikhomirov@557: } else { tikhomirov@557: delegate.line(lnInFinal, block.targetChangesetIndex(), lineContent.elementAt(lnInFinal), new LineDescriptorImpl()); tikhomirov@557: } tikhomirov@557: knownLines[lnInFinal] = true; tikhomirov@557: } tikhomirov@557: } tikhomirov@557: } tikhomirov@557: tikhomirov@557: public void changed(ChangeBlock block) { tikhomirov@557: added(block); tikhomirov@557: } tikhomirov@557: tikhomirov@557: public void deleted(DeleteBlock block) { tikhomirov@557: } tikhomirov@557: tikhomirov@557: private final class LineDescriptorImpl implements LineDescriptor { tikhomirov@557: LineDescriptorImpl() { tikhomirov@557: } tikhomirov@557: tikhomirov@557: public int totalLines() { tikhomirov@557: return FileAnnotation.this.knownLines.length; tikhomirov@557: } tikhomirov@557: } tikhomirov@555: }