tikhomirov@586: /* tikhomirov@586: * Copyright (c) 2013 TMate Software Ltd tikhomirov@586: * tikhomirov@586: * This program is free software; you can redistribute it and/or modify tikhomirov@586: * it under the terms of the GNU General Public License as published by tikhomirov@586: * the Free Software Foundation; version 2 of the License. tikhomirov@586: * tikhomirov@586: * This program is distributed in the hope that it will be useful, tikhomirov@586: * but WITHOUT ANY WARRANTY; without even the implied warranty of tikhomirov@586: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the tikhomirov@586: * GNU General Public License for more details. tikhomirov@586: * tikhomirov@586: * For information on how to redistribute this software under tikhomirov@586: * the terms of a license other than GNU General Public License tikhomirov@586: * contact TMate Software at support@hg4j.com tikhomirov@586: */ tikhomirov@586: package org.tmatesoft.hg.core; tikhomirov@586: tikhomirov@586: import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION; tikhomirov@586: tikhomirov@586: import java.io.IOException; tikhomirov@586: import java.nio.ByteBuffer; tikhomirov@586: import java.util.ArrayList; tikhomirov@586: tikhomirov@586: import org.tmatesoft.hg.internal.ByteArrayChannel; tikhomirov@617: import org.tmatesoft.hg.internal.COWTransaction; tikhomirov@591: import org.tmatesoft.hg.internal.CommitFacility; tikhomirov@617: import org.tmatesoft.hg.internal.CompleteRepoLock; tikhomirov@586: import org.tmatesoft.hg.internal.Experimental; tikhomirov@586: import org.tmatesoft.hg.internal.FileContentSupplier; tikhomirov@591: import org.tmatesoft.hg.internal.Internals; tikhomirov@617: import org.tmatesoft.hg.internal.Transaction; tikhomirov@586: import org.tmatesoft.hg.repo.HgChangelog; tikhomirov@586: import org.tmatesoft.hg.repo.HgDataFile; tikhomirov@586: import org.tmatesoft.hg.repo.HgInternals; tikhomirov@586: import org.tmatesoft.hg.repo.HgRepository; tikhomirov@586: import org.tmatesoft.hg.repo.HgRuntimeException; tikhomirov@586: import org.tmatesoft.hg.repo.HgStatusCollector.Record; tikhomirov@586: import org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector; tikhomirov@586: import org.tmatesoft.hg.util.CancelledException; tikhomirov@586: import org.tmatesoft.hg.util.Outcome; tikhomirov@586: import org.tmatesoft.hg.util.Outcome.Kind; tikhomirov@586: import org.tmatesoft.hg.util.Pair; tikhomirov@586: import org.tmatesoft.hg.util.Path; tikhomirov@586: tikhomirov@586: /** tikhomirov@586: * WORK IN PROGRESS. UNSTABLE API tikhomirov@586: * tikhomirov@586: * 'hg commit' counterpart, commit changes tikhomirov@586: * tikhomirov@586: * @author Artem Tikhomirov tikhomirov@586: * @author TMate Software Ltd. tikhomirov@586: */ tikhomirov@586: @Experimental(reason="Work in progress. Unstable API") tikhomirov@586: public class HgCommitCommand extends HgAbstractCommand { tikhomirov@586: tikhomirov@586: private final HgRepository repo; tikhomirov@586: private String message; tikhomirov@586: private String user; tikhomirov@586: // nodeid of newly added revision tikhomirov@586: private Nodeid newRevision; tikhomirov@586: tikhomirov@586: public HgCommitCommand(HgRepository hgRepo) { tikhomirov@586: repo = hgRepo; tikhomirov@586: } tikhomirov@586: tikhomirov@586: tikhomirov@586: public HgCommitCommand message(String msg) { tikhomirov@586: message = msg; tikhomirov@586: return this; tikhomirov@586: } tikhomirov@586: tikhomirov@586: public HgCommitCommand user(String userName) { tikhomirov@586: user = userName; tikhomirov@586: return this; tikhomirov@586: } tikhomirov@586: tikhomirov@586: /** tikhomirov@586: * Tell if changes in the working directory constitute merge commit. May be invoked prior to (and independently from) {@link #execute()} tikhomirov@586: * tikhomirov@586: * @return true if working directory changes are result of a merge tikhomirov@586: * @throws HgException subclass thereof to indicate specific issue with the repository tikhomirov@586: */ tikhomirov@586: public boolean isMergeCommit() throws HgException { tikhomirov@586: int[] parents = new int[2]; tikhomirov@586: detectParentFromDirstate(parents); tikhomirov@586: return parents[0] != NO_REVISION && parents[1] != NO_REVISION; tikhomirov@586: } tikhomirov@586: tikhomirov@586: /** tikhomirov@586: * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state tikhomirov@617: * @throws HgRepositoryLockException if failed to lock the repo for modifications tikhomirov@586: * @throws IOException propagated IO errors from status walker over working directory tikhomirov@586: * @throws CancelledException if execution of the command was cancelled tikhomirov@586: */ tikhomirov@586: public Outcome execute() throws HgException, IOException, CancelledException { tikhomirov@586: if (message == null) { tikhomirov@586: throw new HgBadArgumentException("Shall supply commit message", null); tikhomirov@586: } tikhomirov@617: final CompleteRepoLock repoLock = new CompleteRepoLock(repo); tikhomirov@617: repoLock.acquire(); tikhomirov@586: try { tikhomirov@586: int[] parentRevs = new int[2]; tikhomirov@586: detectParentFromDirstate(parentRevs); tikhomirov@586: if (parentRevs[0] != NO_REVISION && parentRevs[1] != NO_REVISION) { tikhomirov@586: throw new HgBadArgumentException("Sorry, I'm not yet smart enough to perform merge commits", null); tikhomirov@586: } tikhomirov@586: HgWorkingCopyStatusCollector sc = new HgWorkingCopyStatusCollector(repo); tikhomirov@586: Record status = sc.status(HgRepository.WORKING_COPY); tikhomirov@586: if (status.getModified().size() == 0 && status.getAdded().size() == 0 && status.getRemoved().size() == 0) { tikhomirov@586: newRevision = Nodeid.NULL; tikhomirov@586: return new Outcome(Kind.Failure, "nothing to add"); tikhomirov@586: } tikhomirov@591: CommitFacility cf = new CommitFacility(Internals.getInstance(repo), parentRevs[0], parentRevs[1]); tikhomirov@586: for (Path m : status.getModified()) { tikhomirov@586: HgDataFile df = repo.getFileNode(m); tikhomirov@586: cf.add(df, new WorkingCopyContent(df)); tikhomirov@586: } tikhomirov@586: ArrayList toClear = new ArrayList(); tikhomirov@586: for (Path a : status.getAdded()) { tikhomirov@586: HgDataFile df = repo.getFileNode(a); // TODO need smth explicit, like repo.createNewFileNode(Path) here tikhomirov@586: // XXX might be an interesting exercise not to demand a content supplier, but instead return a "DataRequester" tikhomirov@586: // object, that would indicate interest in data, and this code would "push" it to requester, so that any exception tikhomirov@586: // is handled here, right away, and won't need to travel supplier and CommitFacility. (although try/catch inside tikhomirov@586: // supplier.read (with empty throws declaration) tikhomirov@586: FileContentSupplier fcs = new FileContentSupplier(repo, a); tikhomirov@586: cf.add(df, fcs); tikhomirov@586: toClear.add(fcs); tikhomirov@586: } tikhomirov@586: for (Path r : status.getRemoved()) { tikhomirov@586: HgDataFile df = repo.getFileNode(r); tikhomirov@586: cf.forget(df); tikhomirov@586: } tikhomirov@586: cf.branch(detectBranch()); tikhomirov@586: cf.user(detectUser()); tikhomirov@617: Transaction.Factory trFactory = new COWTransaction.Factory(); tikhomirov@617: Transaction tr = trFactory.create(repo); tikhomirov@617: try { tikhomirov@617: newRevision = cf.commit(message, tr); tikhomirov@617: tr.commit(); tikhomirov@617: } catch (RuntimeException ex) { tikhomirov@617: tr.rollback(); tikhomirov@617: throw ex; tikhomirov@617: } catch (HgException ex) { tikhomirov@617: tr.rollback(); tikhomirov@617: throw ex; tikhomirov@617: } tikhomirov@586: // TODO toClear list is awful tikhomirov@586: for (FileContentSupplier fcs : toClear) { tikhomirov@586: fcs.done(); tikhomirov@586: } tikhomirov@586: return new Outcome(Kind.Success, "Commit ok"); tikhomirov@586: } catch (HgRuntimeException ex) { tikhomirov@586: throw new HgLibraryFailureException(ex); tikhomirov@617: } finally { tikhomirov@617: repoLock.release(); tikhomirov@586: } tikhomirov@586: } tikhomirov@586: tikhomirov@586: public Nodeid getCommittedRevision() { tikhomirov@586: if (newRevision == null) { tikhomirov@586: throw new IllegalStateException("Call #execute() first!"); tikhomirov@586: } tikhomirov@586: return newRevision; tikhomirov@586: } tikhomirov@586: tikhomirov@586: private String detectBranch() { tikhomirov@586: return repo.getWorkingCopyBranchName(); tikhomirov@586: } tikhomirov@586: tikhomirov@586: private String detectUser() { tikhomirov@586: if (user != null) { tikhomirov@586: return user; tikhomirov@586: } tikhomirov@586: // TODO HgInternals is odd place for getNextCommitUsername() tikhomirov@586: return new HgInternals(repo).getNextCommitUsername(); tikhomirov@586: } tikhomirov@586: tikhomirov@586: private void detectParentFromDirstate(int[] parents) { tikhomirov@586: Pair pn = repo.getWorkingCopyParents(); tikhomirov@586: HgChangelog clog = repo.getChangelog(); tikhomirov@586: parents[0] = pn.first().isNull() ? NO_REVISION : clog.getRevisionIndex(pn.first()); tikhomirov@586: parents[1] = pn.second().isNull() ? NO_REVISION : clog.getRevisionIndex(pn.second()); tikhomirov@586: } tikhomirov@586: tikhomirov@586: private static class WorkingCopyContent implements CommitFacility.ByteDataSupplier { tikhomirov@586: private final HgDataFile file; tikhomirov@586: private ByteBuffer fileContent; tikhomirov@586: tikhomirov@586: public WorkingCopyContent(HgDataFile dataFile) { tikhomirov@586: file = dataFile; tikhomirov@586: if (!dataFile.exists()) { tikhomirov@586: throw new IllegalArgumentException(); tikhomirov@586: } tikhomirov@586: } tikhomirov@586: tikhomirov@586: public int read(ByteBuffer dst) { tikhomirov@586: if (fileContent == null) { tikhomirov@586: try { tikhomirov@586: ByteArrayChannel sink = new ByteArrayChannel(); tikhomirov@586: // TODO desperately need partial read here tikhomirov@586: file.workingCopy(sink); tikhomirov@586: fileContent = ByteBuffer.wrap(sink.toArray()); tikhomirov@586: } catch (CancelledException ex) { tikhomirov@586: // ByteArrayChannel doesn't cancel, never happens tikhomirov@586: assert false; tikhomirov@586: } tikhomirov@586: } tikhomirov@586: if (fileContent.remaining() == 0) { tikhomirov@586: return -1; tikhomirov@586: } tikhomirov@586: int dstCap = dst.remaining(); tikhomirov@586: if (fileContent.remaining() > dstCap) { tikhomirov@586: // save actual limit, and pretend we've got exactly desired amount of bytes tikhomirov@586: final int lim = fileContent.limit(); tikhomirov@586: fileContent.limit(dstCap); tikhomirov@586: dst.put(fileContent); tikhomirov@586: fileContent.limit(lim); tikhomirov@586: } else { tikhomirov@586: dst.put(fileContent); tikhomirov@586: } tikhomirov@586: return dstCap - dst.remaining(); tikhomirov@586: } tikhomirov@586: } tikhomirov@586: }