/* * Copyright (C) 2010, Chris Aniszczyk * Copyright (C) 2011, 2020 Matthias Sohn and others * * This program and the accompanying materials are made available under the * terms of the Eclipse Distribution License v. 1.0 which is available at * https://www.eclipse.org/org/documents/edl-v10.php. * * SPDX-License-Identifier: BSD-3-Clause */ package org.eclipse.jgit.api; import static org.eclipse.jgit.treewalk.TreeWalk.OperationType.CHECKOUT_OP; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Set; import org.eclipse.jgit.api.CheckoutResult.Status; import org.eclipse.jgit.api.errors.CheckoutConflictException; 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.DirCacheCheckout; import org.eclipse.jgit.dircache.DirCacheCheckout.CheckoutMetadata; import org.eclipse.jgit.dircache.DirCacheEditor; import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit; import org.eclipse.jgit.dircache.DirCacheEntry; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.AmbiguousObjectException; import org.eclipse.jgit.errors.UnmergedPathException; import org.eclipse.jgit.events.WorkingTreeModifiedEvent; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.CoreConfig.EolStreamType; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.ProgressMonitor; 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.RevTree; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.WorkingTreeOptions; import org.eclipse.jgit.treewalk.filter.PathFilterGroup; /** * Checkout a branch to the working tree. *

* Examples (git is a {@link org.eclipse.jgit.api.Git} instance): *

* Check out an existing branch: * *

 * git.checkout().setName("feature").call();
 * 
*

* Check out paths from the index: * *

 * git.checkout().addPath("file1.txt").addPath("file2.txt").call();
 * 
*

* Check out a path from a commit: * *

 * git.checkout().setStartPoint("HEADˆ").addPath("file1.txt").call();
 * 
* *

* Create a new branch and check it out: * *

 * git.checkout().setCreateBranch(true).setName("newbranch").call();
 * 
*

* Create a new tracking branch for a remote branch and check it out: * *

 * git.checkout().setCreateBranch(true).setName("stable")
 * 		.setUpstreamMode(SetupUpstreamMode.SET_UPSTREAM)
 * 		.setStartPoint("origin/stable").call();
 * 
* * @see Git * documentation about Checkout */ public class CheckoutCommand extends GitCommand { /** * Stage to check out, see {@link CheckoutCommand#setStage(Stage)}. */ public enum Stage { /** * Base stage (#1) */ BASE(DirCacheEntry.STAGE_1), /** * Ours stage (#2) */ OURS(DirCacheEntry.STAGE_2), /** * Theirs stage (#3) */ THEIRS(DirCacheEntry.STAGE_3); private final int number; private Stage(int number) { this.number = number; } } private String name; private boolean forceRefUpdate = false; private boolean forced = false; private boolean createBranch = false; private boolean orphan = false; private CreateBranchCommand.SetupUpstreamMode upstreamMode; private String startPoint = null; private RevCommit startCommit; private Stage checkoutStage = null; private CheckoutResult status; private List paths; private boolean checkoutAllPaths; private Set actuallyModifiedPaths; private ProgressMonitor monitor = NullProgressMonitor.INSTANCE; /** * Constructor for CheckoutCommand * * @param repo * the {@link org.eclipse.jgit.lib.Repository} */ protected CheckoutCommand(Repository repo) { super(repo); this.paths = new LinkedList<>(); } /** {@inheritDoc} */ @Override public Ref call() throws GitAPIException, RefAlreadyExistsException, RefNotFoundException, InvalidRefNameException, CheckoutConflictException { checkCallable(); try { processOptions(); if (checkoutAllPaths || !paths.isEmpty()) { checkoutPaths(); status = new CheckoutResult(Status.OK, paths); setCallable(false); return null; } if (createBranch) { try (Git git = new Git(repo)) { CreateBranchCommand command = git.branchCreate(); command.setName(name); if (startCommit != null) command.setStartPoint(startCommit); else command.setStartPoint(startPoint); if (upstreamMode != null) command.setUpstreamMode(upstreamMode); command.call(); } } Ref headRef = repo.exactRef(Constants.HEAD); if (headRef == null) { // TODO Git CLI supports checkout from unborn branch, we should // also allow this throw new UnsupportedOperationException( JGitText.get().cannotCheckoutFromUnbornBranch); } String shortHeadRef = getShortBranchName(headRef); String refLogMessage = "checkout: moving from " + shortHeadRef; //$NON-NLS-1$ 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.exactRef(Constants.HEAD); } branch = getStartPointObjectId(); } else { branch = repo.resolve(name); if (branch == null) throw new RefNotFoundException(MessageFormat.format( JGitText.get().refNotResolved, name)); } RevCommit headCommit = null; RevCommit newCommit = null; try (RevWalk revWalk = new RevWalk(repo)) { AnyObjectId headId = headRef.getObjectId(); headCommit = headId == null ? null : revWalk.parseCommit(headId); newCommit = revWalk.parseCommit(branch); } RevTree headTree = headCommit == null ? null : headCommit.getTree(); DirCacheCheckout dco; DirCache dc = repo.lockDirCache(); try { dco = new DirCacheCheckout(repo, headTree, dc, newCommit.getTree()); dco.setFailOnConflict(true); dco.setForce(forced); if (forced) { dco.setFailOnConflict(false); } dco.setProgressMonitor(monitor); try { dco.checkout(); } catch (org.eclipse.jgit.errors.CheckoutConflictException e) { status = new CheckoutResult(Status.CONFLICTS, dco.getConflicts()); throw new CheckoutConflictException(dco.getConflicts(), e); } } finally { dc.unlock(); } Ref ref = repo.findRef(name); if (ref != null && !ref.getName().startsWith(Constants.R_HEADS)) ref = null; String toName = Repository.shortenRefName(name); RefUpdate refUpdate = repo.updateRef(Constants.HEAD, ref == null); refUpdate.setForceUpdate(forceRefUpdate); refUpdate.setRefLogMessage(refLogMessage + " to " + toName, false); //$NON-NLS-1$ Result updateResult; if (ref != null) updateResult = refUpdate.link(ref.getName()); else if (orphan) { updateResult = refUpdate.link(getBranchName()); ref = repo.exactRef(Constants.HEAD); } else { refUpdate.setNewObjectId(newCommit); updateResult = refUpdate.forceUpdate(); } 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())); if (!dco.getToBeDeleted().isEmpty()) { status = new CheckoutResult(Status.NONDELETED, dco.getToBeDeleted(), new ArrayList<>(dco.getUpdated().keySet()), dco.getRemoved()); } else status = new CheckoutResult(new ArrayList<>(dco .getUpdated().keySet()), dco.getRemoved()); return ref; } catch (IOException ioe) { throw new JGitInternalException(ioe.getMessage(), ioe); } finally { if (status == null) status = CheckoutResult.ERROR_RESULT; } } private String getShortBranchName(Ref headRef) { if (headRef.isSymbolic()) { return Repository.shortenRefName(headRef.getTarget().getName()); } // Detached HEAD. Every non-symbolic ref in the ref database has an // object id, so this cannot be null. ObjectId id = headRef.getObjectId(); if (id == null) { throw new NullPointerException(); } return id.getName(); } /** * @param monitor * a progress monitor * @return this instance * @since 4.11 */ public CheckoutCommand setProgressMonitor(ProgressMonitor monitor) { if (monitor == null) { monitor = NullProgressMonitor.INSTANCE; } this.monitor = monitor; return this; } /** * Add a single slash-separated path to the list of paths to check out. To * check out all paths, use {@link #setAllPaths(boolean)}. *

* If this option is set, neither the {@link #setCreateBranch(boolean)} nor * {@link #setName(String)} option is considered. In other words, these * options are exclusive. * * @param path * path to update in the working tree and index (with * / as separator) * @return {@code this} */ public CheckoutCommand addPath(String path) { checkCallable(); this.paths.add(path); return this; } /** * Add multiple slash-separated paths to the list of paths to check out. To * check out all paths, use {@link #setAllPaths(boolean)}. *

* If this option is set, neither the {@link #setCreateBranch(boolean)} nor * {@link #setName(String)} option is considered. In other words, these * options are exclusive. * * @param p * paths to update in the working tree and index (with * / as separator) * @return {@code this} * @since 4.6 */ public CheckoutCommand addPaths(List p) { checkCallable(); this.paths.addAll(p); return this; } /** * Set whether to checkout all paths. *

* This options should be used when you want to do a path checkout on the * entire repository and so calling {@link #addPath(String)} is not possible * since empty paths are not allowed. *

* If this option is set, neither the {@link #setCreateBranch(boolean)} nor * {@link #setName(String)} option is considered. In other words, these * options are exclusive. * * @param all * true to checkout all paths, false * otherwise * @return {@code this} * @since 2.0 */ public CheckoutCommand setAllPaths(boolean all) { checkoutAllPaths = all; return this; } /** * Checkout paths into index and working directory, firing a * {@link org.eclipse.jgit.events.WorkingTreeModifiedEvent} if the working * tree was modified. * * @return this instance * @throws java.io.IOException * @throws org.eclipse.jgit.api.errors.RefNotFoundException */ protected CheckoutCommand checkoutPaths() throws IOException, RefNotFoundException { actuallyModifiedPaths = new HashSet<>(); WorkingTreeOptions options = repo.getConfig() .get(WorkingTreeOptions.KEY); DirCache dc = repo.lockDirCache(); try (RevWalk revWalk = new RevWalk(repo); TreeWalk treeWalk = new TreeWalk(repo, revWalk.getObjectReader())) { treeWalk.setRecursive(true); if (!checkoutAllPaths) treeWalk.setFilter(PathFilterGroup.createFromStrings(paths)); if (isCheckoutIndex()) checkoutPathsFromIndex(treeWalk, dc, options); else { RevCommit commit = revWalk.parseCommit(getStartPointObjectId()); checkoutPathsFromCommit(treeWalk, dc, commit, options); } } finally { try { dc.unlock(); } finally { WorkingTreeModifiedEvent event = new WorkingTreeModifiedEvent( actuallyModifiedPaths, null); actuallyModifiedPaths = null; if (!event.isEmpty()) { repo.fireEvent(event); } } } return this; } private void checkoutPathsFromIndex(TreeWalk treeWalk, DirCache dc, WorkingTreeOptions options) throws IOException { DirCacheIterator dci = new DirCacheIterator(dc); treeWalk.addTree(dci); String previousPath = null; final ObjectReader r = treeWalk.getObjectReader(); DirCacheEditor editor = dc.editor(); while (treeWalk.next()) { String path = treeWalk.getPathString(); // Only add one edit per path if (path.equals(previousPath)) continue; final EolStreamType eolStreamType = treeWalk .getEolStreamType(CHECKOUT_OP); final String filterCommand = treeWalk .getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE); editor.add(new PathEdit(path) { @Override public void apply(DirCacheEntry ent) { int stage = ent.getStage(); if (stage > DirCacheEntry.STAGE_0) { if (checkoutStage != null) { if (stage == checkoutStage.number) { checkoutPath(ent, r, options, new CheckoutMetadata(eolStreamType, filterCommand)); actuallyModifiedPaths.add(path); } } else { UnmergedPathException e = new UnmergedPathException( ent); throw new JGitInternalException(e.getMessage(), e); } } else { checkoutPath(ent, r, options, new CheckoutMetadata(eolStreamType, filterCommand)); actuallyModifiedPaths.add(path); } } }); previousPath = path; } editor.commit(); } private void checkoutPathsFromCommit(TreeWalk treeWalk, DirCache dc, RevCommit commit, WorkingTreeOptions options) throws IOException { treeWalk.addTree(commit.getTree()); final ObjectReader r = treeWalk.getObjectReader(); DirCacheEditor editor = dc.editor(); while (treeWalk.next()) { final ObjectId blobId = treeWalk.getObjectId(0); final FileMode mode = treeWalk.getFileMode(0); final EolStreamType eolStreamType = treeWalk .getEolStreamType(CHECKOUT_OP); final String filterCommand = treeWalk .getFilterCommand(Constants.ATTR_FILTER_TYPE_SMUDGE); final String path = treeWalk.getPathString(); editor.add(new PathEdit(path) { @Override public void apply(DirCacheEntry ent) { if (ent.getStage() != DirCacheEntry.STAGE_0) { // A checkout on a conflicting file stages the checked // out file and resolves the conflict. ent.setStage(DirCacheEntry.STAGE_0); } ent.setObjectId(blobId); ent.setFileMode(mode); checkoutPath(ent, r, options, new CheckoutMetadata(eolStreamType, filterCommand)); actuallyModifiedPaths.add(path); } }); } editor.commit(); } private void checkoutPath(DirCacheEntry entry, ObjectReader reader, WorkingTreeOptions options, CheckoutMetadata checkoutMetadata) { try { DirCacheCheckout.checkoutEntry(repo, entry, reader, true, checkoutMetadata, options); } catch (IOException e) { throw new JGitInternalException(MessageFormat.format( JGitText.get().checkoutConflictWithFile, entry.getPathString()), e); } } private boolean isCheckoutIndex() { return startCommit == null && startPoint == null; } private ObjectId getStartPointObjectId() throws AmbiguousObjectException, RefNotFoundException, IOException { if (startCommit != null) return startCommit.getId(); String startPointOrHead = (startPoint != null) ? startPoint : Constants.HEAD; ObjectId result = repo.resolve(startPointOrHead); if (result == null) throw new RefNotFoundException(MessageFormat.format( JGitText.get().refNotResolved, startPointOrHead)); return result; } 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.exactRef(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; } /** * Specify the name of the branch or commit to check out, or the new branch * name. *

* When only checking out paths and not switching branches, use * {@link #setStartPoint(String)} or {@link #setStartPoint(RevCommit)} to * specify from which branch or commit to check out files. *

* When {@link #setCreateBranch(boolean)} is set to true, use * this method to set the name of the new branch to create and * {@link #setStartPoint(String)} or {@link #setStartPoint(RevCommit)} to * specify the start point of the branch. * * @param name * the name of the branch or commit * @return this instance */ public CheckoutCommand setName(String name) { checkCallable(); this.name = name; return this; } /** * Specify whether to create a new branch. *

* If true is used, the name of the new branch must be set * using {@link #setName(String)}. The commit at which to start the new * branch can be set using {@link #setStartPoint(String)} or * {@link #setStartPoint(RevCommit)}; if not specified, HEAD is used. Also * see {@link #setUpstreamMode} for setting up branch tracking. * * @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; } /** * 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 * @since 3.3 */ public CheckoutCommand setOrphan(boolean orphan) { checkCallable(); this.orphan = orphan; return this; } /** * Specify to force the ref update in case of a branch switch. * * @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 * @deprecated this method was badly named comparing its semantics to native * git's checkout --force option, use * {@link #setForceRefUpdate(boolean)} instead */ @Deprecated public CheckoutCommand setForce(boolean force) { return setForceRefUpdate(force); } /** * Specify to force the ref update in case of a branch switch. * * In releases prior to 5.2 this method was called setForce() but this name * was misunderstood to implement native git's --force option, which is not * true. * * @param forceRefUpdate * 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 * @since 5.3 */ public CheckoutCommand setForceRefUpdate(boolean forceRefUpdate) { checkCallable(); this.forceRefUpdate = forceRefUpdate; return this; } /** * Allow a checkout even if the workingtree or index differs from HEAD. This * matches native git's '--force' option. * * JGit releases before 5.2 had a method setForce() offering * semantics different from this new setForced(). This old * semantic can now be found in {@link #setForceRefUpdate(boolean)} * * @param forced * if set to true then allow the checkout even if * workingtree or index doesn't match HEAD. Overwrite workingtree * files and index content with the new content in this case. * @return this instance * @since 5.3 */ public CheckoutCommand setForced(boolean forced) { checkCallable(); this.forced = forced; return this; } /** * Set the name of the commit that should be checked out. *

* When checking out files and this is not specified or null, * the index is used. *

* When creating a new branch, this will be used as the start point. If not * specified or null, the current HEAD is used. * * @param startPoint * commit name to check out * @return this instance */ public CheckoutCommand setStartPoint(String startPoint) { checkCallable(); this.startPoint = startPoint; this.startCommit = null; checkOptions(); return this; } /** * Set the commit that should be checked out. *

* When creating a new branch, this will be used as the start point. If not * specified or null, the current HEAD is used. *

* When checking out files and this is not specified or null, * the index is used. * * @param startCommit * commit to check out * @return this instance */ public CheckoutCommand setStartPoint(RevCommit startCommit) { checkCallable(); this.startCommit = startCommit; this.startPoint = null; checkOptions(); return this; } /** * When creating a branch with {@link #setCreateBranch(boolean)}, this can * be used to configure branch tracking. * * @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; } /** * When checking out the index, check out the specified stage (ours or * theirs) for unmerged paths. *

* This can not be used when checking out a branch, only when checking out * the index. * * @param stage * the stage to check out * @return this */ public CheckoutCommand setStage(Stage stage) { checkCallable(); this.checkoutStage = stage; checkOptions(); return this; } /** * Get the result, never null * * @return the result, never null */ public CheckoutResult getResult() { if (status == null) return CheckoutResult.NOT_TRIED_RESULT; return status; } private void checkOptions() { if (checkoutStage != null && !isCheckoutIndex()) throw new IllegalStateException( JGitText.get().cannotCheckoutOursSwitchBranch); } }