From 923443f94faba9e903cabf35a925e65487b17ac3 Mon Sep 17 00:00:00 2001 From: Chris Aniszczyk Date: Sat, 30 Oct 2010 10:19:51 -0500 Subject: [PATCH] Add CheckoutCommand Add the ability to checkout a branch to the working tree. Bug: 330860 Change-Id: Ie06b9e799a9e1be384da0b8996efa7209b32eac3 Signed-off-by: Chris Aniszczyk --- .../eclipse/jgit/api/CheckoutCommandTest.java | 123 ++++++++ .../org/eclipse/jgit/JGitText.properties | 1 + .../src/org/eclipse/jgit/JGitText.java | 1 + .../org/eclipse/jgit/api/CheckoutCommand.java | 272 ++++++++++++++++++ .../src/org/eclipse/jgit/api/Git.java | 13 + 5 files changed, 410 insertions(+) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java new file mode 100644 index 000000000..295a284c0 --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2010, Chris Aniszczyk + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api; + +import java.io.IOException; + +import org.eclipse.jgit.api.errors.InvalidRefNameException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.api.errors.RefAlreadyExistsException; +import org.eclipse.jgit.api.errors.RefNotFoundException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RepositoryTestCase; +import org.eclipse.jgit.revwalk.RevCommit; + +public class CheckoutCommandTest extends RepositoryTestCase { + private Git git; + + RevCommit initialCommit; + + RevCommit secondCommit; + + @Override + protected void setUp() throws Exception { + super.setUp(); + git = new Git(db); + // commit something + writeTrashFile("Test.txt", "Hello world"); + git.add().addFilepattern("Test.txt").call(); + initialCommit = git.commit().setMessage("Initial commit").call(); + + // create a master branch and switch to it + git.branchCreate().setName("test").call(); + RefUpdate rup = db.updateRef(Constants.HEAD); + rup.link("refs/heads/test"); + + // commit something on the test branch + writeTrashFile("Test.txt", "Some change"); + git.add().addFilepattern("Test.txt").call(); + secondCommit = git.commit().setMessage("Second commit").call(); + } + + public void testSimpleCheckout() { + try { + git.checkout().setName("test").call(); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + public void testCheckout() { + try { + git.checkout().setName("test").call(); + assertEquals("[Test.txt, mode:100644, content:Some change]", + indexState(CONTENT)); + git.checkout().setName("master").call(); + assertEquals("[Test.txt, mode:100644, content:Hello world]", + indexState(CONTENT)); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + public void testCreateBranchOnCheckout() throws IOException { + try { + git.checkout().setCreateBranch(true).setName("test2").call(); + } catch (Exception e) { + fail(e.getMessage()); + } + assertNotNull(db.getRef("test2")); + } + + public void testCheckoutToNonExistingBranch() throws JGitInternalException, + RefAlreadyExistsException, InvalidRefNameException { + try { + git.checkout().setName("badbranch").call(); + fail("Should have failed"); + } catch (RefNotFoundException e) { + // except to hit here + } + } + +} diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties index 4de5744c8..44f67ed1b 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/JGitText.properties @@ -79,6 +79,7 @@ channelMustBeInRange0_255=channel {0} must be in range [0, 255] characterClassIsNotSupported=The character class {0} is not supported. checkoutConflictWithFile=Checkout conflict with file: {0} checkoutConflictWithFiles=Checkout conflict with files: {0} +checkoutUnexpectedResult=Checkout returned unexpected result {0} classCastNotA=Not a {0} collisionOn=Collision on {0} commandWasCalledInTheWrongState=Command {0} was called in the wrong state diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java index 6ec8ed69e..c24e5bb65 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/JGitText.java @@ -137,6 +137,7 @@ public static JGitText get() { /***/ public String cantPassMeATree; /***/ public String channelMustBeInRange0_255; /***/ public String characterClassIsNotSupported; + /***/ public String checkoutUnexpectedResult; /***/ public String checkoutConflictWithFile; /***/ public String checkoutConflictWithFiles; /***/ public String classCastNotA; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java new file mode 100644 index 000000000..4e74a5460 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2010, Chris Aniszczyk + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package org.eclipse.jgit.api; + +import java.io.IOException; +import java.text.MessageFormat; + +import org.eclipse.jgit.JGitText; +import org.eclipse.jgit.api.errors.InvalidRefNameException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.api.errors.RefAlreadyExistsException; +import org.eclipse.jgit.api.errors.RefNotFoundException; +import org.eclipse.jgit.dircache.DirCacheCheckout; +import org.eclipse.jgit.errors.AmbiguousObjectException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.RefUpdate; +import org.eclipse.jgit.lib.RefUpdate.Result; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; + +/** + * Checkout a branch to the working tree + * + * @see Git documentation about Checkout + */ +public class CheckoutCommand extends GitCommand { + private String name; + + private boolean force = false; + + private boolean createBranch = false; + + private CreateBranchCommand.SetupUpstreamMode upstreamMode; + + private String startPoint = Constants.HEAD; + + private RevCommit startCommit; + + /** + * @param repo + */ + protected CheckoutCommand(Repository repo) { + super(repo); + } + + /** + * @throws RefAlreadyExistsException + * when trying to create (without force) a branch with a name + * that already exists + * @throws RefNotFoundException + * if the start point or branch can not be found + * @throws InvalidRefNameException + * if the provided name is null or otherwise + * invalid + * @return the newly created branch + */ + public Ref call() throws JGitInternalException, RefAlreadyExistsException, + RefNotFoundException, InvalidRefNameException { + checkCallable(); + processOptions(); + try { + + if(createBranch) { + Git git = new Git(repo); + CreateBranchCommand command = git.branchCreate(); + command.setName(name); + command.setStartPoint(getStartPoint().name()); + if (upstreamMode != null) + command.setUpstreamMode(upstreamMode); + command.call(); + } + + RevWalk revWalk = new RevWalk(repo); + Ref headRef = repo.getRef(Constants.HEAD); + RevCommit headCommit = revWalk.parseCommit(headRef.getObjectId()); + String refLogMessage = "checkout: moving from " + + headRef.getTarget().getName(); + ObjectId branch = repo.resolve(name); + Ref ref = repo.getRef(name); + if (branch == null) + throw new RefNotFoundException(MessageFormat.format( + JGitText.get().refNotResolved, name)); + + RevCommit newCommit = revWalk.parseCommit(branch); + + DirCacheCheckout dco = new DirCacheCheckout(repo, + headCommit.getTree(), repo.lockDirCache(), + newCommit.getTree()); + dco.setFailOnConflict(true); + dco.checkout(); + RefUpdate refUpdate = repo.updateRef(Constants.HEAD); + refUpdate.setForceUpdate(force); + refUpdate.setRefLogMessage( + refLogMessage + "to " + newCommit.getName(), false); + Result updateResult = refUpdate.link(ref.getName()); + + setCallable(false); + + boolean ok = false; + switch (updateResult) { + case NEW: + ok = true; + break; + case NO_CHANGE: + case FAST_FORWARD: + case FORCED: + ok = true; + break; + default: + break; + } + + if (!ok) + throw new JGitInternalException(MessageFormat.format( + JGitText.get().checkoutUnexpectedResult, + updateResult + .name())); + + Ref result = repo.getRef(name); + + return result; + } catch (IOException ioe) { + throw new JGitInternalException(ioe.getMessage(), ioe); + } + } + + private ObjectId getStartPoint() throws AmbiguousObjectException, + RefNotFoundException, IOException { + if (startCommit != null) + return startCommit.getId(); + ObjectId result = null; + try { + result = repo.resolve((startPoint == null) ? Constants.HEAD + : startPoint); + } catch (AmbiguousObjectException e) { + throw e; + } + if (result == null) + throw new RefNotFoundException(MessageFormat.format( + JGitText.get().refNotResolved, + startPoint != null ? startPoint : Constants.HEAD)); + return result; + } + + private void processOptions() throws InvalidRefNameException { + if (name == null + || !Repository.isValidRefName(Constants.R_HEADS + name)) + throw new InvalidRefNameException(MessageFormat.format(JGitText + .get().branchNameInvalid, name == null ? "" : name)); + } + + /** + * @param name + * the name of the new branch + * @return this instance + */ + public CheckoutCommand setName(String name) { + checkCallable(); + this.name = name; + return this; + } + + /** + * @param createBranch + * if true a branch will be created as part of the + * checkout and set to the specified start point + * @return this instance + */ + public CheckoutCommand setCreateBranch(boolean createBranch) { + checkCallable(); + this.createBranch = createBranch; + return this; + } + + /** + * @param force + * if true and the branch with the given name + * already exists, the start-point of an existing branch will be + * set to a new start-point; if false, the existing branch will + * not be changed + * @return this instance + */ + public CheckoutCommand setForce(boolean force) { + checkCallable(); + this.force = force; + return this; + } + + /** + * @param startPoint + * corresponds to the start-point option; if null, + * the current HEAD will be used + * @return this instance + */ + public CheckoutCommand setStartPoint(String startPoint) { + checkCallable(); + this.startPoint = startPoint; + this.startCommit = null; + return this; + } + + /** + * @param startCommit + * corresponds to the start-point option; if null, + * the current HEAD will be used + * @return this instance + */ + public CheckoutCommand setStartPoint(RevCommit startCommit) { + checkCallable(); + this.startCommit = startCommit; + this.startPoint = null; + return this; + } + + /** + * @param mode + * corresponds to the --track/--no-track options; may be + * null + * @return this instance + */ + public CheckoutCommand setUpstreamMode( + CreateBranchCommand.SetupUpstreamMode mode) { + checkCallable(); + this.upstreamMode = mode; + return this; + } +} diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java index 3bcdfc7aa..8623bb76a 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/Git.java @@ -269,6 +269,19 @@ public RmCommand rm() { return new RmCommand(repo); } + /** + * Returns a command object to execute a {@code checkout} command + * + * @see Git documentation about checkout + * @return a {@link CheckoutCommand} used to collect all optional parameters + * and to finally execute the {@code checkout} command + */ + public CheckoutCommand checkout() { + return new CheckoutCommand(repo); + } + /** * @return the git repository this class is interacting with */