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 index 4087fb0dd..614cdd0ce 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/CheckoutCommandTest.java @@ -59,7 +59,9 @@ import org.eclipse.jgit.api.CheckoutResult.Status; import org.eclipse.jgit.api.errors.GitAPIException; +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.DirCache; import org.eclipse.jgit.dircache.DirCacheEntry; @@ -351,4 +353,98 @@ public void testUpdateSmudgedEntries() throws Exception { assertEquals(size, entry.getLength()); assertEquals(mTime, entry.getLastModified()); } + + @Test + public void testCheckoutOrphanBranch() throws Exception { + CheckoutCommand co = newOrphanBranchCommand(); + assertCheckoutRef(co.call()); + + File HEAD = new File(trash, ".git/HEAD"); + String headRef = read(HEAD); + assertEquals("ref: refs/heads/orphanbranch\n", headRef); + assertEquals(2, trash.list().length); + + File heads = new File(trash, ".git/refs/heads"); + assertEquals(2, heads.listFiles().length); + + this.assertNoHead(); + this.assertRepositoryCondition(1); + assertEquals(CheckoutResult.NOT_TRIED_RESULT, co.getResult()); + } + + private CheckoutCommand newOrphanBranchCommand() { + return git.checkout().setOrphan(true) + .setName("orphanbranch"); + } + + private static void assertCheckoutRef(Ref ref) { + assertNotNull(ref); + assertEquals("refs/heads/orphanbranch", ref.getTarget().getName()); + } + + private void assertNoHead() throws IOException { + assertNull(db.resolve("HEAD")); + } + + private void assertRepositoryCondition(int files) throws GitAPIException { + org.eclipse.jgit.api.Status status = this.git.status().call(); + assertFalse(status.isClean()); + assertEquals(files, status.getAdded().size()); + } + + @Test + public void testCreateOrphanBranchWithStartCommit() throws Exception { + CheckoutCommand co = newOrphanBranchCommand(); + Ref ref = co.setStartPoint(initialCommit).call(); + assertCheckoutRef(ref); + assertEquals(2, trash.list().length); + this.assertNoHead(); + this.assertRepositoryCondition(1); + } + + @Test + public void testCreateOrphanBranchWithStartPoint() throws Exception { + CheckoutCommand co = newOrphanBranchCommand(); + Ref ref = co.setStartPoint("HEAD^").call(); + assertCheckoutRef(ref); + + assertEquals(2, trash.list().length); + this.assertNoHead(); + this.assertRepositoryCondition(1); + } + + @Test + public void testInvalidRefName() throws Exception { + try { + git.checkout().setOrphan(true).setName("../invalidname").call(); + fail("Should have failed"); + } catch (InvalidRefNameException e) { + // except to hit here + } + } + + @Test + public void testNullRefName() throws Exception { + try { + git.checkout().setOrphan(true).setName(null).call(); + fail("Should have failed"); + } catch (InvalidRefNameException e) { + // except to hit here + } + } + + @Test + public void testAlreadyExists() throws Exception { + this.git.checkout().setCreateBranch(true).setName("orphanbranch") + .call(); + this.git.checkout().setName("master").call(); + + try { + newOrphanBranchCommand().call(); + fail("Should have failed"); + } catch (RefAlreadyExistsException e) { + // except to hit here + } + } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java index 8dfd211a0..ee37bbfb8 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CheckoutCommand.java @@ -47,6 +47,7 @@ import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; +import java.util.EnumSet; import java.util.LinkedList; import java.util.List; @@ -158,6 +159,8 @@ private Stage(int number) { private boolean createBranch = false; + private boolean orphan = false; + private CreateBranchCommand.SetupUpstreamMode upstreamMode; private String startPoint = null; @@ -197,8 +200,8 @@ public Ref call() throws GitAPIException, RefAlreadyExistsException, RefNotFoundException, InvalidRefNameException, CheckoutConflictException { checkCallable(); - processOptions(); try { + processOptions(); if (checkoutAllPaths || !paths.isEmpty()) { checkoutPaths(); status = new CheckoutResult(Status.OK, paths); @@ -219,10 +222,25 @@ public Ref call() throws GitAPIException, RefAlreadyExistsException, Ref headRef = repo.getRef(Constants.HEAD); String shortHeadRef = getShortBranchName(headRef); String refLogMessage = "checkout: moving from " + shortHeadRef; //$NON-NLS-1$ - ObjectId branch = repo.resolve(name); - if (branch == null) - throw new RefNotFoundException(MessageFormat.format(JGitText - .get().refNotResolved, name)); + ObjectId branch; + if (orphan) { + if (startPoint == null && startCommit == null) { + Result r = repo.updateRef(Constants.HEAD).link( + getBranchName()); + if (!EnumSet.of(Result.NEW, Result.FORCED).contains(r)) + throw new JGitInternalException(MessageFormat.format( + JGitText.get().checkoutUnexpectedResult, + r.name())); + this.status = CheckoutResult.NOT_TRIED_RESULT; + return repo.getRef(Constants.HEAD); + } + branch = getStartPoint(); + } else { + branch = repo.resolve(name); + if (branch == null) + throw new RefNotFoundException(MessageFormat.format( + JGitText.get().refNotResolved, name)); + } RevWalk revWalk = new RevWalk(repo); AnyObjectId headId = headRef.getObjectId(); @@ -256,7 +274,10 @@ public Ref call() throws GitAPIException, RefAlreadyExistsException, Result updateResult; if (ref != null) updateResult = refUpdate.link(ref.getName()); - else { + else if (orphan) { + updateResult = refUpdate.link(getBranchName()); + ref = repo.getRef(Constants.HEAD); + } else { refUpdate.setNewObjectId(newCommit); updateResult = refUpdate.forceUpdate(); } @@ -465,12 +486,27 @@ private ObjectId getStartPoint() throws AmbiguousObjectException, return result; } - private void processOptions() throws InvalidRefNameException { - if ((!checkoutAllPaths && paths.isEmpty()) + private void processOptions() throws InvalidRefNameException, + RefAlreadyExistsException, IOException { + if (((!checkoutAllPaths && paths.isEmpty()) || orphan) && (name == null || !Repository .isValidRefName(Constants.R_HEADS + name))) throw new InvalidRefNameException(MessageFormat.format(JGitText .get().branchNameInvalid, name == null ? "" : name)); //$NON-NLS-1$ + + if (orphan) { + Ref refToCheck = repo.getRef(getBranchName()); + if (refToCheck != null) + throw new RefAlreadyExistsException(MessageFormat.format( + JGitText.get().refAlreadyExists, name)); + } + } + + private String getBranchName() { + if (name.startsWith(Constants.R_REFS)) + return name; + + return Constants.R_HEADS + name; } /** @@ -516,6 +552,25 @@ public CheckoutCommand setCreateBranch(boolean createBranch) { return this; } + /** + * Specify whether to create a new orphan branch. + *

+ * If true is used, the name of the new orphan branch must be + * set using {@link #setName(String)}. The commit at which to start the new + * orphan branch can be set using {@link #setStartPoint(String)} or + * {@link #setStartPoint(RevCommit)}; if not specified, HEAD is used. + * + * @param orphan + * if true a orphan branch will be created as part + * of the checkout to the specified start point + * @return this instance + */ + public CheckoutCommand setOrphan(boolean orphan) { + checkCallable(); + this.orphan = orphan; + return this; + } + /** * Specify to force the ref update in case of a branch switch. *