tikhomirov@107: /* tikhomirov@565: * Copyright (c) 2011-2013 TMate Software Ltd tikhomirov@107: * tikhomirov@107: * This program is free software; you can redistribute it and/or modify tikhomirov@107: * it under the terms of the GNU General Public License as published by tikhomirov@107: * the Free Software Foundation; version 2 of the License. tikhomirov@107: * tikhomirov@107: * This program is distributed in the hope that it will be useful, tikhomirov@107: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@107: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@107: * GNU General Public License for more details. tikhomirov@107: * tikhomirov@107: * For information on how to redistribute this software under tikhomirov@107: * the terms of a license other than GNU General Public License tikhomirov@130: * contact TMate Software at support@hg4j.com tikhomirov@107: */ tikhomirov@107: package org.tmatesoft.hg.core; tikhomirov@107: tikhomirov@367: import static org.tmatesoft.hg.repo.HgInternals.wrongRevisionIndex; tikhomirov@107: import static org.tmatesoft.hg.repo.HgRepository.BAD_REVISION; tikhomirov@107: import static org.tmatesoft.hg.repo.HgRepository.TIP; tikhomirov@107: tikhomirov@322: import java.io.IOException; tikhomirov@322: import java.nio.ByteBuffer; tikhomirov@107: tikhomirov@683: import org.tmatesoft.hg.internal.CsetParamKeeper; tikhomirov@107: import org.tmatesoft.hg.repo.HgDataFile; tikhomirov@107: import org.tmatesoft.hg.repo.HgRepository; tikhomirov@427: import org.tmatesoft.hg.repo.HgRuntimeException; tikhomirov@322: import org.tmatesoft.hg.util.Adaptable; tikhomirov@115: import org.tmatesoft.hg.util.ByteChannel; tikhomirov@322: import org.tmatesoft.hg.util.CancelSupport; tikhomirov@148: import org.tmatesoft.hg.util.CancelledException; tikhomirov@133: import org.tmatesoft.hg.util.Path; tikhomirov@107: tikhomirov@107: /** tikhomirov@131: * Command to obtain content of a file, 'hg cat' counterpart. tikhomirov@107: * tikhomirov@107: * @author Artem Tikhomirov tikhomirov@107: * @author TMate Software Ltd. tikhomirov@107: */ tikhomirov@215: public class HgCatCommand extends HgAbstractCommand { tikhomirov@107: tikhomirov@107: private final HgRepository repo; tikhomirov@107: private Path file; tikhomirov@367: private int revisionIndex = TIP; tikhomirov@107: private Nodeid revision; tikhomirov@232: private Nodeid cset; tikhomirov@107: tikhomirov@131: public HgCatCommand(HgRepository hgRepo) { tikhomirov@107: repo = hgRepo; tikhomirov@107: } tikhomirov@107: tikhomirov@148: /** tikhomirov@148: * File to read, required parameter tikhomirov@148: * @param fname path to a repository file, can't be null tikhomirov@148: * @return this for convenience tikhomirov@148: * @throws IllegalArgumentException if supplied fname is null or points to directory tikhomirov@148: */ tikhomirov@131: public HgCatCommand file(Path fname) { tikhomirov@148: if (fname == null || fname.isDirectory()) { tikhomirov@148: throw new IllegalArgumentException(String.valueOf(fname)); tikhomirov@148: } tikhomirov@107: file = fname; tikhomirov@107: return this; tikhomirov@107: } tikhomirov@107: tikhomirov@148: /** tikhomirov@368: * Select specific revision of the file to cat with revision local index. Note, revision numbering is of particular file, not that of tikhomirov@232: * repository (i.e. revision 0 means initial content of the file, irrespective of changeset revision at the time of commit) tikhomirov@232: * tikhomirov@148: * Invocation of this method clears revision set with {@link #revision(Nodeid)} or {@link #revision(int)} earlier. tikhomirov@232: * tikhomirov@368: * @param fileRevisionIndex - revision local index, non-negative, or one of predefined constants. Note, use of {@link HgRepository#BAD_REVISION}, tikhomirov@148: * although possible, makes little sense (command would fail if executed). tikhomirov@148: * @return this for convenience tikhomirov@148: */ tikhomirov@683: public HgCatCommand revision(int fileRevisionIndex) { // TODO [2.0 API break] shall throw HgBadArgumentException, like other commands do tikhomirov@367: if (wrongRevisionIndex(fileRevisionIndex)) { tikhomirov@367: throw new IllegalArgumentException(String.valueOf(fileRevisionIndex)); tikhomirov@148: } tikhomirov@367: revisionIndex = fileRevisionIndex; tikhomirov@107: revision = null; tikhomirov@232: cset = null; tikhomirov@107: return this; tikhomirov@107: } tikhomirov@107: tikhomirov@148: /** tikhomirov@367: * Select file revision to read. Note, this revision is file revision (i.e. the one from manifest), not the changeset revision. tikhomirov@232: * tikhomirov@232: * Invocation of this method clears revision set with {@link #revision(int)} or {@link #revision(Nodeid)} earlier. tikhomirov@148: * tikhomirov@232: * @param nodeid - unique file revision identifier, Note, use of null or {@link Nodeid#NULL} is senseless tikhomirov@148: * @return this for convenience tikhomirov@148: */ tikhomirov@131: public HgCatCommand revision(Nodeid nodeid) { tikhomirov@148: if (nodeid != null && nodeid.isNull()) { tikhomirov@148: nodeid = null; tikhomirov@148: } tikhomirov@107: revision = nodeid; tikhomirov@367: revisionIndex = BAD_REVISION; tikhomirov@232: cset = null; tikhomirov@232: return this; tikhomirov@232: } tikhomirov@248: tikhomirov@248: /** tikhomirov@248: * Parameterize the command from file revision object. tikhomirov@248: * tikhomirov@248: * @param fileRev file revision to cat tikhomirov@248: * @return this for convenience tikhomirov@248: */ tikhomirov@248: public HgCatCommand revision(HgFileRevision fileRev) { tikhomirov@248: return file(fileRev.getPath()).revision(fileRev.getRevision()); tikhomirov@248: } tikhomirov@232: tikhomirov@232: /** tikhomirov@232: * Select whatever revision of the file that was actual at the time of the specified changeset. Unlike {@link #revision(int)} or {@link #revision(Nodeid)}, this method tikhomirov@232: * operates in terms of repository global revisions (aka changesets). tikhomirov@232: * tikhomirov@368: * Invocation of this method clears selection of a file revision with its index. tikhomirov@232: * tikhomirov@232: * @param nodeid changeset revision tikhomirov@232: * @return this for convenience tikhomirov@232: */ tikhomirov@232: public HgCatCommand changeset(Nodeid nodeid) { tikhomirov@367: revisionIndex = BAD_REVISION; tikhomirov@232: revision = null; tikhomirov@232: cset = nodeid; tikhomirov@565: // TODO [2.0 API break] shall use CsetParamKeeper instead, but Exception thrown would break the API tikhomirov@107: return this; tikhomirov@107: } tikhomirov@107: tikhomirov@148: /** tikhomirov@683: * Select file by changeset tikhomirov@683: * @see #changeset(Nodeid) tikhomirov@683: * @param revisionIndex index of changelog revision tikhomirov@683: * @return this for convenience tikhomirov@683: * @throws HgBadArgumentException if failed to find supplied changeset revision tikhomirov@683: */ tikhomirov@683: public HgCatCommand changeset(int revisionIndex) throws HgBadArgumentException { tikhomirov@683: int ri = new CsetParamKeeper(repo).set(revisionIndex).get(); tikhomirov@683: return changeset(repo.getChangelog().getRevision(ri)); tikhomirov@683: } tikhomirov@683: tikhomirov@683: /** tikhomirov@148: * Runs the command with current set of parameters and pipes data to provided sink. tikhomirov@148: * tikhomirov@148: * @param sink output channel to write data to. tikhomirov@396: * tikhomirov@396: * @throws HgBadArgumentException if no target file node found tikhomirov@427: * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state tikhomirov@427: * @throws CancelledException if execution of the command was cancelled tikhomirov@148: * @throws IllegalArgumentException when command arguments are incomplete or wrong tikhomirov@148: */ tikhomirov@396: public void execute(ByteChannel sink) throws HgException, CancelledException { tikhomirov@396: // XXX perhaps, IAE together with HgBadArgumentException is not the best idea tikhomirov@367: if (revisionIndex == BAD_REVISION && revision == null && cset == null) { tikhomirov@232: throw new IllegalArgumentException("File revision, corresponing local number, or a changset nodeid shall be specified"); tikhomirov@107: } tikhomirov@107: if (file == null) { tikhomirov@107: throw new IllegalArgumentException("Name of the file is missing"); tikhomirov@107: } tikhomirov@115: if (sink == null) { tikhomirov@148: throw new IllegalArgumentException("Need an output channel"); tikhomirov@107: } tikhomirov@427: try { tikhomirov@427: HgDataFile dataFile = repo.getFileNode(file); tikhomirov@427: if (!dataFile.exists()) { tikhomirov@427: // TODO may benefit from repo.getStoragePath to print revlog location in addition to human-friendly file path tikhomirov@427: throw new HgPathNotFoundException(String.format("File %s not found in the repository", file), file); tikhomirov@427: } tikhomirov@427: int revToExtract; tikhomirov@427: if (cset != null) { tikhomirov@427: int csetRev = repo.getChangelog().getRevisionIndex(cset); tikhomirov@427: Nodeid toExtract = null; tikhomirov@427: do { tikhomirov@427: // TODO post-1.0 perhaps, HgChangesetFileSneaker may come handy? tikhomirov@427: toExtract = repo.getManifest().getFileRevision(csetRev, file); tikhomirov@427: if (toExtract == null) { tikhomirov@427: if (dataFile.isCopy()) { tikhomirov@427: file = dataFile.getCopySourceName(); tikhomirov@427: dataFile = repo.getFileNode(file); tikhomirov@427: } else { tikhomirov@427: break; tikhomirov@427: } tikhomirov@427: } tikhomirov@427: } while (toExtract == null); tikhomirov@232: if (toExtract == null) { tikhomirov@427: String m = String.format("File %s nor its origins were known at repository's %s revision", file, cset.shortNotation()); tikhomirov@427: throw new HgPathNotFoundException(m, file).setRevision(cset); tikhomirov@232: } tikhomirov@427: revToExtract = dataFile.getRevisionIndex(toExtract); tikhomirov@427: } else if (revision != null) { tikhomirov@427: revToExtract = dataFile.getRevisionIndex(revision); tikhomirov@427: } else { tikhomirov@427: revToExtract = revisionIndex; tikhomirov@232: } tikhomirov@427: ByteChannel sinkWrap; tikhomirov@427: if (getCancelSupport(null, false) == null) { tikhomirov@427: // no command-specific cancel helper, no need for extra proxy tikhomirov@427: // sink itself still may supply CS tikhomirov@427: sinkWrap = sink; tikhomirov@427: } else { tikhomirov@427: // try CS from sink, if any. at least there is CS from command tikhomirov@427: CancelSupport cancelHelper = getCancelSupport(sink, true); tikhomirov@427: cancelHelper.checkCancelled(); tikhomirov@427: sinkWrap = new ByteChannelProxy(sink, cancelHelper); tikhomirov@427: } tikhomirov@427: dataFile.contentWithFilters(revToExtract, sinkWrap); tikhomirov@427: } catch (HgRuntimeException ex) { tikhomirov@427: throw new HgLibraryFailureException(ex); tikhomirov@107: } tikhomirov@322: } tikhomirov@322: tikhomirov@322: private static class ByteChannelProxy implements ByteChannel, Adaptable { tikhomirov@322: private final ByteChannel delegate; tikhomirov@322: private final CancelSupport cancelHelper; tikhomirov@322: tikhomirov@322: public ByteChannelProxy(ByteChannel _delegate, CancelSupport cs) { tikhomirov@322: assert _delegate != null; tikhomirov@322: delegate = _delegate; tikhomirov@322: cancelHelper = cs; tikhomirov@322: } tikhomirov@322: public int write(ByteBuffer buffer) throws IOException, CancelledException { tikhomirov@322: return delegate.write(buffer); tikhomirov@322: } tikhomirov@322: tikhomirov@322: public T getAdapter(Class adapterClass) { tikhomirov@322: if (CancelSupport.class == adapterClass) { tikhomirov@322: return adapterClass.cast(cancelHelper); tikhomirov@322: } tikhomirov@356: return Adaptable.Factory.getAdapter(delegate, adapterClass, null); tikhomirov@322: } tikhomirov@107: } tikhomirov@107: }