tikhomirov@542: /* tikhomirov@542: * Copyright (c) 2013 TMate Software Ltd tikhomirov@542: * tikhomirov@542: * This program is free software; you can redistribute it and/or modify tikhomirov@542: * it under the terms of the GNU General Public License as published by tikhomirov@542: * the Free Software Foundation; version 2 of the License. tikhomirov@542: * tikhomirov@542: * This program is distributed in the hope that it will be useful, tikhomirov@542: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@542: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@542: * GNU General Public License for more details. tikhomirov@542: * tikhomirov@542: * For information on how to redistribute this software under tikhomirov@542: * the terms of a license other than GNU General Public License tikhomirov@542: * contact TMate Software at support@hg4j.com tikhomirov@542: */ tikhomirov@556: package org.tmatesoft.hg.repo; tikhomirov@542: tikhomirov@569: import static org.tmatesoft.hg.core.HgIterateDirection.NewToOld; tikhomirov@569: import static org.tmatesoft.hg.core.HgIterateDirection.OldToNew; tikhomirov@569: import static org.tmatesoft.hg.repo.HgInternals.wrongRevisionIndex; tikhomirov@569: import static org.tmatesoft.hg.repo.HgRepository.*; tikhomirov@542: tikhomirov@569: import java.util.Arrays; tikhomirov@552: import java.util.BitSet; tikhomirov@569: import java.util.Collections; tikhomirov@552: import java.util.LinkedList; tikhomirov@552: tikhomirov@562: import org.tmatesoft.hg.core.HgCallbackTargetException; tikhomirov@552: import org.tmatesoft.hg.core.HgIterateDirection; tikhomirov@542: import org.tmatesoft.hg.core.Nodeid; tikhomirov@569: import org.tmatesoft.hg.internal.BlameHelper; tikhomirov@556: import org.tmatesoft.hg.internal.Callback; tikhomirov@556: import org.tmatesoft.hg.internal.Experimental; tikhomirov@556: import org.tmatesoft.hg.internal.IntVector; tikhomirov@555: import org.tmatesoft.hg.util.Adaptable; tikhomirov@542: tikhomirov@542: /** tikhomirov@555: * Facility with diff/annotate functionality. tikhomirov@542: * tikhomirov@542: * @author Artem Tikhomirov tikhomirov@542: * @author TMate Software Ltd. tikhomirov@542: */ tikhomirov@556: @Experimental(reason="Unstable API") tikhomirov@556: public final class HgBlameFacility { tikhomirov@568: private final HgDataFile df; tikhomirov@568: tikhomirov@568: public HgBlameFacility(HgDataFile file) { tikhomirov@568: if (file == null) { tikhomirov@568: throw new IllegalArgumentException(); tikhomirov@568: } tikhomirov@568: df = file; tikhomirov@568: } tikhomirov@549: tikhomirov@549: /** tikhomirov@552: * mimic 'hg diff -r clogRevIndex1 -r clogRevIndex2' tikhomirov@549: */ tikhomirov@568: public void diff(int clogRevIndex1, int clogRevIndex2, Inspector insp) throws HgCallbackTargetException { tikhomirov@569: // FIXME clogRevIndex1 and clogRevIndex2 may point to different files, need to decide whether to throw an exception tikhomirov@569: // or to attempt to look up correct file node (tricky) tikhomirov@552: int fileRevIndex1 = fileRevIndex(df, clogRevIndex1); tikhomirov@552: int fileRevIndex2 = fileRevIndex(df, clogRevIndex2); tikhomirov@569: BlameHelper bh = new BlameHelper(insp, 5); tikhomirov@569: bh.useFileUpTo(df, clogRevIndex2); tikhomirov@569: bh.diff(fileRevIndex1, clogRevIndex1, fileRevIndex2, clogRevIndex2); tikhomirov@552: } tikhomirov@552: tikhomirov@555: /** tikhomirov@568: * Walk file history up/down to revision at given changeset and report changes for each revision tikhomirov@555: */ tikhomirov@568: public void annotate(int changelogRevisionIndex, Inspector insp, HgIterateDirection iterateOrder) throws HgCallbackTargetException { tikhomirov@569: annotate(0, changelogRevisionIndex, insp, iterateOrder); tikhomirov@569: } tikhomirov@569: tikhomirov@569: /** tikhomirov@569: * Walk file history range and report changes for each revision tikhomirov@569: */ tikhomirov@569: public void annotate(int changelogRevIndexStart, int changelogRevIndexEnd, Inspector insp, HgIterateDirection iterateOrder) throws HgCallbackTargetException { tikhomirov@569: if (wrongRevisionIndex(changelogRevIndexStart) || wrongRevisionIndex(changelogRevIndexEnd)) { tikhomirov@569: throw new IllegalArgumentException(); tikhomirov@569: } tikhomirov@569: // Note, changelogRevisionIndex may be TIP, while the code below doesn't tolerate constants tikhomirov@569: // tikhomirov@569: int lastRevision = df.getRepo().getChangelog().getLastRevision(); tikhomirov@569: if (changelogRevIndexEnd == TIP) { tikhomirov@569: changelogRevIndexEnd = lastRevision; tikhomirov@569: } tikhomirov@569: HgInternals.checkRevlogRange(changelogRevIndexStart, changelogRevIndexEnd, lastRevision); tikhomirov@552: if (!df.exists()) { tikhomirov@552: return; tikhomirov@552: } tikhomirov@569: BlameHelper bh = new BlameHelper(insp, 10); tikhomirov@569: HgDataFile currentFile = df; tikhomirov@569: int fileLastClogRevIndex = changelogRevIndexEnd; tikhomirov@569: FileRevisionHistoryChunk nextChunk = null; tikhomirov@569: LinkedList fileCompleteHistory = new LinkedList(); tikhomirov@569: do { tikhomirov@569: FileRevisionHistoryChunk fileHistory = new FileRevisionHistoryChunk(currentFile); tikhomirov@569: fileHistory.init(fileLastClogRevIndex); tikhomirov@569: fileHistory.linkTo(nextChunk); tikhomirov@569: fileCompleteHistory.addFirst(fileHistory); // to get the list in old-to-new order tikhomirov@569: nextChunk = fileHistory; tikhomirov@569: bh.useFileUpTo(currentFile, fileLastClogRevIndex); tikhomirov@569: if (currentFile.isCopy()) { tikhomirov@569: // TODO SessionContext.getPathFactory() and replace all Path.create tikhomirov@569: HgRepository repo = currentFile.getRepo(); tikhomirov@569: currentFile = repo.getFileNode(currentFile.getCopySourceName()); tikhomirov@569: fileLastClogRevIndex = repo.getChangelog().getRevisionIndex(currentFile.getCopySourceRevision()); tikhomirov@569: // XXX perhaps, shall fail with meaningful exception if new file doesn't exist (.i/.d not found for whatever reason) tikhomirov@569: // or source revision is missing? tikhomirov@569: } else { tikhomirov@569: currentFile = null; // stop iterating tikhomirov@569: } tikhomirov@569: } while (currentFile != null && fileLastClogRevIndex >= changelogRevIndexStart); tikhomirov@569: // fileCompleteHistory is in (origin, intermediate target, ultimate target) order tikhomirov@568: tikhomirov@569: int[] fileClogParentRevs = new int[2]; tikhomirov@569: int[] fileParentRevs = new int[2]; tikhomirov@569: if (iterateOrder == NewToOld) { tikhomirov@569: Collections.reverse(fileCompleteHistory); tikhomirov@569: } tikhomirov@569: boolean shallFilterStart = changelogRevIndexStart != 0; // no reason if complete history is walked tikhomirov@569: for (FileRevisionHistoryChunk fileHistory : fileCompleteHistory) { tikhomirov@569: for (int fri : fileHistory.fileRevisions(iterateOrder)) { tikhomirov@569: int clogRevIndex = fileHistory.changeset(fri); tikhomirov@569: if (shallFilterStart) { tikhomirov@569: if (iterateOrder == NewToOld) { tikhomirov@569: // clogRevIndex decreases tikhomirov@569: if (clogRevIndex < changelogRevIndexStart) { tikhomirov@569: break; tikhomirov@569: } tikhomirov@569: // fall-through, clogRevIndex is in the [start..end] range tikhomirov@569: } else { // old to new tikhomirov@569: // the way we built fileHistory ensures we won't walk past changelogRevIndexEnd tikhomirov@569: // here we ensure we start from the right one, the one indicated with changelogRevIndexStart tikhomirov@569: if (clogRevIndex < changelogRevIndexStart) { tikhomirov@569: continue; tikhomirov@569: } else { tikhomirov@569: shallFilterStart = false; // once boundary is crossed, no need to check tikhomirov@569: // fall-through tikhomirov@569: } tikhomirov@569: } tikhomirov@569: } tikhomirov@569: fileHistory.fillFileParents(fri, fileParentRevs); tikhomirov@569: fileHistory.fillCsetParents(fri, fileClogParentRevs); tikhomirov@569: bh.annotateChange(fri, clogRevIndex, fileParentRevs, fileClogParentRevs); tikhomirov@569: } tikhomirov@552: } tikhomirov@549: } tikhomirov@548: tikhomirov@548: /** tikhomirov@555: * Annotates changes of the file against its parent(s). tikhomirov@562: * Unlike {@link #annotate(HgDataFile, int, Inspector, HgIterateDirection)}, doesn't tikhomirov@555: * walk file history, looks at the specified revision only. Handles both parents (if merge revision). tikhomirov@548: */ tikhomirov@568: public void annotateSingleRevision(int changelogRevisionIndex, Inspector insp) throws HgCallbackTargetException { tikhomirov@545: // TODO detect if file is text/binary (e.g. looking for chars < ' ' and not \t\r\n\f tikhomirov@552: int fileRevIndex = fileRevIndex(df, changelogRevisionIndex); tikhomirov@542: int[] fileRevParents = new int[2]; tikhomirov@542: df.parents(fileRevIndex, fileRevParents, null, null); tikhomirov@552: if (changelogRevisionIndex == TIP) { tikhomirov@552: changelogRevisionIndex = df.getChangesetRevisionIndex(fileRevIndex); tikhomirov@548: } tikhomirov@569: BlameHelper bh = new BlameHelper(insp, 5); tikhomirov@569: bh.useFileUpTo(df, changelogRevisionIndex); tikhomirov@569: int[] fileClogParentRevs = new int[2]; tikhomirov@569: fileClogParentRevs[0] = fileRevParents[0] == NO_REVISION ? NO_REVISION : df.getChangesetRevisionIndex(fileRevParents[0]); tikhomirov@569: fileClogParentRevs[1] = fileRevParents[1] == NO_REVISION ? NO_REVISION : df.getChangesetRevisionIndex(fileRevParents[1]); tikhomirov@569: bh.annotateChange(fileRevIndex, changelogRevisionIndex, fileRevParents, fileClogParentRevs); tikhomirov@542: } tikhomirov@542: tikhomirov@555: /** tikhomirov@555: * Client's sink for revision differences. tikhomirov@555: * tikhomirov@555: * When implemented, clients shall not expect new {@link Block blocks} instances in each call. tikhomirov@555: * tikhomirov@555: * In case more information about annotated revision is needed, inspector instances may supply tikhomirov@555: * {@link RevisionDescriptor.Recipient} through {@link Adaptable}. tikhomirov@555: */ tikhomirov@542: @Callback tikhomirov@562: public interface Inspector { tikhomirov@562: void same(EqualBlock block) throws HgCallbackTargetException; tikhomirov@562: void added(AddBlock block) throws HgCallbackTargetException; tikhomirov@562: void changed(ChangeBlock block) throws HgCallbackTargetException; tikhomirov@562: void deleted(DeleteBlock block) throws HgCallbackTargetException; tikhomirov@562: } tikhomirov@562: tikhomirov@562: /** tikhomirov@562: * No need to keep "Block" prefix as long as there's only one {@link Inspector} tikhomirov@562: */ tikhomirov@562: @Deprecated tikhomirov@562: public interface BlockInspector extends Inspector { tikhomirov@542: } tikhomirov@542: tikhomirov@554: /** tikhomirov@554: * Represents content of a block, either as a sequence of bytes or a tikhomirov@554: * sequence of smaller blocks (lines), if appropriate (according to usage context). tikhomirov@554: * tikhomirov@554: * This approach allows line-by-line access to content data along with complete byte sequence for the whole block, i.e. tikhomirov@554: *
tikhomirov@554: 	 *    BlockData bd = addBlock.addedLines()
tikhomirov@554: 	 *    // bd describes data from the addition completely.
tikhomirov@554: 	 *    // elements of the BlockData are lines
tikhomirov@554: 	 *    bd.elementCount() == addBlock.totalAddedLines();
tikhomirov@554: 	 *    // one cat obtain complete addition with
tikhomirov@554: 	 *    byte[] everythingAdded = bd.asArray();
tikhomirov@554: 	 *    // or iterate line by line
tikhomirov@554: 	 *    for (int i = 0; i < bd.elementCount(); i++) {
tikhomirov@554: 	 *    	 byte[] lineContent = bd.elementAt(i);
tikhomirov@554: 	 *       String line = new String(lineContent, fileEncodingCharset);
tikhomirov@554: 	 *    }
tikhomirov@554: 	 *    where bd.elementAt(0) is the line at index addBlock.firstAddedLine() 
tikhomirov@554: 	 * 
tikhomirov@554: * tikhomirov@554: * LineData or ChunkData? tikhomirov@554: */ tikhomirov@554: public interface BlockData { tikhomirov@554: BlockData elementAt(int index); tikhomirov@554: int elementCount(); tikhomirov@554: byte[] asArray(); tikhomirov@554: } tikhomirov@554: tikhomirov@555: /** tikhomirov@562: * {@link Inspector} may optionally request extra information about revisions tikhomirov@555: * being inspected, denoting itself as a {@link RevisionDescriptor.Recipient}. This class tikhomirov@555: * provides complete information about file revision under annotation now. tikhomirov@555: */ tikhomirov@555: public interface RevisionDescriptor { tikhomirov@555: /** tikhomirov@555: * @return complete source of the diff origin, never null tikhomirov@555: */ tikhomirov@555: BlockData origin(); tikhomirov@555: /** tikhomirov@555: * @return complete source of the diff target, never null tikhomirov@555: */ tikhomirov@555: BlockData target(); tikhomirov@555: /** tikhomirov@555: * @return changeset revision index of original file, or {@link HgRepository#NO_REVISION} if it's the very first revision tikhomirov@555: */ tikhomirov@555: int originChangesetIndex(); tikhomirov@555: /** tikhomirov@555: * @return changeset revision index of the target file tikhomirov@555: */ tikhomirov@555: int targetChangesetIndex(); tikhomirov@555: /** tikhomirov@555: * @return true if this revision is merge tikhomirov@555: */ tikhomirov@555: boolean isMerge(); tikhomirov@555: /** tikhomirov@555: * @return changeset revision index of the second, merged parent tikhomirov@555: */ tikhomirov@555: int mergeChangesetIndex(); tikhomirov@555: /** tikhomirov@556: * @return revision index of the change in target file's revlog tikhomirov@555: */ tikhomirov@555: int fileRevisionIndex(); tikhomirov@555: tikhomirov@555: /** tikhomirov@569: * @return file object under blame (target file) tikhomirov@569: */ tikhomirov@569: HgDataFile file(); tikhomirov@569: tikhomirov@569: /** tikhomirov@555: * Implement to indicate interest in {@link RevisionDescriptor}. tikhomirov@555: * tikhomirov@555: * Note, instance of {@link RevisionDescriptor} is the same for tikhomirov@555: * {@link #start(RevisionDescriptor)} and {@link #done(RevisionDescriptor)} tikhomirov@555: * methods, and not necessarily a new one (i.e. ==) for the next tikhomirov@555: * revision announced. tikhomirov@555: */ tikhomirov@555: @Callback tikhomirov@555: public interface Recipient { tikhomirov@555: /** tikhomirov@555: * Comes prior to any change {@link Block blocks} tikhomirov@555: */ tikhomirov@562: void start(RevisionDescriptor revisionDescription) throws HgCallbackTargetException; tikhomirov@555: /** tikhomirov@555: * Comes after all change {@link Block blocks} were dispatched tikhomirov@555: */ tikhomirov@562: void done(RevisionDescriptor revisionDescription) throws HgCallbackTargetException; tikhomirov@555: } tikhomirov@555: } tikhomirov@555: tikhomirov@556: /** tikhomirov@556: * Each change block comes from a single origin, blocks that are result of a merge tikhomirov@556: * have {@link #originChangesetIndex()} equal to {@link RevisionDescriptor#mergeChangesetIndex()}. tikhomirov@556: */ tikhomirov@542: public interface Block { tikhomirov@545: int originChangesetIndex(); tikhomirov@545: int targetChangesetIndex(); tikhomirov@542: } tikhomirov@542: tikhomirov@545: public interface EqualBlock extends Block { tikhomirov@545: int originStart(); tikhomirov@545: int targetStart(); tikhomirov@545: int length(); tikhomirov@554: BlockData content(); tikhomirov@545: } tikhomirov@545: tikhomirov@542: public interface AddBlock extends Block { tikhomirov@556: /** tikhomirov@556: * @return line index in the origin where this block is inserted tikhomirov@556: */ tikhomirov@556: int insertedAt(); tikhomirov@556: /** tikhomirov@556: * @return line index of the first added line in the target revision tikhomirov@556: */ tikhomirov@542: int firstAddedLine(); tikhomirov@556: /** tikhomirov@556: * @return number of added lines in this block tikhomirov@556: */ tikhomirov@542: int totalAddedLines(); tikhomirov@556: /** tikhomirov@556: * @return content of added lines tikhomirov@556: */ tikhomirov@554: BlockData addedLines(); tikhomirov@542: } tikhomirov@542: public interface DeleteBlock extends Block { tikhomirov@556: /** tikhomirov@556: * @return line index in the target revision were this deleted block would be tikhomirov@556: */ tikhomirov@556: int removedAt(); tikhomirov@556: /** tikhomirov@556: * @return line index of the first removed line in the original revision tikhomirov@556: */ tikhomirov@542: int firstRemovedLine(); tikhomirov@556: /** tikhomirov@556: * @return number of deleted lines in this block tikhomirov@556: */ tikhomirov@542: int totalRemovedLines(); tikhomirov@556: /** tikhomirov@556: * @return content of deleted lines tikhomirov@556: */ tikhomirov@554: BlockData removedLines(); tikhomirov@542: } tikhomirov@542: public interface ChangeBlock extends AddBlock, DeleteBlock { tikhomirov@542: } tikhomirov@542: tikhomirov@542: tikhomirov@569: private static int fileRevIndex(HgDataFile df, int csetRevIndex) { tikhomirov@569: Nodeid fileRev = df.getRepo().getManifest().getFileRevision(csetRevIndex, df.getPath()); tikhomirov@569: return df.getRevisionIndex(fileRev); tikhomirov@542: } tikhomirov@542: tikhomirov@569: private static class FileRevisionHistoryChunk { tikhomirov@569: private final HgDataFile df; tikhomirov@569: // change ancestry, sequence of file revisions tikhomirov@569: private IntVector fileRevsToVisit; tikhomirov@569: // parent pairs of complete file history tikhomirov@569: private IntVector fileParentRevs; tikhomirov@569: // map file revision to changelog revision (sparse array, only file revisions to visit are set) tikhomirov@569: private int[] file2changelog; tikhomirov@569: private int originChangelogRev = BAD_REVISION, originFileRev = BAD_REVISION; tikhomirov@542: tikhomirov@569: public FileRevisionHistoryChunk(HgDataFile file) { tikhomirov@569: df = file; tikhomirov@542: } tikhomirov@545: tikhomirov@569: public void init(int changelogRevisionIndex) { tikhomirov@569: // XXX df.indexWalk(0, fileRevIndex, ) might be more effective tikhomirov@569: int fileRevIndex = fileRevIndex(df, changelogRevisionIndex); tikhomirov@569: int[] fileRevParents = new int[2]; tikhomirov@569: fileParentRevs = new IntVector((fileRevIndex+1) * 2, 0); tikhomirov@569: fileParentRevs.add(NO_REVISION, NO_REVISION); // parents of fileRevIndex == 0 tikhomirov@569: for (int i = 1; i <= fileRevIndex; i++) { tikhomirov@569: df.parents(i, fileRevParents, null, null); tikhomirov@569: fileParentRevs.add(fileRevParents[0], fileRevParents[1]); tikhomirov@545: } tikhomirov@569: // fileRevsToVisit keep file change ancestry from new to old tikhomirov@569: fileRevsToVisit = new IntVector(fileRevIndex + 1, 0); tikhomirov@569: // keep map of file revision to changelog revision tikhomirov@569: file2changelog = new int[fileRevIndex+1]; tikhomirov@569: // only elements worth visit would get mapped, so there would be unfilled areas in the file2changelog, tikhomirov@569: // prevent from error (make it explicit) by bad value tikhomirov@569: Arrays.fill(file2changelog, BAD_REVISION); tikhomirov@569: LinkedList queue = new LinkedList(); tikhomirov@569: BitSet seen = new BitSet(fileRevIndex + 1); tikhomirov@569: queue.add(fileRevIndex); tikhomirov@569: do { tikhomirov@569: int x = queue.removeFirst(); tikhomirov@569: if (seen.get(x)) { tikhomirov@569: continue; tikhomirov@549: } tikhomirov@569: seen.set(x); tikhomirov@569: fileRevsToVisit.add(x); tikhomirov@569: file2changelog[x] = df.getChangesetRevisionIndex(x); tikhomirov@569: int p1 = fileParentRevs.get(2*x); tikhomirov@569: int p2 = fileParentRevs.get(2*x + 1); tikhomirov@569: if (p1 != NO_REVISION) { tikhomirov@569: queue.addLast(p1); tikhomirov@569: } tikhomirov@569: if (p2 != NO_REVISION) { tikhomirov@569: queue.addLast(p2); tikhomirov@569: } tikhomirov@569: } while (!queue.isEmpty()); tikhomirov@569: // make sure no child is processed before we handled all (grand-)parents of the element tikhomirov@569: fileRevsToVisit.sort(false); tikhomirov@549: } tikhomirov@549: tikhomirov@569: public void linkTo(FileRevisionHistoryChunk target) { tikhomirov@569: // assume that target.init() has been called already tikhomirov@569: if (target == null) { tikhomirov@569: return; tikhomirov@569: } tikhomirov@569: target.originFileRev = fileRevsToVisit.get(0); // files to visit are new to old tikhomirov@569: target.originChangelogRev = changeset(target.originFileRev); tikhomirov@569: } tikhomirov@569: tikhomirov@569: public int[] fileRevisions(HgIterateDirection iterateOrder) { tikhomirov@569: // fileRevsToVisit is { r10, r7, r6, r5, r0 }, new to old tikhomirov@569: int[] rv = fileRevsToVisit.toArray(); tikhomirov@569: if (iterateOrder == OldToNew) { tikhomirov@569: // reverse return value tikhomirov@569: for (int a = 0, b = rv.length-1; a < b; a++, b--) { tikhomirov@569: int t = rv[b]; tikhomirov@569: rv[b] = rv[a]; tikhomirov@569: rv[a] = t; tikhomirov@549: } tikhomirov@549: } tikhomirov@569: return rv; tikhomirov@555: } tikhomirov@555: tikhomirov@569: public int changeset(int fileRevIndex) { tikhomirov@569: return file2changelog[fileRevIndex]; tikhomirov@555: } tikhomirov@569: tikhomirov@569: public void fillFileParents(int fileRevIndex, int[] fileParents) { tikhomirov@569: if (fileRevIndex == 0 && originFileRev != BAD_REVISION) { tikhomirov@569: // this chunk continues another file tikhomirov@569: assert originFileRev != NO_REVISION; tikhomirov@569: fileParents[0] = originFileRev; tikhomirov@569: fileParents[1] = NO_REVISION; tikhomirov@569: return; tikhomirov@557: } tikhomirov@569: fileParents[0] = fileParentRevs.get(fileRevIndex * 2); tikhomirov@569: fileParents[1] = fileParentRevs.get(fileRevIndex * 2 + 1); tikhomirov@557: } tikhomirov@569: tikhomirov@569: public void fillCsetParents(int fileRevIndex, int[] csetParents) { tikhomirov@569: if (fileRevIndex == 0 && originFileRev != BAD_REVISION) { tikhomirov@569: assert originFileRev != NO_REVISION; tikhomirov@569: csetParents[0] = originChangelogRev; tikhomirov@569: csetParents[1] = NO_REVISION; // I wonder if possible to start a copy with two parents? tikhomirov@569: return; tikhomirov@569: } tikhomirov@569: int fp1 = fileParentRevs.get(fileRevIndex * 2); tikhomirov@569: int fp2 = fileParentRevs.get(fileRevIndex * 2 + 1); tikhomirov@569: csetParents[0] = fp1 == NO_REVISION ? NO_REVISION : changeset(fp1); tikhomirov@569: csetParents[1] = fp2 == NO_REVISION ? NO_REVISION : changeset(fp2); tikhomirov@549: } tikhomirov@549: } tikhomirov@542: }