StashCreateCommand.java
- /*
- * Copyright (C) 2012, GitHub Inc. 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.lib.Constants.OBJECT_ID_ABBREV_STRING_LENGTH;
- import java.io.File;
- import java.io.IOException;
- import java.io.InputStream;
- import java.text.MessageFormat;
- import java.util.ArrayList;
- import java.util.List;
- import org.eclipse.jgit.api.ResetCommand.ResetType;
- import org.eclipse.jgit.api.errors.GitAPIException;
- import org.eclipse.jgit.api.errors.JGitInternalException;
- import org.eclipse.jgit.api.errors.NoHeadException;
- import org.eclipse.jgit.api.errors.UnmergedPathsException;
- import org.eclipse.jgit.dircache.DirCache;
- import org.eclipse.jgit.dircache.DirCacheBuilder;
- import org.eclipse.jgit.dircache.DirCacheEditor;
- import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
- import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
- import org.eclipse.jgit.dircache.DirCacheEntry;
- import org.eclipse.jgit.dircache.DirCacheIterator;
- import org.eclipse.jgit.errors.UnmergedPathException;
- import org.eclipse.jgit.events.WorkingTreeModifiedEvent;
- import org.eclipse.jgit.internal.JGitText;
- import org.eclipse.jgit.lib.CommitBuilder;
- import org.eclipse.jgit.lib.Constants;
- import org.eclipse.jgit.lib.MutableObjectId;
- import org.eclipse.jgit.lib.ObjectId;
- import org.eclipse.jgit.lib.ObjectInserter;
- import org.eclipse.jgit.lib.ObjectReader;
- import org.eclipse.jgit.lib.PersonIdent;
- import org.eclipse.jgit.lib.Ref;
- import org.eclipse.jgit.lib.RefUpdate;
- import org.eclipse.jgit.lib.Repository;
- import org.eclipse.jgit.revwalk.RevCommit;
- import org.eclipse.jgit.revwalk.RevWalk;
- import org.eclipse.jgit.treewalk.AbstractTreeIterator;
- import org.eclipse.jgit.treewalk.FileTreeIterator;
- import org.eclipse.jgit.treewalk.TreeWalk;
- import org.eclipse.jgit.treewalk.WorkingTreeIterator;
- import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
- import org.eclipse.jgit.treewalk.filter.IndexDiffFilter;
- import org.eclipse.jgit.treewalk.filter.SkipWorkTreeFilter;
- import org.eclipse.jgit.util.FileUtils;
- /**
- * Command class to stash changes in the working directory and index in a
- * commit.
- *
- * @see <a href="http://www.kernel.org/pub/software/scm/git/docs/git-stash.html"
- * >Git documentation about Stash</a>
- * @since 2.0
- */
- public class StashCreateCommand extends GitCommand<RevCommit> {
- private static final String MSG_INDEX = "index on {0}: {1} {2}"; //$NON-NLS-1$
- private static final String MSG_UNTRACKED = "untracked files on {0}: {1} {2}"; //$NON-NLS-1$
- private static final String MSG_WORKING_DIR = "WIP on {0}: {1} {2}"; //$NON-NLS-1$
- private String indexMessage = MSG_INDEX;
- private String workingDirectoryMessage = MSG_WORKING_DIR;
- private String ref = Constants.R_STASH;
- private PersonIdent person;
- private boolean includeUntracked;
- /**
- * Create a command to stash changes in the working directory and index
- *
- * @param repo
- * a {@link org.eclipse.jgit.lib.Repository} object.
- */
- public StashCreateCommand(Repository repo) {
- super(repo);
- person = new PersonIdent(repo);
- }
- /**
- * Set the message used when committing index changes
- * <p>
- * The message will be formatted with the current branch, abbreviated commit
- * id, and short commit message when used.
- *
- * @param message
- * the stash message
- * @return {@code this}
- */
- public StashCreateCommand setIndexMessage(String message) {
- indexMessage = message;
- return this;
- }
- /**
- * Set the message used when committing working directory changes
- * <p>
- * The message will be formatted with the current branch, abbreviated commit
- * id, and short commit message when used.
- *
- * @param message
- * the working directory message
- * @return {@code this}
- */
- public StashCreateCommand setWorkingDirectoryMessage(String message) {
- workingDirectoryMessage = message;
- return this;
- }
- /**
- * Set the person to use as the author and committer in the commits made
- *
- * @param person
- * the {@link org.eclipse.jgit.lib.PersonIdent} of the person who
- * creates the stash.
- * @return {@code this}
- */
- public StashCreateCommand setPerson(PersonIdent person) {
- this.person = person;
- return this;
- }
- /**
- * Set the reference to update with the stashed commit id If null, no
- * reference is updated
- * <p>
- * This value defaults to {@link org.eclipse.jgit.lib.Constants#R_STASH}
- *
- * @param ref
- * the name of the {@code Ref} to update
- * @return {@code this}
- */
- public StashCreateCommand setRef(String ref) {
- this.ref = ref;
- return this;
- }
- /**
- * Whether to include untracked files in the stash.
- *
- * @param includeUntracked
- * whether to include untracked files in the stash
- * @return {@code this}
- * @since 3.4
- */
- public StashCreateCommand setIncludeUntracked(boolean includeUntracked) {
- this.includeUntracked = includeUntracked;
- return this;
- }
- private RevCommit parseCommit(final ObjectReader reader,
- final ObjectId headId) throws IOException {
- try (RevWalk walk = new RevWalk(reader)) {
- return walk.parseCommit(headId);
- }
- }
- private CommitBuilder createBuilder() {
- CommitBuilder builder = new CommitBuilder();
- PersonIdent author = person;
- if (author == null)
- author = new PersonIdent(repo);
- builder.setAuthor(author);
- builder.setCommitter(author);
- return builder;
- }
- private void updateStashRef(ObjectId commitId, PersonIdent refLogIdent,
- String refLogMessage) throws IOException {
- if (ref == null)
- return;
- Ref currentRef = repo.findRef(ref);
- RefUpdate refUpdate = repo.updateRef(ref);
- refUpdate.setNewObjectId(commitId);
- refUpdate.setRefLogIdent(refLogIdent);
- refUpdate.setRefLogMessage(refLogMessage, false);
- refUpdate.setForceRefLog(true);
- if (currentRef != null)
- refUpdate.setExpectedOldObjectId(currentRef.getObjectId());
- else
- refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
- refUpdate.forceUpdate();
- }
- private Ref getHead() throws GitAPIException {
- try {
- Ref head = repo.exactRef(Constants.HEAD);
- if (head == null || head.getObjectId() == null)
- throw new NoHeadException(JGitText.get().headRequiredToStash);
- return head;
- } catch (IOException e) {
- throw new JGitInternalException(JGitText.get().stashFailed, e);
- }
- }
- /**
- * {@inheritDoc}
- * <p>
- * Stash the contents on the working directory and index in separate commits
- * and reset to the current HEAD commit.
- */
- @Override
- public RevCommit call() throws GitAPIException {
- checkCallable();
- List<String> deletedFiles = new ArrayList<>();
- Ref head = getHead();
- try (ObjectReader reader = repo.newObjectReader()) {
- RevCommit headCommit = parseCommit(reader, head.getObjectId());
- DirCache cache = repo.lockDirCache();
- ObjectId commitId;
- try (ObjectInserter inserter = repo.newObjectInserter();
- TreeWalk treeWalk = new TreeWalk(repo, reader)) {
- treeWalk.setRecursive(true);
- treeWalk.addTree(headCommit.getTree());
- treeWalk.addTree(new DirCacheIterator(cache));
- treeWalk.addTree(new FileTreeIterator(repo));
- treeWalk.getTree(2, FileTreeIterator.class)
- .setDirCacheIterator(treeWalk, 1);
- treeWalk.setFilter(AndTreeFilter.create(new SkipWorkTreeFilter(
- 1), new IndexDiffFilter(1, 2)));
- // Return null if no local changes to stash
- if (!treeWalk.next())
- return null;
- MutableObjectId id = new MutableObjectId();
- List<PathEdit> wtEdits = new ArrayList<>();
- List<String> wtDeletes = new ArrayList<>();
- List<DirCacheEntry> untracked = new ArrayList<>();
- boolean hasChanges = false;
- do {
- AbstractTreeIterator headIter = treeWalk.getTree(0,
- AbstractTreeIterator.class);
- DirCacheIterator indexIter = treeWalk.getTree(1,
- DirCacheIterator.class);
- WorkingTreeIterator wtIter = treeWalk.getTree(2,
- WorkingTreeIterator.class);
- if (indexIter != null
- && !indexIter.getDirCacheEntry().isMerged())
- throw new UnmergedPathsException(
- new UnmergedPathException(
- indexIter.getDirCacheEntry()));
- if (wtIter != null) {
- if (indexIter == null && headIter == null
- && !includeUntracked)
- continue;
- hasChanges = true;
- if (indexIter != null && wtIter.idEqual(indexIter))
- continue;
- if (headIter != null && wtIter.idEqual(headIter))
- continue;
- treeWalk.getObjectId(id, 0);
- final DirCacheEntry entry = new DirCacheEntry(
- treeWalk.getRawPath());
- entry.setLength(wtIter.getEntryLength());
- entry.setLastModified(
- wtIter.getEntryLastModifiedInstant());
- entry.setFileMode(wtIter.getEntryFileMode());
- long contentLength = wtIter.getEntryContentLength();
- try (InputStream in = wtIter.openEntryStream()) {
- entry.setObjectId(inserter.insert(
- Constants.OBJ_BLOB, contentLength, in));
- }
- if (indexIter == null && headIter == null)
- untracked.add(entry);
- else
- wtEdits.add(new PathEdit(entry) {
- @Override
- public void apply(DirCacheEntry ent) {
- ent.copyMetaData(entry);
- }
- });
- }
- hasChanges = true;
- if (wtIter == null && headIter != null)
- wtDeletes.add(treeWalk.getPathString());
- } while (treeWalk.next());
- if (!hasChanges)
- return null;
- String branch = Repository.shortenRefName(head.getTarget()
- .getName());
- // Commit index changes
- CommitBuilder builder = createBuilder();
- builder.setParentId(headCommit);
- builder.setTreeId(cache.writeTree(inserter));
- builder.setMessage(MessageFormat.format(indexMessage, branch,
- headCommit.abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH)
- .name(),
- headCommit.getShortMessage()));
- ObjectId indexCommit = inserter.insert(builder);
- // Commit untracked changes
- ObjectId untrackedCommit = null;
- if (!untracked.isEmpty()) {
- DirCache untrackedDirCache = DirCache.newInCore();
- DirCacheBuilder untrackedBuilder = untrackedDirCache
- .builder();
- for (DirCacheEntry entry : untracked)
- untrackedBuilder.add(entry);
- untrackedBuilder.finish();
- builder.setParentIds(new ObjectId[0]);
- builder.setTreeId(untrackedDirCache.writeTree(inserter));
- builder.setMessage(MessageFormat.format(MSG_UNTRACKED,
- branch,
- headCommit
- .abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH)
- .name(),
- headCommit.getShortMessage()));
- untrackedCommit = inserter.insert(builder);
- }
- // Commit working tree changes
- if (!wtEdits.isEmpty() || !wtDeletes.isEmpty()) {
- DirCacheEditor editor = cache.editor();
- for (PathEdit edit : wtEdits)
- editor.add(edit);
- for (String path : wtDeletes)
- editor.add(new DeletePath(path));
- editor.finish();
- }
- builder.setParentId(headCommit);
- builder.addParentId(indexCommit);
- if (untrackedCommit != null)
- builder.addParentId(untrackedCommit);
- builder.setMessage(MessageFormat.format(
- workingDirectoryMessage, branch,
- headCommit.abbreviate(OBJECT_ID_ABBREV_STRING_LENGTH)
- .name(),
- headCommit.getShortMessage()));
- builder.setTreeId(cache.writeTree(inserter));
- commitId = inserter.insert(builder);
- inserter.flush();
- updateStashRef(commitId, builder.getAuthor(),
- builder.getMessage());
- // Remove untracked files
- if (includeUntracked) {
- for (DirCacheEntry entry : untracked) {
- String repoRelativePath = entry.getPathString();
- File file = new File(repo.getWorkTree(),
- repoRelativePath);
- FileUtils.delete(file);
- deletedFiles.add(repoRelativePath);
- }
- }
- } finally {
- cache.unlock();
- }
- // Hard reset to HEAD
- new ResetCommand(repo).setMode(ResetType.HARD).call();
- // Return stashed commit
- return parseCommit(reader, commitId);
- } catch (IOException e) {
- throw new JGitInternalException(JGitText.get().stashFailed, e);
- } finally {
- if (!deletedFiles.isEmpty()) {
- repo.fireEvent(
- new WorkingTreeModifiedEvent(null, deletedFiles));
- }
- }
- }
- }