tikhomirov@629: /* tikhomirov@629: * Copyright (c) 2013 TMate Software Ltd tikhomirov@629: * tikhomirov@629: * This program is free software; you can redistribute it and/or modify tikhomirov@629: * it under the terms of the GNU General Public License as published by tikhomirov@629: * the Free Software Foundation; version 2 of the License. tikhomirov@629: * tikhomirov@629: * This program is distributed in the hope that it will be useful, tikhomirov@629: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@629: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@629: * GNU General Public License for more details. tikhomirov@629: * tikhomirov@629: * For information on how to redistribute this software under tikhomirov@629: * the terms of a license other than GNU General Public License tikhomirov@629: * contact TMate Software at support@hg4j.com tikhomirov@629: */ tikhomirov@629: package org.tmatesoft.hg.core; tikhomirov@629: tikhomirov@629: import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION; tikhomirov@629: import static org.tmatesoft.hg.repo.HgRepository.TIP; tikhomirov@629: tikhomirov@630: import org.tmatesoft.hg.internal.CsetParamKeeper; tikhomirov@629: import org.tmatesoft.hg.internal.FileHistory; tikhomirov@629: import org.tmatesoft.hg.internal.FileRevisionHistoryChunk; tikhomirov@703: import org.tmatesoft.hg.internal.diff.BlameHelper; tikhomirov@629: import org.tmatesoft.hg.repo.HgDataFile; tikhomirov@629: import org.tmatesoft.hg.repo.HgRepository; tikhomirov@629: import org.tmatesoft.hg.repo.HgRuntimeException; tikhomirov@632: import org.tmatesoft.hg.util.CancelSupport; tikhomirov@629: import org.tmatesoft.hg.util.CancelledException; tikhomirov@629: import org.tmatesoft.hg.util.Path; tikhomirov@632: import org.tmatesoft.hg.util.ProgressSupport; tikhomirov@629: tikhomirov@629: /** tikhomirov@632: * 'hg diff' counterpart, with similar, although not identical, functionality. tikhomirov@632: * Despite both 'hg diff' and this command are diff-based, implementation tikhomirov@632: * peculiarities may lead to slightly different diff results. Either is valid tikhomirov@632: * as there's no strict diff specification. tikhomirov@632: * tikhomirov@632: *

tikhomirov@632: * Note, at the moment this command annotates single file only. Diff over tikhomirov@632: * complete repository (all the file changed in a given changeset) might tikhomirov@632: * be added later. tikhomirov@629: * tikhomirov@629: * @since 1.1 tikhomirov@629: * @author Artem Tikhomirov tikhomirov@629: * @author TMate Software Ltd. tikhomirov@629: */ tikhomirov@629: public class HgDiffCommand extends HgAbstractCommand { tikhomirov@629: tikhomirov@629: private final HgRepository repo; tikhomirov@629: private HgDataFile df; tikhomirov@630: private final CsetParamKeeper clogRevIndexStart, clogRevIndexEnd; tikhomirov@629: private HgIterateDirection iterateDirection = HgIterateDirection.NewToOld; tikhomirov@629: tikhomirov@629: public HgDiffCommand(HgRepository hgRepo) { tikhomirov@629: repo = hgRepo; tikhomirov@630: clogRevIndexStart = new CsetParamKeeper(hgRepo); tikhomirov@630: clogRevIndexEnd = new CsetParamKeeper(hgRepo); tikhomirov@629: } tikhomirov@629: tikhomirov@629: public HgDiffCommand file(Path file) { tikhomirov@629: df = repo.getFileNode(file); tikhomirov@629: return this; tikhomirov@629: } tikhomirov@629: tikhomirov@630: /** tikhomirov@630: * Selects the file which history to blame, mandatory. tikhomirov@630: * tikhomirov@630: * @param file repository file tikhomirov@630: * @return this for convenience tikhomirov@630: */ tikhomirov@629: public HgDiffCommand file(HgDataFile file) { tikhomirov@629: df = file; tikhomirov@629: return this; tikhomirov@629: } tikhomirov@629: tikhomirov@630: /** tikhomirov@630: * Select range of file's history for {@link #executeDiff(HgBlameInspector)} tikhomirov@630: * and {@link #executeAnnotate(HgBlameInspector)}. tikhomirov@630: *

tikhomirov@630: * {@link #executeDiff(HgBlameInspector) diff} uses these as revisions to diff against each other, while tikhomirov@630: * {@link #executeAnnotate(HgBlameInspector) annotate} walks the range. tikhomirov@630: * tikhomirov@630: * @param changelogRevIndexStart index of changelog revision, left range boundary tikhomirov@630: * @param changelogRevIndexEnd index of changelog revision, right range boundary tikhomirov@630: * @return this for convenience tikhomirov@630: * @throws HgBadArgumentException if failed to find any of supplied changeset tikhomirov@630: */ tikhomirov@630: public HgDiffCommand range(int changelogRevIndexStart, int changelogRevIndexEnd) throws HgBadArgumentException { tikhomirov@630: clogRevIndexStart.set(changelogRevIndexStart); tikhomirov@630: clogRevIndexEnd.set(changelogRevIndexEnd); tikhomirov@629: return this; tikhomirov@629: } tikhomirov@629: tikhomirov@630: /** tikhomirov@683: * Select range of file history, limited by changesets. tikhomirov@683: * @see #range(int, int) tikhomirov@683: * @param cset1 changelog revision, left range boundary tikhomirov@683: * @param cset2 changelog revision, right range boundary tikhomirov@683: * @return this for convenience tikhomirov@683: * @throws HgBadArgumentException if revisions are not valid changeset identifiers tikhomirov@683: */ tikhomirov@683: public HgDiffCommand range(Nodeid cset1, Nodeid cset2) throws HgBadArgumentException { tikhomirov@683: clogRevIndexStart.set(cset1); tikhomirov@683: clogRevIndexEnd.set(cset2); tikhomirov@683: return this; tikhomirov@683: } tikhomirov@683: tikhomirov@683: /** tikhomirov@632: * Selects revision for {@link #executeParentsAnnotate(HgBlameInspector)}, the one tikhomirov@630: * to diff against its parents. tikhomirov@630: * tikhomirov@630: * Besides, it is handy when range of interest spans up to the very beginning of the file history tikhomirov@630: * (and thus is equivalent to range(0, changelogRevIndex)) tikhomirov@630: * tikhomirov@630: * @param changelogRevIndex index of changelog revision tikhomirov@630: * @return this for convenience tikhomirov@630: * @throws HgBadArgumentException if failed to find supplied changeset tikhomirov@630: */ tikhomirov@630: public HgDiffCommand changeset(int changelogRevIndex) throws HgBadArgumentException { tikhomirov@630: clogRevIndexStart.set(0); tikhomirov@630: clogRevIndexEnd.set(changelogRevIndex); tikhomirov@629: return this; tikhomirov@629: } tikhomirov@683: tikhomirov@683: /** tikhomirov@683: * Select specific changeset or a range [0..changeset], like {@link #changeset(int)} tikhomirov@683: * tikhomirov@683: * @param nid changeset tikhomirov@683: * @return this for convenience tikhomirov@683: * @throws HgBadArgumentException if failed to find supplied changeset revision tikhomirov@683: */ tikhomirov@683: public HgDiffCommand changeset(Nodeid nid) throws HgBadArgumentException { tikhomirov@683: clogRevIndexStart.set(0); tikhomirov@683: clogRevIndexEnd.set(nid); tikhomirov@683: return this; tikhomirov@683: } tikhomirov@683: tikhomirov@629: tikhomirov@630: /** tikhomirov@630: * Revision differences are reported in selected order when tikhomirov@630: * annotating {@link #range(int, int) range} of changesets with tikhomirov@630: * {@link #executeAnnotate(HgBlameInspector)}. tikhomirov@630: *

tikhomirov@632: * This method doesn't affect {@link #executeParentsAnnotate(HgBlameInspector)} and tikhomirov@630: * {@link #executeDiff(HgBlameInspector)} tikhomirov@630: * tikhomirov@630: * @param order desired iteration order tikhomirov@630: * @return this for convenience tikhomirov@630: */ tikhomirov@629: public HgDiffCommand order(HgIterateDirection order) { tikhomirov@629: iterateDirection = order; tikhomirov@629: return this; tikhomirov@629: } tikhomirov@629: tikhomirov@629: /** tikhomirov@630: * Diff two revisions selected with {@link #range(int, int)} against each other. tikhomirov@630: *

mimics 'hg diff -r clogRevIndex1 -r clogRevIndex2' tikhomirov@630: * tikhomirov@630: * @throws HgCallbackTargetException propagated exception from the handler tikhomirov@630: * @throws CancelledException if execution of the command was cancelled tikhomirov@630: * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state tikhomirov@629: */ tikhomirov@629: public void executeDiff(HgBlameInspector insp) throws HgCallbackTargetException, CancelledException, HgException { tikhomirov@630: checkFile(); tikhomirov@632: final ProgressSupport progress = getProgressSupport(insp); tikhomirov@632: progress.start(2); tikhomirov@629: try { tikhomirov@675: final int startRevIndex = clogRevIndexStart.get(0); tikhomirov@675: final int endRevIndex = clogRevIndexEnd.get(TIP); tikhomirov@632: final CancelSupport cancel = getCancelSupport(insp, true); tikhomirov@675: int fileRevIndex1 = fileRevIndex(df, startRevIndex); tikhomirov@675: int fileRevIndex2 = fileRevIndex(df, endRevIndex); tikhomirov@629: BlameHelper bh = new BlameHelper(insp); tikhomirov@675: bh.prepare(df, startRevIndex, endRevIndex); tikhomirov@632: progress.worked(1); tikhomirov@632: cancel.checkCancelled(); tikhomirov@675: bh.diff(fileRevIndex1, startRevIndex, fileRevIndex2, endRevIndex); tikhomirov@632: progress.worked(1); tikhomirov@632: cancel.checkCancelled(); tikhomirov@629: } catch (HgRuntimeException ex) { tikhomirov@629: throw new HgLibraryFailureException(ex); tikhomirov@632: } finally { tikhomirov@632: progress.done(); tikhomirov@629: } tikhomirov@629: } tikhomirov@629: tikhomirov@629: /** tikhomirov@630: * Walk file history {@link #range(int, int) range} and report changes (diff) for each revision tikhomirov@630: * tikhomirov@630: * @throws HgCallbackTargetException propagated exception from the handler tikhomirov@630: * @throws CancelledException if execution of the command was cancelled tikhomirov@630: * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state tikhomirov@629: */ tikhomirov@629: public void executeAnnotate(HgBlameInspector insp) throws HgCallbackTargetException, CancelledException, HgException { tikhomirov@630: checkFile(); tikhomirov@632: ProgressSupport progress = null; tikhomirov@629: try { tikhomirov@629: if (!df.exists()) { tikhomirov@629: return; tikhomirov@629: } tikhomirov@632: final CancelSupport cancel = getCancelSupport(insp, true); tikhomirov@629: BlameHelper bh = new BlameHelper(insp); tikhomirov@675: final int startRevIndex = clogRevIndexStart.get(0); tikhomirov@675: final int endRevIndex = clogRevIndexEnd.get(TIP); tikhomirov@675: FileHistory fileHistory = bh.prepare(df, startRevIndex, endRevIndex); tikhomirov@632: // tikhomirov@632: cancel.checkCancelled(); tikhomirov@632: int totalWork = 0; tikhomirov@632: for (FileRevisionHistoryChunk fhc : fileHistory.iterate(iterateDirection)) { tikhomirov@632: totalWork += fhc.revisionCount(); tikhomirov@632: } tikhomirov@632: progress = getProgressSupport(insp); tikhomirov@632: progress.start(totalWork + 1); tikhomirov@632: progress.worked(1); // BlameHelper.prepare tikhomirov@632: // tikhomirov@629: int[] fileClogParentRevs = new int[2]; tikhomirov@629: int[] fileParentRevs = new int[2]; tikhomirov@629: for (FileRevisionHistoryChunk fhc : fileHistory.iterate(iterateDirection)) { tikhomirov@629: for (int fri : fhc.fileRevisions(iterateDirection)) { tikhomirov@629: int clogRevIndex = fhc.changeset(fri); tikhomirov@629: // the way we built fileHistory ensures we won't walk past [changelogRevIndexStart..changelogRevIndexEnd] tikhomirov@675: assert clogRevIndex >= startRevIndex; tikhomirov@675: assert clogRevIndex <= endRevIndex; tikhomirov@629: fhc.fillFileParents(fri, fileParentRevs); tikhomirov@629: fhc.fillCsetParents(fri, fileClogParentRevs); tikhomirov@629: bh.annotateChange(fri, clogRevIndex, fileParentRevs, fileClogParentRevs); tikhomirov@632: progress.worked(1); tikhomirov@632: cancel.checkCancelled(); tikhomirov@629: } tikhomirov@629: } tikhomirov@629: } catch (HgRuntimeException ex) { tikhomirov@629: throw new HgLibraryFailureException(ex); tikhomirov@632: } finally { tikhomirov@632: if (progress != null) { tikhomirov@632: progress.done(); tikhomirov@632: } tikhomirov@629: } tikhomirov@629: } tikhomirov@629: tikhomirov@629: /** tikhomirov@629: * Annotates changes of the file against its parent(s). tikhomirov@629: * Unlike {@link #annotate(HgDataFile, int, Inspector, HgIterateDirection)}, doesn't tikhomirov@629: * walk file history, looks at the specified revision only. Handles both parents (if merge revision). tikhomirov@630: * tikhomirov@630: * @throws HgCallbackTargetException propagated exception from the handler tikhomirov@630: * @throws CancelledException if execution of the command was cancelled tikhomirov@630: * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state tikhomirov@629: */ tikhomirov@632: public void executeParentsAnnotate(HgBlameInspector insp) throws HgCallbackTargetException, CancelledException, HgException { tikhomirov@630: checkFile(); tikhomirov@632: final ProgressSupport progress = getProgressSupport(insp); tikhomirov@632: progress.start(2); tikhomirov@629: try { tikhomirov@632: final CancelSupport cancel = getCancelSupport(insp, true); tikhomirov@709: int changelogRevisionIndex = clogRevIndexEnd.get(TIP); tikhomirov@629: // TODO detect if file is text/binary (e.g. looking for chars < ' ' and not \t\r\n\f tikhomirov@629: int fileRevIndex = fileRevIndex(df, changelogRevisionIndex); tikhomirov@629: int[] fileRevParents = new int[2]; tikhomirov@629: df.parents(fileRevIndex, fileRevParents, null, null); tikhomirov@629: if (changelogRevisionIndex == TIP) { tikhomirov@629: changelogRevisionIndex = df.getChangesetRevisionIndex(fileRevIndex); tikhomirov@629: } tikhomirov@629: int[] fileClogParentRevs = new int[2]; tikhomirov@629: fileClogParentRevs[0] = fileRevParents[0] == NO_REVISION ? NO_REVISION : df.getChangesetRevisionIndex(fileRevParents[0]); tikhomirov@629: fileClogParentRevs[1] = fileRevParents[1] == NO_REVISION ? NO_REVISION : df.getChangesetRevisionIndex(fileRevParents[1]); tikhomirov@629: BlameHelper bh = new BlameHelper(insp); tikhomirov@629: int clogIndexStart = fileClogParentRevs[0] == NO_REVISION ? (fileClogParentRevs[1] == NO_REVISION ? 0 : fileClogParentRevs[1]) : fileClogParentRevs[0]; tikhomirov@629: bh.prepare(df, clogIndexStart, changelogRevisionIndex); tikhomirov@632: progress.worked(1); tikhomirov@632: cancel.checkCancelled(); tikhomirov@629: bh.annotateChange(fileRevIndex, changelogRevisionIndex, fileRevParents, fileClogParentRevs); tikhomirov@632: progress.worked(1); tikhomirov@632: cancel.checkCancelled(); tikhomirov@629: } catch (HgRuntimeException ex) { tikhomirov@629: throw new HgLibraryFailureException(ex); tikhomirov@632: } finally { tikhomirov@632: progress.done(); tikhomirov@629: } tikhomirov@629: } tikhomirov@629: tikhomirov@630: private void checkFile() { tikhomirov@630: if (df == null) { tikhomirov@630: throw new IllegalArgumentException("File is not set"); tikhomirov@630: } tikhomirov@630: } tikhomirov@629: tikhomirov@629: private static int fileRevIndex(HgDataFile df, int csetRevIndex) throws HgRuntimeException { tikhomirov@629: Nodeid fileRev = df.getRepo().getManifest().getFileRevision(csetRevIndex, df.getPath()); tikhomirov@629: return df.getRevisionIndex(fileRev); tikhomirov@629: } tikhomirov@629: }