tikhomirov@525: /* tikhomirov@525: * Copyright (c) 2012-2013 TMate Software Ltd tikhomirov@525: * tikhomirov@525: * This program is free software; you can redistribute it and/or modify tikhomirov@525: * it under the terms of the GNU General Public License as published by tikhomirov@525: * the Free Software Foundation; version 2 of the License. tikhomirov@525: * tikhomirov@525: * This program is distributed in the hope that it will be useful, tikhomirov@525: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@525: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@525: * GNU General Public License for more details. tikhomirov@525: * tikhomirov@525: * For information on how to redistribute this software under tikhomirov@525: * the terms of a license other than GNU General Public License tikhomirov@525: * contact TMate Software at support@hg4j.com tikhomirov@525: */ tikhomirov@525: package org.tmatesoft.hg.core; tikhomirov@525: tikhomirov@527: import static org.tmatesoft.hg.repo.HgRepositoryFiles.Branch; tikhomirov@526: import static org.tmatesoft.hg.repo.HgRepositoryFiles.Dirstate; tikhomirov@526: tikhomirov@525: import java.io.File; tikhomirov@525: import java.io.FileOutputStream; tikhomirov@525: import java.io.IOException; tikhomirov@527: import java.io.OutputStreamWriter; tikhomirov@525: import java.nio.channels.FileChannel; tikhomirov@525: tikhomirov@565: import org.tmatesoft.hg.internal.CsetParamKeeper; tikhomirov@525: import org.tmatesoft.hg.internal.DirstateBuilder; tikhomirov@527: import org.tmatesoft.hg.internal.EncodingHelper; tikhomirov@525: import org.tmatesoft.hg.internal.Experimental; tikhomirov@525: import org.tmatesoft.hg.internal.Internals; tikhomirov@525: import org.tmatesoft.hg.internal.WorkingDirFileWriter; tikhomirov@525: import org.tmatesoft.hg.repo.HgDataFile; tikhomirov@563: import org.tmatesoft.hg.repo.HgDirstate; tikhomirov@563: import org.tmatesoft.hg.repo.HgDirstate.EntryKind; tikhomirov@563: import org.tmatesoft.hg.repo.HgDirstate.Record; tikhomirov@565: import org.tmatesoft.hg.repo.HgInternals; tikhomirov@565: import org.tmatesoft.hg.repo.HgManifest; tikhomirov@526: import org.tmatesoft.hg.repo.HgManifest.Flags; tikhomirov@525: import org.tmatesoft.hg.repo.HgRepository; tikhomirov@525: import org.tmatesoft.hg.repo.HgRuntimeException; tikhomirov@581: import org.tmatesoft.hg.util.CancelSupport; tikhomirov@525: import org.tmatesoft.hg.util.CancelledException; tikhomirov@525: import org.tmatesoft.hg.util.Path; tikhomirov@581: import org.tmatesoft.hg.util.ProgressSupport; tikhomirov@525: tikhomirov@525: /** tikhomirov@525: * WORK IN PROGRESS. tikhomirov@525: * tikhomirov@525: * Update working directory to specific state, 'hg checkout' counterpart. tikhomirov@525: * For the time being, only 'clean' checkout is supported ('hg co --clean') tikhomirov@525: * tikhomirov@525: * @since 1.1 tikhomirov@525: * @author Artem Tikhomirov tikhomirov@525: * @author TMate Software Ltd. tikhomirov@525: */ tikhomirov@525: @Experimental(reason="Work in progress") tikhomirov@525: public class HgCheckoutCommand extends HgAbstractCommand{ tikhomirov@525: tikhomirov@525: private final HgRepository repo; tikhomirov@565: private final CsetParamKeeper revisionToCheckout; tikhomirov@563: private boolean cleanCheckout; tikhomirov@525: tikhomirov@525: public HgCheckoutCommand(HgRepository hgRepo) { tikhomirov@525: repo = hgRepo; tikhomirov@565: revisionToCheckout = new CsetParamKeeper(repo); tikhomirov@525: } tikhomirov@525: tikhomirov@525: /** tikhomirov@563: * Whether to discard all uncommited changes prior to check-out. tikhomirov@563: * tikhomirov@563: * NOTE, at the moment, only clean checkout is supported! tikhomirov@563: * tikhomirov@563: * @param clean true to discard any change tikhomirov@563: * @return this for convenience tikhomirov@563: */ tikhomirov@563: public HgCheckoutCommand clean(boolean clean) { tikhomirov@563: cleanCheckout = clean; tikhomirov@563: return this; tikhomirov@563: } tikhomirov@563: tikhomirov@563: /** tikhomirov@525: * Select revision to check out tikhomirov@525: * tikhomirov@525: * @param nodeid revision tikhomirov@525: * @return this for convenience tikhomirov@525: * @throws HgBadArgumentException if failed to find supplied changeset tikhomirov@525: */ tikhomirov@525: public HgCheckoutCommand changeset(Nodeid nodeid) throws HgBadArgumentException { tikhomirov@565: revisionToCheckout.set(nodeid); tikhomirov@565: return this; tikhomirov@525: } tikhomirov@525: tikhomirov@525: /** tikhomirov@525: * Select revision to check out using local revision index tikhomirov@525: * tikhomirov@565: * @param changesetIndex local changelog revision index, or {@link HgRepository#TIP} tikhomirov@525: * @return this for convenience tikhomirov@525: * @throws HgBadArgumentException if failed to find supplied changeset tikhomirov@525: */ tikhomirov@525: public HgCheckoutCommand changeset(int changesetIndex) throws HgBadArgumentException { tikhomirov@565: revisionToCheckout.set(changesetIndex); tikhomirov@525: return this; tikhomirov@525: } tikhomirov@525: tikhomirov@525: /** tikhomirov@581: * Update working copy to match state of the selected revision. tikhomirov@525: * tikhomirov@525: * @throws HgIOException to indicate troubles updating files in working copy tikhomirov@581: * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state tikhomirov@581: * @throws CancelledException if execution of the command was cancelled tikhomirov@525: */ tikhomirov@525: public void execute() throws HgException, CancelledException { tikhomirov@526: try { tikhomirov@581: final ProgressSupport progress = getProgressSupport(null); tikhomirov@581: final CancelSupport cancellation = getCancelSupport(null, true); tikhomirov@581: cancellation.checkCancelled(); tikhomirov@581: progress.start(6); tikhomirov@526: Internals internalRepo = Internals.getInstance(repo); tikhomirov@563: if (cleanCheckout) { tikhomirov@563: // remove tracked files from wd (perhaps, just forget 'Added'?) tikhomirov@563: // for now, just delete each and every tracked file tikhomirov@563: // TODO WorkingCopy container with getFile(HgDataFile/Path) to access files in WD tikhomirov@563: HgDirstate dirstate = new HgInternals(repo).getDirstate(); tikhomirov@563: dirstate.walk(new HgDirstate.Inspector() { tikhomirov@563: tikhomirov@563: public boolean next(EntryKind kind, Record entry) { tikhomirov@563: File f = new File(repo.getWorkingDir(), entry.name().toString()); tikhomirov@563: if (f.exists()) { tikhomirov@563: f.delete(); tikhomirov@563: } tikhomirov@563: return true; tikhomirov@563: } tikhomirov@563: }); tikhomirov@563: } else { tikhomirov@563: throw new HgBadArgumentException("Sorry, only clean checkout is supported now, use #clean(true)", null); tikhomirov@563: } tikhomirov@581: progress.worked(1); tikhomirov@581: cancellation.checkCancelled(); tikhomirov@526: final DirstateBuilder dirstateBuilder = new DirstateBuilder(internalRepo); tikhomirov@526: final CheckoutWorker worker = new CheckoutWorker(internalRepo); tikhomirov@526: HgManifest.Inspector insp = new HgManifest.Inspector() { tikhomirov@526: tikhomirov@526: public boolean next(Nodeid nid, Path fname, Flags flags) { tikhomirov@526: if (worker.next(nid, fname, flags)) { tikhomirov@580: // Mercurial seems to write "n 0 -1 unset fname" on `hg --clean co -rev ` tikhomirov@580: // and the reason for 'force lookup' I suspect is a slight chance of simultaneous modification tikhomirov@580: // of the file by user that doesn't alter its size the very second dirstate is being written tikhomirov@580: // (or the file is being updated and the update brought in changes that didn't alter the file size - tikhomirov@580: // with size and timestamp set, later `hg status` won't notice these changes) tikhomirov@580: tikhomirov@580: // However, as long as we use this class to write clean copies of the files, we can put all the fields tikhomirov@580: // right away. tikhomirov@580: int mtime = worker.getLastFileModificationTime(); tikhomirov@580: // Manifest flags are chars (despite octal values `hg manifest --debug` displays), tikhomirov@580: // while dirstate keeps actual unix flags. tikhomirov@580: int fmode = worker.getLastFileMode(); tikhomirov@580: dirstateBuilder.recordNormal(fname, fmode, mtime, worker.getLastFileSize()); tikhomirov@526: return true; tikhomirov@526: } tikhomirov@526: return false; tikhomirov@526: } tikhomirov@526: tikhomirov@526: public boolean end(int manifestRevision) { tikhomirov@526: return false; tikhomirov@526: } tikhomirov@526: tikhomirov@526: public boolean begin(int mainfestRevision, Nodeid nid, int changelogRevision) { tikhomirov@525: return true; tikhomirov@525: } tikhomirov@526: }; tikhomirov@565: // checkout tip if no revision set tikhomirov@565: final int coRevision = revisionToCheckout.get(HgRepository.TIP); tikhomirov@565: dirstateBuilder.parents(repo.getChangelog().getRevision(coRevision), null); tikhomirov@565: repo.getManifest().walk(coRevision, coRevision, insp); tikhomirov@526: worker.checkFailed(); tikhomirov@581: progress.worked(3); tikhomirov@581: cancellation.checkCancelled(); tikhomirov@526: File dirstateFile = internalRepo.getRepositoryFile(Dirstate); tikhomirov@526: try { tikhomirov@563: FileChannel dirstateFileChannel = new FileOutputStream(dirstateFile).getChannel(); tikhomirov@563: dirstateBuilder.serialize(dirstateFileChannel); tikhomirov@563: dirstateFileChannel.close(); tikhomirov@526: } catch (IOException ex) { tikhomirov@526: throw new HgIOException("Can't write down new directory state", ex, dirstateFile); tikhomirov@525: } tikhomirov@581: progress.worked(1); tikhomirov@581: cancellation.checkCancelled(); tikhomirov@565: String branchName = repo.getChangelog().range(coRevision, coRevision).get(0).branch(); tikhomirov@527: assert branchName != null; tikhomirov@581: File branchFile = internalRepo.getRepositoryFile(Branch); tikhomirov@581: if (HgRepository.DEFAULT_BRANCH_NAME.equals(branchName)) { tikhomirov@581: // clean actual branch, if any tikhomirov@581: if (branchFile.isFile()) { tikhomirov@581: branchFile.delete(); tikhomirov@581: } tikhomirov@581: } else { tikhomirov@527: try { tikhomirov@527: // branch file is UTF-8, see http://mercurial.selenic.com/wiki/EncodingStrategy#UTF-8_strings tikhomirov@527: OutputStreamWriter ow = new OutputStreamWriter(new FileOutputStream(branchFile), EncodingHelper.getUTF8()); tikhomirov@527: ow.write(branchName); tikhomirov@527: ow.close(); tikhomirov@527: } catch (IOException ex) { tikhomirov@527: throw new HgIOException("Can't write down branch information", ex, branchFile); tikhomirov@527: } tikhomirov@527: } tikhomirov@581: progress.worked(1); tikhomirov@581: progress.done(); tikhomirov@526: } catch (HgRuntimeException ex) { tikhomirov@526: throw new HgLibraryFailureException(ex); tikhomirov@526: } tikhomirov@526: } tikhomirov@526: tikhomirov@526: static class CheckoutWorker { tikhomirov@526: private final Internals hgRepo; tikhomirov@526: private HgException failure; tikhomirov@526: private int lastWrittenFileSize; tikhomirov@580: private int lastFileMode; tikhomirov@580: private int lastFileModificationTime; tikhomirov@526: tikhomirov@526: CheckoutWorker(Internals implRepo) { tikhomirov@526: hgRepo = implRepo; tikhomirov@526: } tikhomirov@526: tikhomirov@526: public boolean next(Nodeid nid, Path fname, Flags flags) { tikhomirov@572: WorkingDirFileWriter workingDirWriter = null; tikhomirov@526: try { tikhomirov@526: HgDataFile df = hgRepo.getRepo().getFileNode(fname); tikhomirov@526: int fileRevIndex = df.getRevisionIndex(nid); tikhomirov@526: // check out files based on manifest tikhomirov@572: workingDirWriter = new WorkingDirFileWriter(hgRepo); tikhomirov@580: workingDirWriter.processFile(df, fileRevIndex, flags); tikhomirov@526: lastWrittenFileSize = workingDirWriter.bytesWritten(); tikhomirov@580: lastFileMode = workingDirWriter.fmode(); tikhomirov@580: lastFileModificationTime = workingDirWriter.mtime(); tikhomirov@525: return true; tikhomirov@526: } catch (IOException ex) { tikhomirov@572: failure = new HgIOException("Failed to write down file revision", ex, workingDirWriter.getDestinationFile()); tikhomirov@526: } catch (HgRuntimeException ex) { tikhomirov@526: failure = new HgLibraryFailureException(ex); tikhomirov@525: } tikhomirov@526: return false; tikhomirov@525: } tikhomirov@526: tikhomirov@580: public int getLastFileMode() { tikhomirov@580: return lastFileMode; tikhomirov@580: } tikhomirov@580: tikhomirov@580: public int getLastFileModificationTime() { tikhomirov@580: return lastFileModificationTime; tikhomirov@580: } tikhomirov@580: tikhomirov@580: public int getLastFileSize() { tikhomirov@526: return lastWrittenFileSize; tikhomirov@525: } tikhomirov@526: tikhomirov@526: public void checkFailed() throws HgException { tikhomirov@526: if (failure != null) { tikhomirov@526: throw failure; tikhomirov@526: } tikhomirov@526: } tikhomirov@526: }; tikhomirov@525: }