| /* |
| * 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 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(7).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(7).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(7).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)); |
| } |
| } |
| } |
| } |