# HG changeset patch # User Artem Tikhomirov # Date 1366994321 -7200 # Node ID 73c20c648c1ffa1d4bd2bf37b15bb8c5a03422b8 # Parent b47ef0d2777bfd406dabb668f555fc1d32afd142 HgCommitCommand initial support diff -r b47ef0d2777b -r 73c20c648c1f src/org/tmatesoft/hg/core/HgAnnotateCommand.java --- a/src/org/tmatesoft/hg/core/HgAnnotateCommand.java Thu Apr 25 17:53:44 2013 +0200 +++ b/src/org/tmatesoft/hg/core/HgAnnotateCommand.java Fri Apr 26 18:38:41 2013 +0200 @@ -92,6 +92,14 @@ // TODO [1.1] set encoding and provide String line content from LineInfo + /** + * Annotate selected file + * + * @param inspector + * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state + * @throws HgCallbackTargetException + * @throws CancelledException if execution of the command was cancelled + */ public void execute(Inspector inspector) throws HgException, HgCallbackTargetException, CancelledException { if (inspector == null) { throw new IllegalArgumentException(); diff -r b47ef0d2777b -r 73c20c648c1f src/org/tmatesoft/hg/core/HgCommitCommand.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/core/HgCommitCommand.java Fri Apr 26 18:38:41 2013 +0200 @@ -0,0 +1,205 @@ +/* + * Copyright (c) 2013 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.core; + +import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; + +import org.tmatesoft.hg.internal.ByteArrayChannel; +import org.tmatesoft.hg.internal.Experimental; +import org.tmatesoft.hg.internal.FileContentSupplier; +import org.tmatesoft.hg.repo.CommitFacility; +import org.tmatesoft.hg.repo.HgChangelog; +import org.tmatesoft.hg.repo.HgDataFile; +import org.tmatesoft.hg.repo.HgInternals; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.repo.HgRuntimeException; +import org.tmatesoft.hg.repo.HgStatusCollector.Record; +import org.tmatesoft.hg.repo.HgWorkingCopyStatusCollector; +import org.tmatesoft.hg.util.CancelledException; +import org.tmatesoft.hg.util.Outcome; +import org.tmatesoft.hg.util.Outcome.Kind; +import org.tmatesoft.hg.util.Pair; +import org.tmatesoft.hg.util.Path; + +/** + * WORK IN PROGRESS. UNSTABLE API + * + * 'hg commit' counterpart, commit changes + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +@Experimental(reason="Work in progress. Unstable API") +public class HgCommitCommand extends HgAbstractCommand { + + private final HgRepository repo; + private String message; + private String user; + // nodeid of newly added revision + private Nodeid newRevision; + + public HgCommitCommand(HgRepository hgRepo) { + repo = hgRepo; + } + + + public HgCommitCommand message(String msg) { + message = msg; + return this; + } + + public HgCommitCommand user(String userName) { + user = userName; + return this; + } + + /** + * Tell if changes in the working directory constitute merge commit. May be invoked prior to (and independently from) {@link #execute()} + * + * @return true if working directory changes are result of a merge + * @throws HgException subclass thereof to indicate specific issue with the repository + */ + public boolean isMergeCommit() throws HgException { + int[] parents = new int[2]; + detectParentFromDirstate(parents); + return parents[0] != NO_REVISION && parents[1] != NO_REVISION; + } + + /** + * @throws HgException subclass thereof to indicate specific issue with the command arguments or repository state + * @throws IOException propagated IO errors from status walker over working directory + * @throws CancelledException if execution of the command was cancelled + */ + public Outcome execute() throws HgException, IOException, CancelledException { + if (message == null) { + throw new HgBadArgumentException("Shall supply commit message", null); + } + try { + int[] parentRevs = new int[2]; + detectParentFromDirstate(parentRevs); + if (parentRevs[0] != NO_REVISION && parentRevs[1] != NO_REVISION) { + throw new HgBadArgumentException("Sorry, I'm not yet smart enough to perform merge commits", null); + } + HgWorkingCopyStatusCollector sc = new HgWorkingCopyStatusCollector(repo); + Record status = sc.status(HgRepository.WORKING_COPY); + if (status.getModified().size() == 0 && status.getAdded().size() == 0 && status.getRemoved().size() == 0) { + newRevision = Nodeid.NULL; + return new Outcome(Kind.Failure, "nothing to add"); + } + CommitFacility cf = new CommitFacility(repo, parentRevs[0], parentRevs[1]); + for (Path m : status.getModified()) { + HgDataFile df = repo.getFileNode(m); + cf.add(df, new WorkingCopyContent(df)); + } + ArrayList toClear = new ArrayList(); + for (Path a : status.getAdded()) { + HgDataFile df = repo.getFileNode(a); // TODO need smth explicit, like repo.createNewFileNode(Path) here + // XXX might be an interesting exercise not to demand a content supplier, but instead return a "DataRequester" + // object, that would indicate interest in data, and this code would "push" it to requester, so that any exception + // is handled here, right away, and won't need to travel supplier and CommitFacility. (although try/catch inside + // supplier.read (with empty throws declaration) + FileContentSupplier fcs = new FileContentSupplier(repo, a); + cf.add(df, fcs); + toClear.add(fcs); + } + for (Path r : status.getRemoved()) { + HgDataFile df = repo.getFileNode(r); + cf.forget(df); + } + cf.branch(detectBranch()); + cf.user(detectUser()); + newRevision = cf.commit(message); + // TODO toClear list is awful + for (FileContentSupplier fcs : toClear) { + fcs.done(); + } + return new Outcome(Kind.Success, "Commit ok"); + } catch (HgRuntimeException ex) { + throw new HgLibraryFailureException(ex); + } + } + + public Nodeid getCommittedRevision() { + if (newRevision == null) { + throw new IllegalStateException("Call #execute() first!"); + } + return newRevision; + } + + private String detectBranch() { + return repo.getWorkingCopyBranchName(); + } + + private String detectUser() { + if (user != null) { + return user; + } + // TODO HgInternals is odd place for getNextCommitUsername() + return new HgInternals(repo).getNextCommitUsername(); + } + + private void detectParentFromDirstate(int[] parents) { + Pair pn = repo.getWorkingCopyParents(); + HgChangelog clog = repo.getChangelog(); + parents[0] = pn.first().isNull() ? NO_REVISION : clog.getRevisionIndex(pn.first()); + parents[1] = pn.second().isNull() ? NO_REVISION : clog.getRevisionIndex(pn.second()); + } + + private static class WorkingCopyContent implements CommitFacility.ByteDataSupplier { + private final HgDataFile file; + private ByteBuffer fileContent; + + public WorkingCopyContent(HgDataFile dataFile) { + file = dataFile; + if (!dataFile.exists()) { + throw new IllegalArgumentException(); + } + } + + public int read(ByteBuffer dst) { + if (fileContent == null) { + try { + ByteArrayChannel sink = new ByteArrayChannel(); + // TODO desperately need partial read here + file.workingCopy(sink); + fileContent = ByteBuffer.wrap(sink.toArray()); + } catch (CancelledException ex) { + // ByteArrayChannel doesn't cancel, never happens + assert false; + } + } + if (fileContent.remaining() == 0) { + return -1; + } + int dstCap = dst.remaining(); + if (fileContent.remaining() > dstCap) { + // save actual limit, and pretend we've got exactly desired amount of bytes + final int lim = fileContent.limit(); + fileContent.limit(dstCap); + dst.put(fileContent); + fileContent.limit(lim); + } else { + dst.put(fileContent); + } + return dstCap - dst.remaining(); + } + } +} diff -r b47ef0d2777b -r 73c20c648c1f src/org/tmatesoft/hg/core/HgRepoFacade.java --- a/src/org/tmatesoft/hg/core/HgRepoFacade.java Thu Apr 25 17:53:44 2013 +0200 +++ b/src/org/tmatesoft/hg/core/HgRepoFacade.java Fri Apr 26 18:38:41 2013 +0200 @@ -126,9 +126,31 @@ return new HgIncomingCommand(repo); } - // TODO [1.1] add factory methods for all new commands + public HgCloneCommand createCloneCommand() { + return new HgCloneCommand(); + } + + public HgUpdateConfigCommand createUpdateRepositoryConfigCommand() { + return HgUpdateConfigCommand.forRepository(repo); + } + + public HgAddRemoveCommand createAddRemoveCommand() { + return new HgAddRemoveCommand(repo); + } + + public HgCheckoutCommand createCheckoutCommand() { + return new HgCheckoutCommand(repo); + } + + public HgRevertCommand createRevertCommand() { + return new HgRevertCommand(repo); + } public HgAnnotateCommand createAnnotateCommand() { return new HgAnnotateCommand(repo); } + + public HgCommitCommand createCommitCommand() { + return new HgCommitCommand(repo); + } } diff -r b47ef0d2777b -r 73c20c648c1f src/org/tmatesoft/hg/internal/FileContentSupplier.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/org/tmatesoft/hg/internal/FileContentSupplier.java Fri Apr 26 18:38:41 2013 +0200 @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2013 TMate Software Ltd + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; version 2 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * For information on how to redistribute this software under + * the terms of a license other than GNU General Public License + * contact TMate Software at support@hg4j.com + */ +package org.tmatesoft.hg.internal; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +import org.tmatesoft.hg.core.HgIOException; +import org.tmatesoft.hg.repo.CommitFacility; +import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.Path; + +/** + * FIXME files are opened at the moment of instantiation, though the moment the data is requested might be distant + * + * @author Artem Tikhomirov + * @author TMate Software Ltd. + */ +public class FileContentSupplier implements CommitFacility.ByteDataSupplier { + private final FileChannel channel; + private IOException error; + + public FileContentSupplier(HgRepository repo, Path file) throws HgIOException { + this(new File(repo.getWorkingDir(), file.toString())); + } + + public FileContentSupplier(File f) throws HgIOException { + if (!f.canRead()) { + throw new HgIOException(String.format("Can't read file %s", f), f); + } + try { + channel = new FileInputStream(f).getChannel(); + } catch (FileNotFoundException ex) { + throw new HgIOException("Can't open file", ex, f); + } + } + + public int read(ByteBuffer buf) { + if (error != null) { + return -1; + } + try { + return channel.read(buf); + } catch (IOException ex) { + error = ex; + } + return -1; + } + + public void done() throws IOException { + channel.close(); + if (error != null) { + throw error; + } + } +} \ No newline at end of file diff -r b47ef0d2777b -r 73c20c648c1f src/org/tmatesoft/hg/repo/CommitFacility.java --- a/src/org/tmatesoft/hg/repo/CommitFacility.java Thu Apr 25 17:53:44 2013 +0200 +++ b/src/org/tmatesoft/hg/repo/CommitFacility.java Fri Apr 26 18:38:41 2013 +0200 @@ -28,6 +28,7 @@ import java.util.TreeMap; import java.util.TreeSet; +import org.tmatesoft.hg.core.HgCommitCommand; import org.tmatesoft.hg.core.HgRepositoryLockException; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.internal.ByteArrayChannel; @@ -44,6 +45,8 @@ /** * WORK IN PROGRESS + * Name: CommitObject, FutureCommit or PendingCommit + * Only public API now: {@link HgCommitCommand}. TEMPORARILY lives in the oth.repo public packages, until code interdependencies are resolved * * @author Artem Tikhomirov * @author TMate Software Ltd. @@ -54,7 +57,7 @@ private final int p1Commit, p2Commit; private Map> files = new LinkedHashMap>(); private Set removals = new TreeSet(); - private String branch; + private String branch, user; public CommitFacility(HgRepository hgRepo, int parentCommit) { this(hgRepo, parentCommit, NO_REVISION); @@ -90,6 +93,10 @@ branch = branchName; } + public void user(String userName) { + user = userName; + } + public Nodeid commit(String message) throws HgRepositoryLockException { final HgChangelog clog = repo.getChangelog(); @@ -172,6 +179,7 @@ final ChangelogEntryBuilder changelogBuilder = new ChangelogEntryBuilder(); changelogBuilder.setModified(files.keySet()); changelogBuilder.branch(branch == null ? HgRepository.DEFAULT_BRANCH_NAME : branch); + changelogBuilder.user(String.valueOf(user)); byte[] clogContent = changelogBuilder.build(manifestRev, message); RevlogStreamWriter changelogWriter = new RevlogStreamWriter(repo.getSessionContext(), clog.content); Nodeid changesetRev = changelogWriter.addRevision(clogContent, clogRevisionIndex, p1Commit, p2Commit); @@ -210,6 +218,9 @@ // unlike DataAccess (which provides structured access), this one // deals with a sequence of bytes, when there's no need in structure of the data + // FIXME java.nio.ReadableByteChannel or ByteStream/ByteSequence(read, length, reset) + // SHALL be inline with util.ByteChannel, reading bytes from HgDataFile, preferably DataAccess#readBytes(BB) to match API, + // and a wrap for ByteVector public interface ByteDataSupplier { // TODO look if can resolve DataAccess in HgCloneCommand visibility issue // FIXME needs lifecycle, e.g. for supplier that reads from WC int read(ByteBuffer buf); diff -r b47ef0d2777b -r 73c20c648c1f test/org/tmatesoft/hg/test/TestCommit.java --- a/test/org/tmatesoft/hg/test/TestCommit.java Thu Apr 25 17:53:44 2013 +0200 +++ b/test/org/tmatesoft/hg/test/TestCommit.java Fri Apr 26 18:38:41 2013 +0200 @@ -18,27 +18,29 @@ import static org.junit.Assert.*; import static org.tmatesoft.hg.repo.HgRepository.*; -import static org.tmatesoft.hg.repo.HgRepository.DEFAULT_BRANCH_NAME; -import static org.tmatesoft.hg.repo.HgRepository.NO_REVISION; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; import java.util.List; import org.junit.Test; import org.tmatesoft.hg.core.HgAddRemoveCommand; import org.tmatesoft.hg.core.HgCatCommand; import org.tmatesoft.hg.core.HgChangeset; +import org.tmatesoft.hg.core.HgCommitCommand; import org.tmatesoft.hg.core.HgLogCommand; +import org.tmatesoft.hg.core.HgRevertCommand; +import org.tmatesoft.hg.core.HgStatus.Kind; +import org.tmatesoft.hg.core.HgStatusCommand; import org.tmatesoft.hg.core.Nodeid; import org.tmatesoft.hg.internal.ByteArrayChannel; +import org.tmatesoft.hg.internal.FileContentSupplier; import org.tmatesoft.hg.repo.CommitFacility; import org.tmatesoft.hg.repo.HgDataFile; import org.tmatesoft.hg.repo.HgLookup; import org.tmatesoft.hg.repo.HgRepository; +import org.tmatesoft.hg.util.Outcome; import org.tmatesoft.hg.util.Path; /** @@ -224,6 +226,53 @@ assertHgVerifyOk(repoLoc); } + @Test + public void testCommandBasics() throws Exception { + File repoLoc = RepoUtils.cloneRepoToTempLocation("log-1", "test-commit-cmd", false); + HgRepository hgRepo = new HgLookup().detect(repoLoc); + HgDataFile dfB = hgRepo.getFileNode("b"); + assertTrue("[sanity]", dfB.exists()); + File fileB = new File(repoLoc, "b"); + assertTrue("[sanity]", fileB.canRead()); + RepoUtils.modifyFileAppend(fileB, " 1 \n"); + + HgCommitCommand cmd = new HgCommitCommand(hgRepo); + assertFalse(cmd.isMergeCommit()); + Outcome r = cmd.message("FIRST").execute(); + assertTrue(r.isOk()); + Nodeid c1 = cmd.getCommittedRevision(); + + hgRepo = new HgLookup().detect(repoLoc); + // + new HgRevertCommand(hgRepo).file(dfB.getPath()).execute(); // FIXME Hack to emulate dirstate update + // + TestStatus.StatusCollector status = new TestStatus.StatusCollector(); + new HgStatusCommand(hgRepo).defaults().execute(status); + assertTrue(status.getErrors().isEmpty()); + assertTrue(status.get(Kind.Modified).isEmpty()); + + HgDataFile dfD = hgRepo.getFileNode("d"); + assertTrue("[sanity]", dfD.exists()); + File fileD = new File(repoLoc, "d"); + assertTrue("[sanity]", fileD.canRead()); + // + RepoUtils.modifyFileAppend(fileD, " 1 \n"); + cmd = new HgCommitCommand(hgRepo); + assertFalse(cmd.isMergeCommit()); + r = cmd.message("SECOND").execute(); + assertTrue(r.isOk()); + Nodeid c2 = cmd.getCommittedRevision(); + // + hgRepo = new HgLookup().detect(repoLoc); + int lastRev = hgRepo.getChangelog().getLastRevision(); + List csets = new HgLogCommand(hgRepo).range(lastRev-1, lastRev).execute(); + assertEquals(csets.get(0).getNodeid(), c1); + assertEquals(csets.get(1).getNodeid(), c2); + assertEquals(csets.get(0).getComment(), "FIRST"); + assertEquals(csets.get(1).getComment(), "SECOND"); + assertHgVerifyOk(repoLoc); + } + private void assertHgVerifyOk(File repoLoc) throws InterruptedException, IOException { ExecHelper verifyRun = new ExecHelper(new OutputParser.Stub(), repoLoc); verifyRun.run("hg", "verify"); @@ -275,35 +324,4 @@ return count; } } - - static class FileContentSupplier implements CommitFacility.ByteDataSupplier { - private final FileChannel channel; - private IOException error; - - public FileContentSupplier(File f) throws IOException { - if (!f.canRead()) { - throw new IOException(String.format("Can't read file %s", f)); - } - channel = new FileInputStream(f).getChannel(); - } - - public int read(ByteBuffer buf) { - if (error != null) { - return -1; - } - try { - return channel.read(buf); - } catch (IOException ex) { - error = ex; - } - return -1; - } - - public void done() throws IOException { - channel.close(); - if (error != null) { - throw error; - } - } - } } diff -r b47ef0d2777b -r 73c20c648c1f test/org/tmatesoft/hg/test/TestRevlog.java --- a/test/org/tmatesoft/hg/test/TestRevlog.java Thu Apr 25 17:53:44 2013 +0200 +++ b/test/org/tmatesoft/hg/test/TestRevlog.java Fri Apr 26 18:38:41 2013 +0200 @@ -56,7 +56,7 @@ RevlogReader rr = new RevlogReader(indexFile); rr.init(true); rr.needData(true); - int startEntryIndex = 76507 + 100; // 150--87 + int startEntryIndex = 76507; // 150--87 rr.startFrom(startEntryIndex); rr.readNext(); final long s0 = System.currentTimeMillis();