CheckoutCommand.java
- /*
- * Copyright (C) 2010, Chris Aniszczyk <caniszczyk@gmail.com>
- * Copyright (C) 2011, Matthias Sohn <matthias.sohn@sap.com> 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.filter.PathFilterGroup;
- /**
- * Checkout a branch to the working tree.
- * <p>
- * Examples (<code>git</code> is a {@link org.eclipse.jgit.api.Git} instance):
- * <p>
- * Check out an existing branch:
- *
- * <pre>
- * git.checkout().setName("feature").call();
- * </pre>
- * <p>
- * Check out paths from the index:
- *
- * <pre>
- * git.checkout().addPath("file1.txt").addPath("file2.txt").call();
- * </pre>
- * <p>
- * Check out a path from a commit:
- *
- * <pre>
- * git.checkout().setStartPoint("HEADˆ").addPath("file1.txt").call();
- * </pre>
- *
- * <p>
- * Create a new branch and check it out:
- *
- * <pre>
- * git.checkout().setCreateBranch(true).setName("newbranch").call();
- * </pre>
- * <p>
- * Create a new tracking branch for a remote branch and check it out:
- *
- * <pre>
- * git.checkout().setCreateBranch(true).setName("stable")
- * .setUpstreamMode(SetupUpstreamMode.SET_UPSTREAM)
- * .setStartPoint("origin/stable").call();
- * </pre>
- *
- * @see <a href=
- * "http://www.kernel.org/pub/software/scm/git/docs/git-checkout.html" >Git
- * documentation about Checkout</a>
- */
- public class CheckoutCommand extends GitCommand<Ref> {
- /**
- * 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<String> paths;
- private boolean checkoutAllPaths;
- private Set<String> 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)}.
- * <p>
- * 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
- * <code>/</code> 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)}.
- * <p>
- * 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
- * <code>/</code> as separator)
- * @return {@code this}
- * @since 4.6
- */
- public CheckoutCommand addPaths(List<String> p) {
- checkCallable();
- this.paths.addAll(p);
- return this;
- }
- /**
- * Set whether to checkout all paths.
- * <p>
- * 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.
- * <p>
- * 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
- * <code>true</code> to checkout all paths, <code>false</code>
- * 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<>();
- 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);
- else {
- RevCommit commit = revWalk.parseCommit(getStartPointObjectId());
- checkoutPathsFromCommit(treeWalk, dc, commit);
- }
- } 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)
- 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, new CheckoutMetadata(
- eolStreamType, filterCommand));
- actuallyModifiedPaths.add(path);
- }
- } else {
- UnmergedPathException e = new UnmergedPathException(
- ent);
- throw new JGitInternalException(e.getMessage(), e);
- }
- } else {
- checkoutPath(ent, r, new CheckoutMetadata(eolStreamType,
- filterCommand));
- actuallyModifiedPaths.add(path);
- }
- }
- });
- previousPath = path;
- }
- editor.commit();
- }
- private void checkoutPathsFromCommit(TreeWalk treeWalk, DirCache dc,
- RevCommit commit) 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) {
- ent.setObjectId(blobId);
- ent.setFileMode(mode);
- checkoutPath(ent, r,
- new CheckoutMetadata(eolStreamType, filterCommand));
- actuallyModifiedPaths.add(path);
- }
- });
- }
- editor.commit();
- }
- private void checkoutPath(DirCacheEntry entry, ObjectReader reader,
- CheckoutMetadata checkoutMetadata) {
- try {
- DirCacheCheckout.checkoutEntry(repo, entry, reader, true,
- checkoutMetadata);
- } 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 ? "<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.
- * <p>
- * 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.
- * <p>
- * When {@link #setCreateBranch(boolean)} is set to <code>true</code>, 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.
- * <p>
- * If <code>true</code> 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 <code>true</code> 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.
- * <p>
- * If <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>true</code> 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 <code>setForce()</code> offering
- * semantics different from this new <code>setForced()</code>. This old
- * semantic can now be found in {@link #setForceRefUpdate(boolean)}
- *
- * @param forced
- * if set to <code>true</code> 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.
- * <p>
- * When checking out files and this is not specified or <code>null</code>,
- * the index is used.
- * <p>
- * When creating a new branch, this will be used as the start point. If not
- * specified or <code>null</code>, 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.
- * <p>
- * When creating a new branch, this will be used as the start point. If not
- * specified or <code>null</code>, the current HEAD is used.
- * <p>
- * When checking out files and this is not specified or <code>null</code>,
- * 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
- * <code>null</code>
- * @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.
- * <p>
- * 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 <code>null</code>
- *
- * @return the result, never <code>null</code>
- */
- 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);
- }
- }