/* * Copyright (C) 2010, 2013 Mathias Kinzler * Copyright (C) 2016, 2021 Laurent Delaigue 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 java.nio.charset.StandardCharsets.UTF_8; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.jgit.annotations.NonNull; import org.eclipse.jgit.api.RebaseResult.Status; import org.eclipse.jgit.api.ResetCommand.ResetType; import org.eclipse.jgit.api.errors.CheckoutConflictException; import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.api.errors.GitAPIException; import org.eclipse.jgit.api.errors.InvalidRebaseStepException; import org.eclipse.jgit.api.errors.InvalidRefNameException; import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.api.errors.NoMessageException; import org.eclipse.jgit.api.errors.RefAlreadyExistsException; import org.eclipse.jgit.api.errors.RefNotFoundException; import org.eclipse.jgit.api.errors.StashApplyFailureException; import org.eclipse.jgit.api.errors.UnmergedPathsException; import org.eclipse.jgit.api.errors.WrongRepositoryStateException; import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.dircache.DirCache; import org.eclipse.jgit.dircache.DirCacheCheckout; import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.RevisionSyntaxException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.CommitConfig; import org.eclipse.jgit.lib.CommitConfig.CleanupMode; import org.eclipse.jgit.lib.ConfigConstants; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.NullProgressMonitor; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectReader; import org.eclipse.jgit.lib.PersonIdent; import org.eclipse.jgit.lib.ProgressMonitor; import org.eclipse.jgit.lib.RebaseTodoLine; import org.eclipse.jgit.lib.RebaseTodoLine.Action; 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.merge.ContentMergeStrategy; import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevSort; import org.eclipse.jgit.revwalk.RevWalk; import org.eclipse.jgit.revwalk.filter.RevFilter; import org.eclipse.jgit.submodule.SubmoduleWalk.IgnoreSubmoduleMode; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.FileUtils; import org.eclipse.jgit.util.IO; import org.eclipse.jgit.util.RawParseUtils; /** * A class used to execute a {@code Rebase} command. It has setters for all * supported options and arguments of this command and a {@link #call()} method * to finally execute the command. Each instance of this class should only be * used for one invocation of the command (means: one call to {@link #call()}) *

* * @see Git documentation about Rebase */ public class RebaseCommand extends GitCommand { /** * The name of the "rebase-merge" folder for interactive rebases. */ public static final String REBASE_MERGE = "rebase-merge"; //$NON-NLS-1$ /** * The name of the "rebase-apply" folder for non-interactive rebases. */ private static final String REBASE_APPLY = "rebase-apply"; //$NON-NLS-1$ /** * The name of the "stopped-sha" file */ public static final String STOPPED_SHA = "stopped-sha"; //$NON-NLS-1$ private static final String AUTHOR_SCRIPT = "author-script"; //$NON-NLS-1$ private static final String DONE = "done"; //$NON-NLS-1$ private static final String GIT_AUTHOR_DATE = "GIT_AUTHOR_DATE"; //$NON-NLS-1$ private static final String GIT_AUTHOR_EMAIL = "GIT_AUTHOR_EMAIL"; //$NON-NLS-1$ private static final String GIT_AUTHOR_NAME = "GIT_AUTHOR_NAME"; //$NON-NLS-1$ private static final String GIT_REBASE_TODO = "git-rebase-todo"; //$NON-NLS-1$ private static final String HEAD_NAME = "head-name"; //$NON-NLS-1$ private static final String INTERACTIVE = "interactive"; //$NON-NLS-1$ private static final String QUIET = "quiet"; //$NON-NLS-1$ private static final String MESSAGE = "message"; //$NON-NLS-1$ private static final String ONTO = "onto"; //$NON-NLS-1$ private static final String ONTO_NAME = "onto_name"; //$NON-NLS-1$ private static final String PATCH = "patch"; //$NON-NLS-1$ private static final String REBASE_HEAD = "orig-head"; //$NON-NLS-1$ /** Pre git 1.7.6 file name for {@link #REBASE_HEAD}. */ private static final String REBASE_HEAD_LEGACY = "head"; //$NON-NLS-1$ private static final String AMEND = "amend"; //$NON-NLS-1$ private static final String MESSAGE_FIXUP = "message-fixup"; //$NON-NLS-1$ private static final String MESSAGE_SQUASH = "message-squash"; //$NON-NLS-1$ private static final String AUTOSTASH = "autostash"; //$NON-NLS-1$ private static final String AUTOSTASH_MSG = "On {0}: autostash"; //$NON-NLS-1$ /** * The folder containing the hashes of (potentially) rewritten commits when * --preserve-merges is used. *

* Native git rebase --merge uses a file of that name to record * commits to copy notes at the end of the whole rebase. *

*/ private static final String REWRITTEN = "rewritten"; //$NON-NLS-1$ /** * File containing the current commit(s) to cherry pick when --preserve-merges * is used. */ private static final String CURRENT_COMMIT = "current-commit"; //$NON-NLS-1$ private static final String REFLOG_PREFIX = "rebase:"; //$NON-NLS-1$ /** * The available operations */ public enum Operation { /** * Initiates rebase */ BEGIN, /** * Continues after a conflict resolution */ CONTINUE, /** * Skips the "current" commit */ SKIP, /** * Aborts and resets the current rebase */ ABORT, /** * Starts processing steps * @since 3.2 */ PROCESS_STEPS; } private Operation operation = Operation.BEGIN; private RevCommit upstreamCommit; private String upstreamCommitName; private ProgressMonitor monitor = NullProgressMonitor.INSTANCE; private final RevWalk walk; private final RebaseState rebaseState; private InteractiveHandler interactiveHandler; private CommitConfig commitConfig; private boolean stopAfterInitialization = false; private RevCommit newHead; private boolean lastStepWasForward; private MergeStrategy strategy = MergeStrategy.RECURSIVE; private ContentMergeStrategy contentStrategy; private boolean preserveMerges = false; /** *

* Constructor for RebaseCommand. *

* * @param repo * the {@link org.eclipse.jgit.lib.Repository} */ protected RebaseCommand(Repository repo) { super(repo); walk = new RevWalk(repo); rebaseState = new RebaseState(repo.getDirectory()); } /** * {@inheritDoc} *

* Executes the {@code Rebase} command with all the options and parameters * collected by the setter methods of this class. Each instance of this * class should only be used for one invocation of the command. Don't call * this method twice on an instance. */ @Override public RebaseResult call() throws GitAPIException, NoHeadException, RefNotFoundException, WrongRepositoryStateException { newHead = null; lastStepWasForward = false; checkCallable(); checkParameters(); commitConfig = repo.getConfig().get(CommitConfig.KEY); try { switch (operation) { case ABORT: try { return abort(RebaseResult.ABORTED_RESULT); } catch (IOException ioe) { throw new JGitInternalException(ioe.getMessage(), ioe); } case PROCESS_STEPS: case SKIP: case CONTINUE: String upstreamCommitId = rebaseState.readFile(ONTO); try { upstreamCommitName = rebaseState.readFile(ONTO_NAME); } catch (FileNotFoundException e) { // Fall back to commit ID if file doesn't exist (e.g. rebase // was started by C Git) upstreamCommitName = upstreamCommitId; } this.upstreamCommit = walk.parseCommit(repo .resolve(upstreamCommitId)); preserveMerges = rebaseState.getRewrittenDir().isDirectory(); break; case BEGIN: autoStash(); if (stopAfterInitialization || !walk.isMergedInto( walk.parseCommit(repo.resolve(Constants.HEAD)), upstreamCommit)) { org.eclipse.jgit.api.Status status = Git.wrap(repo) .status().setIgnoreSubmodules(IgnoreSubmoduleMode.ALL).call(); if (status.hasUncommittedChanges()) { List list = new ArrayList<>(); list.addAll(status.getUncommittedChanges()); return RebaseResult.uncommittedChanges(list); } } RebaseResult res = initFilesAndRewind(); if (stopAfterInitialization) return RebaseResult.INTERACTIVE_PREPARED_RESULT; if (res != null) { autoStashApply(); if (rebaseState.getDir().exists()) FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE); return res; } } if (monitor.isCancelled()) return abort(RebaseResult.ABORTED_RESULT); if (operation == Operation.CONTINUE) { newHead = continueRebase(); List doneLines = repo.readRebaseTodo( rebaseState.getPath(DONE), true); RebaseTodoLine step = doneLines.get(doneLines.size() - 1); if (newHead != null && step.getAction() != Action.PICK) { RebaseTodoLine newStep = new RebaseTodoLine( step.getAction(), AbbreviatedObjectId.fromObjectId(newHead), step.getShortMessage()); RebaseResult result = processStep(newStep, false); if (result != null) return result; } File amendFile = rebaseState.getFile(AMEND); boolean amendExists = amendFile.exists(); if (amendExists) { FileUtils.delete(amendFile); } if (newHead == null && !amendExists) { // continueRebase() returns null only if no commit was // neccessary. This means that no changes where left over // after resolving all conflicts. In this case, cgit stops // and displays a nice message to the user, telling him to // either do changes or skip the commit instead of continue. return RebaseResult.NOTHING_TO_COMMIT_RESULT; } } if (operation == Operation.SKIP) newHead = checkoutCurrentHead(); List steps = repo.readRebaseTodo( rebaseState.getPath(GIT_REBASE_TODO), false); if (steps.isEmpty()) { return finishRebase(walk.parseCommit(repo.resolve(Constants.HEAD)), false); } if (isInteractive()) { interactiveHandler.prepareSteps(steps); repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO), steps, false); } checkSteps(steps); for (RebaseTodoLine step : steps) { popSteps(1); RebaseResult result = processStep(step, true); if (result != null) { return result; } } return finishRebase(newHead, lastStepWasForward); } catch (CheckoutConflictException cce) { return RebaseResult.conflicts(cce.getConflictingPaths()); } catch (IOException ioe) { throw new JGitInternalException(ioe.getMessage(), ioe); } } private void autoStash() throws GitAPIException, IOException { if (repo.getConfig().getBoolean(ConfigConstants.CONFIG_REBASE_SECTION, ConfigConstants.CONFIG_KEY_AUTOSTASH, false)) { String message = MessageFormat.format( AUTOSTASH_MSG, Repository .shortenRefName(getHeadName(getHead()))); RevCommit stashCommit = Git.wrap(repo).stashCreate().setRef(null) .setWorkingDirectoryMessage( message) .call(); if (stashCommit != null) { FileUtils.mkdir(rebaseState.getDir()); rebaseState.createFile(AUTOSTASH, stashCommit.getName()); } } } private boolean autoStashApply() throws IOException, GitAPIException { boolean conflicts = false; if (rebaseState.getFile(AUTOSTASH).exists()) { String stash = rebaseState.readFile(AUTOSTASH); try (Git git = Git.wrap(repo)) { git.stashApply().setStashRef(stash) .ignoreRepositoryState(true).setStrategy(strategy) .call(); } catch (StashApplyFailureException e) { conflicts = true; try (RevWalk rw = new RevWalk(repo)) { ObjectId stashId = repo.resolve(stash); RevCommit commit = rw.parseCommit(stashId); updateStashRef(commit, commit.getAuthorIdent(), commit.getShortMessage()); } } } return conflicts; } private void updateStashRef(ObjectId commitId, PersonIdent refLogIdent, String refLogMessage) throws IOException { Ref currentRef = repo.exactRef(Constants.R_STASH); RefUpdate refUpdate = repo.updateRef(Constants.R_STASH); 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 RebaseResult processStep(RebaseTodoLine step, boolean shouldPick) throws IOException, GitAPIException { if (Action.COMMENT.equals(step.getAction())) return null; if (preserveMerges && shouldPick && (Action.EDIT.equals(step.getAction()) || Action.PICK .equals(step.getAction()))) { writeRewrittenHashes(); } ObjectReader or = repo.newObjectReader(); Collection ids = or.resolve(step.getCommit()); if (ids.size() != 1) throw new JGitInternalException( JGitText.get().cannotResolveUniquelyAbbrevObjectId); RevCommit commitToPick = walk.parseCommit(ids.iterator().next()); if (shouldPick) { if (monitor.isCancelled()) return RebaseResult.result(Status.STOPPED, commitToPick); RebaseResult result = cherryPickCommit(commitToPick); if (result != null) return result; } boolean isSquash = false; switch (step.getAction()) { case PICK: return null; // continue rebase process on pick command case REWORD: String oldMessage = commitToPick.getFullMessage(); CleanupMode mode = commitConfig.resolve(CleanupMode.DEFAULT, true); boolean[] doChangeId = { false }; String newMessage = editCommitMessage(doChangeId, oldMessage, mode, commitConfig.getCommentChar(oldMessage)); try (Git git = new Git(repo)) { newHead = git.commit() .setMessage(newMessage) .setAmend(true) .setNoVerify(true) .setInsertChangeId(doChangeId[0]) .call(); } return null; case EDIT: rebaseState.createFile(AMEND, commitToPick.name()); return stop(commitToPick, Status.EDIT); case COMMENT: break; case SQUASH: isSquash = true; //$FALL-THROUGH$ case FIXUP: resetSoftToParent(); List steps = repo.readRebaseTodo( rebaseState.getPath(GIT_REBASE_TODO), false); boolean isLast = steps.isEmpty(); if (!isLast) { switch (steps.get(0).getAction()) { case FIXUP: case SQUASH: break; default: isLast = true; break; } } File messageFixupFile = rebaseState.getFile(MESSAGE_FIXUP); File messageSquashFile = rebaseState.getFile(MESSAGE_SQUASH); if (isSquash && messageFixupFile.exists()) { messageFixupFile.delete(); } newHead = doSquashFixup(isSquash, commitToPick, isLast, messageFixupFile, messageSquashFile); } return null; } private String editCommitMessage(boolean[] doChangeId, String message, @NonNull CleanupMode mode, char commentChar) { String newMessage; CommitConfig.CleanupMode cleanup; if (interactiveHandler instanceof InteractiveHandler2) { InteractiveHandler2.ModifyResult modification = ((InteractiveHandler2) interactiveHandler) .editCommitMessage(message, mode, commentChar); newMessage = modification.getMessage(); cleanup = modification.getCleanupMode(); if (CleanupMode.DEFAULT.equals(cleanup)) { cleanup = mode; } doChangeId[0] = modification.shouldAddChangeId(); } else { newMessage = interactiveHandler.modifyCommitMessage(message); cleanup = CommitConfig.CleanupMode.STRIP; doChangeId[0] = false; } return CommitConfig.cleanText(newMessage, cleanup, commentChar); } private RebaseResult cherryPickCommit(RevCommit commitToPick) throws IOException, GitAPIException, NoMessageException, UnmergedPathsException, ConcurrentRefUpdateException, WrongRepositoryStateException, NoHeadException { try { monitor.beginTask(MessageFormat.format( JGitText.get().applyingCommit, commitToPick.getShortMessage()), ProgressMonitor.UNKNOWN); if (preserveMerges) { return cherryPickCommitPreservingMerges(commitToPick); } return cherryPickCommitFlattening(commitToPick); } finally { monitor.endTask(); } } private RebaseResult cherryPickCommitFlattening(RevCommit commitToPick) throws IOException, GitAPIException, NoMessageException, UnmergedPathsException, ConcurrentRefUpdateException, WrongRepositoryStateException, NoHeadException { // If the first parent of commitToPick is the current HEAD, // we do a fast-forward instead of cherry-pick to avoid // unnecessary object rewriting newHead = tryFastForward(commitToPick); lastStepWasForward = newHead != null; if (!lastStepWasForward) { // TODO if the content of this commit is already merged // here we should skip this step in order to avoid // confusing pseudo-changed String ourCommitName = getOurCommitName(); try (Git git = new Git(repo)) { CherryPickResult cherryPickResult = git.cherryPick() .include(commitToPick) .setOurCommitName(ourCommitName) .setReflogPrefix(REFLOG_PREFIX) .setStrategy(strategy) .setContentMergeStrategy(contentStrategy) .call(); switch (cherryPickResult.getStatus()) { case FAILED: if (operation == Operation.BEGIN) { return abort(RebaseResult .failed(cherryPickResult.getFailingPaths())); } return stop(commitToPick, Status.STOPPED); case CONFLICTING: return stop(commitToPick, Status.STOPPED); case OK: newHead = cherryPickResult.getNewHead(); } } } return null; } private RebaseResult cherryPickCommitPreservingMerges(RevCommit commitToPick) throws IOException, GitAPIException, NoMessageException, UnmergedPathsException, ConcurrentRefUpdateException, WrongRepositoryStateException, NoHeadException { writeCurrentCommit(commitToPick); List newParents = getNewParents(commitToPick); boolean otherParentsUnchanged = true; for (int i = 1; i < commitToPick.getParentCount(); i++) otherParentsUnchanged &= newParents.get(i).equals( commitToPick.getParent(i)); // If the first parent of commitToPick is the current HEAD, // we do a fast-forward instead of cherry-pick to avoid // unnecessary object rewriting newHead = otherParentsUnchanged ? tryFastForward(commitToPick) : null; lastStepWasForward = newHead != null; if (!lastStepWasForward) { ObjectId headId = getHead().getObjectId(); // getHead() checks for null assert headId != null; if (!AnyObjectId.isEqual(headId, newParents.get(0))) checkoutCommit(headId.getName(), newParents.get(0)); // Use the cherry-pick strategy if all non-first parents did not // change. This is different from C Git, which always uses the merge // strategy (see below). try (Git git = new Git(repo)) { if (otherParentsUnchanged) { boolean isMerge = commitToPick.getParentCount() > 1; String ourCommitName = getOurCommitName(); CherryPickCommand pickCommand = git.cherryPick() .include(commitToPick) .setOurCommitName(ourCommitName) .setReflogPrefix(REFLOG_PREFIX) .setStrategy(strategy) .setContentMergeStrategy(contentStrategy); if (isMerge) { pickCommand.setMainlineParentNumber(1); // We write a MERGE_HEAD and later commit explicitly pickCommand.setNoCommit(true); writeMergeInfo(commitToPick, newParents); } CherryPickResult cherryPickResult = pickCommand.call(); switch (cherryPickResult.getStatus()) { case FAILED: if (operation == Operation.BEGIN) { return abort(RebaseResult.failed( cherryPickResult.getFailingPaths())); } return stop(commitToPick, Status.STOPPED); case CONFLICTING: return stop(commitToPick, Status.STOPPED); case OK: if (isMerge) { // Commit the merge (setup above using // writeMergeInfo()) CommitCommand commit = git.commit(); commit.setAuthor(commitToPick.getAuthorIdent()); commit.setReflogComment(REFLOG_PREFIX + " " //$NON-NLS-1$ + commitToPick.getShortMessage()); newHead = commit.call(); } else newHead = cherryPickResult.getNewHead(); break; } } else { // Use the merge strategy to redo merges, which had some of // their non-first parents rewritten MergeCommand merge = git.merge() .setFastForward(MergeCommand.FastForwardMode.NO_FF) .setProgressMonitor(monitor) .setStrategy(strategy) .setContentMergeStrategy(contentStrategy) .setCommit(false); for (int i = 1; i < commitToPick.getParentCount(); i++) merge.include(newParents.get(i)); MergeResult mergeResult = merge.call(); if (mergeResult.getMergeStatus().isSuccessful()) { CommitCommand commit = git.commit(); commit.setAuthor(commitToPick.getAuthorIdent()); commit.setMessage(commitToPick.getFullMessage()); commit.setReflogComment(REFLOG_PREFIX + " " //$NON-NLS-1$ + commitToPick.getShortMessage()); newHead = commit.call(); } else { if (operation == Operation.BEGIN && mergeResult .getMergeStatus() == MergeResult.MergeStatus.FAILED) return abort(RebaseResult .failed(mergeResult.getFailingPaths())); return stop(commitToPick, Status.STOPPED); } } } } return null; } // Prepare MERGE_HEAD and message for the next commit private void writeMergeInfo(RevCommit commitToPick, List newParents) throws IOException { repo.writeMergeHeads(newParents.subList(1, newParents.size())); repo.writeMergeCommitMsg(commitToPick.getFullMessage()); } // Get the rewritten equivalents for the parents of the given commit private List getNewParents(RevCommit commitToPick) throws IOException { List newParents = new ArrayList<>(); for (int p = 0; p < commitToPick.getParentCount(); p++) { String parentHash = commitToPick.getParent(p).getName(); if (!new File(rebaseState.getRewrittenDir(), parentHash).exists()) newParents.add(commitToPick.getParent(p)); else { String newParent = RebaseState.readFile( rebaseState.getRewrittenDir(), parentHash); if (newParent.length() == 0) newParents.add(walk.parseCommit(repo .resolve(Constants.HEAD))); else newParents.add(walk.parseCommit(ObjectId .fromString(newParent))); } } return newParents; } private void writeCurrentCommit(RevCommit commit) throws IOException { RebaseState.appendToFile(rebaseState.getFile(CURRENT_COMMIT), commit.name()); } private void writeRewrittenHashes() throws RevisionSyntaxException, IOException, RefNotFoundException { File currentCommitFile = rebaseState.getFile(CURRENT_COMMIT); if (!currentCommitFile.exists()) return; ObjectId headId = getHead().getObjectId(); // getHead() checks for null assert headId != null; String head = headId.getName(); String currentCommits = rebaseState.readFile(CURRENT_COMMIT); for (String current : currentCommits.split("\n")) //$NON-NLS-1$ RebaseState .createFile(rebaseState.getRewrittenDir(), current, head); FileUtils.delete(currentCommitFile); } private RebaseResult finishRebase(RevCommit finalHead, boolean lastStepIsForward) throws IOException, GitAPIException { String headName = rebaseState.readFile(HEAD_NAME); updateHead(headName, finalHead, upstreamCommit); boolean stashConflicts = autoStashApply(); getRepository().autoGC(monitor); FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE); if (stashConflicts) return RebaseResult.STASH_APPLY_CONFLICTS_RESULT; if (lastStepIsForward || finalHead == null) return RebaseResult.FAST_FORWARD_RESULT; return RebaseResult.OK_RESULT; } private void checkSteps(List steps) throws InvalidRebaseStepException, IOException { if (steps.isEmpty()) return; if (RebaseTodoLine.Action.SQUASH.equals(steps.get(0).getAction()) || RebaseTodoLine.Action.FIXUP.equals(steps.get(0).getAction())) { if (!rebaseState.getFile(DONE).exists() || rebaseState.readFile(DONE).trim().length() == 0) { throw new InvalidRebaseStepException(MessageFormat.format( JGitText.get().cannotSquashFixupWithoutPreviousCommit, steps.get(0).getAction().name())); } } } private RevCommit doSquashFixup(boolean isSquash, RevCommit commitToPick, boolean isLast, File messageFixup, File messageSquash) throws IOException, GitAPIException { if (!messageSquash.exists()) { // init squash/fixup sequence ObjectId headId = repo.resolve(Constants.HEAD); RevCommit previousCommit = walk.parseCommit(headId); initializeSquashFixupFile(MESSAGE_SQUASH, previousCommit.getFullMessage()); if (!isSquash) { rebaseState.createFile(MESSAGE_FIXUP, previousCommit.getFullMessage()); } } String currSquashMessage = rebaseState.readFile(MESSAGE_SQUASH); int count = parseSquashFixupSequenceCount(currSquashMessage) + 1; String content = composeSquashMessage(isSquash, commitToPick, currSquashMessage, count); rebaseState.createFile(MESSAGE_SQUASH, content); return squashIntoPrevious(!messageFixup.exists(), isLast); } private void resetSoftToParent() throws IOException, GitAPIException, CheckoutConflictException { Ref ref = repo.exactRef(Constants.ORIG_HEAD); ObjectId orig_head = ref == null ? null : ref.getObjectId(); try (Git git = Git.wrap(repo)) { // we have already committed the cherry-picked commit. // what we need is to have changes introduced by this // commit to be on the index // resetting is a workaround git.reset().setMode(ResetType.SOFT) .setRef("HEAD~1").call(); //$NON-NLS-1$ } finally { // set ORIG_HEAD back to where we started because soft // reset moved it repo.writeOrigHead(orig_head); } } private RevCommit squashIntoPrevious(boolean sequenceContainsSquash, boolean isLast) throws IOException, GitAPIException { RevCommit retNewHead; String commitMessage; if (!isLast || sequenceContainsSquash) { commitMessage = rebaseState.readFile(MESSAGE_SQUASH); } else { commitMessage = rebaseState.readFile(MESSAGE_FIXUP); } try (Git git = new Git(repo)) { if (isLast) { boolean[] doChangeId = { false }; if (sequenceContainsSquash) { char commentChar = commitMessage.charAt(0); commitMessage = editCommitMessage(doChangeId, commitMessage, CleanupMode.STRIP, commentChar); } retNewHead = git.commit() .setMessage(commitMessage) .setAmend(true) .setNoVerify(true) .setInsertChangeId(doChangeId[0]) .call(); rebaseState.getFile(MESSAGE_SQUASH).delete(); rebaseState.getFile(MESSAGE_FIXUP).delete(); } else { // Next step is either Squash or Fixup retNewHead = git.commit().setMessage(commitMessage) .setAmend(true).setNoVerify(true).call(); } } return retNewHead; } @SuppressWarnings("nls") private String composeSquashMessage(boolean isSquash, RevCommit commitToPick, String currSquashMessage, int count) { StringBuilder sb = new StringBuilder(); String ordinal = getOrdinal(count); // currSquashMessage is always non-empty here, and the first character // is the comment character used so far. char commentChar = currSquashMessage.charAt(0); String newMessage = commitToPick.getFullMessage(); if (!isSquash) { sb.append(commentChar).append(" This is a combination of ") .append(count).append(" commits.\n"); // Add the previous message without header (i.e first line) sb.append(currSquashMessage .substring(currSquashMessage.indexOf('\n') + 1)); sb.append('\n'); sb.append(commentChar).append(" The ").append(count).append(ordinal) .append(" commit message will be skipped:\n") .append(commentChar).append(' '); sb.append(newMessage.replaceAll("([\n\r])", "$1" + commentChar + ' ')); } else { String currentMessage = currSquashMessage; if (commitConfig.isAutoCommentChar()) { // Figure out a new comment character taking into account the // new message String cleaned = CommitConfig.cleanText(currentMessage, CommitConfig.CleanupMode.STRIP, commentChar) + '\n' + newMessage; char newCommentChar = commitConfig.getCommentChar(cleaned); if (newCommentChar != commentChar) { currentMessage = replaceCommentChar(currentMessage, commentChar, newCommentChar); commentChar = newCommentChar; } } sb.append(commentChar).append(" This is a combination of ") .append(count).append(" commits.\n"); // Add the previous message without header (i.e first line) sb.append( currentMessage.substring(currentMessage.indexOf('\n') + 1)); sb.append('\n'); sb.append(commentChar).append(" This is the ").append(count) .append(ordinal).append(" commit message:\n"); sb.append(newMessage); } return sb.toString(); } private String replaceCommentChar(String message, char oldChar, char newChar) { // (?m) - Switch on multi-line matching; \h - horizontal whitespace return message.replaceAll("(?m)^(\\h*)" + oldChar, "$1" + newChar); //$NON-NLS-1$ //$NON-NLS-2$ } private static String getOrdinal(int count) { switch (count % 10) { case 1: return "st"; //$NON-NLS-1$ case 2: return "nd"; //$NON-NLS-1$ case 3: return "rd"; //$NON-NLS-1$ default: return "th"; //$NON-NLS-1$ } } /** * Parse the count from squashed commit messages * * @param currSquashMessage * the squashed commit message to be parsed * @return the count of squashed messages in the given string */ static int parseSquashFixupSequenceCount(String currSquashMessage) { String regex = "This is a combination of (.*) commits"; //$NON-NLS-1$ String firstLine = currSquashMessage.substring(0, currSquashMessage.indexOf('\n')); Pattern pattern = Pattern.compile(regex); Matcher matcher = pattern.matcher(firstLine); if (!matcher.find()) throw new IllegalArgumentException(); return Integer.parseInt(matcher.group(1)); } private void initializeSquashFixupFile(String messageFile, String fullMessage) throws IOException { char commentChar = commitConfig.getCommentChar(fullMessage); rebaseState.createFile(messageFile, commentChar + " This is a combination of 1 commits.\n" //$NON-NLS-1$ + commentChar + " The first commit's message is:\n" //$NON-NLS-1$ + fullMessage); } private String getOurCommitName() { // If onto is different from upstream, this should say "onto", but // RebaseCommand doesn't support a different "onto" at the moment. String ourCommitName = "Upstream, based on " //$NON-NLS-1$ + Repository.shortenRefName(upstreamCommitName); return ourCommitName; } private void updateHead(String headName, RevCommit aNewHead, RevCommit onto) throws IOException { // point the previous head (if any) to the new commit if (headName.startsWith(Constants.R_REFS)) { RefUpdate rup = repo.updateRef(headName); rup.setNewObjectId(aNewHead); rup.setRefLogMessage("rebase finished: " + headName + " onto " //$NON-NLS-1$ //$NON-NLS-2$ + onto.getName(), false); Result res = rup.forceUpdate(); switch (res) { case FAST_FORWARD: case FORCED: case NO_CHANGE: break; default: throw new JGitInternalException( JGitText.get().updatingHeadFailed); } rup = repo.updateRef(Constants.HEAD); rup.setRefLogMessage("rebase finished: returning to " + headName, //$NON-NLS-1$ false); res = rup.link(headName); switch (res) { case FAST_FORWARD: case FORCED: case NO_CHANGE: break; default: throw new JGitInternalException( JGitText.get().updatingHeadFailed); } } } private RevCommit checkoutCurrentHead() throws IOException, NoHeadException { ObjectId headTree = repo.resolve(Constants.HEAD + "^{tree}"); //$NON-NLS-1$ if (headTree == null) throw new NoHeadException( JGitText.get().cannotRebaseWithoutCurrentHead); DirCache dc = repo.lockDirCache(); try { DirCacheCheckout dco = new DirCacheCheckout(repo, dc, headTree); dco.setFailOnConflict(false); dco.setProgressMonitor(monitor); boolean needsDeleteFiles = dco.checkout(); if (needsDeleteFiles) { List fileList = dco.getToBeDeleted(); for (String filePath : fileList) { File fileToDelete = new File(repo.getWorkTree(), filePath); if (repo.getFS().exists(fileToDelete)) FileUtils.delete(fileToDelete, FileUtils.RECURSIVE | FileUtils.RETRY); } } } finally { dc.unlock(); } try (RevWalk rw = new RevWalk(repo)) { RevCommit commit = rw.parseCommit(repo.resolve(Constants.HEAD)); return commit; } } /** * @return the commit if we had to do a commit, otherwise null * @throws GitAPIException * @throws IOException */ private RevCommit continueRebase() throws GitAPIException, IOException { // if there are still conflicts, we throw a specific Exception DirCache dc = repo.readDirCache(); boolean hasUnmergedPaths = dc.hasUnmergedPaths(); if (hasUnmergedPaths) throw new UnmergedPathsException(); // determine whether we need to commit boolean needsCommit; try (TreeWalk treeWalk = new TreeWalk(repo)) { treeWalk.reset(); treeWalk.setRecursive(true); treeWalk.addTree(new DirCacheIterator(dc)); ObjectId id = repo.resolve(Constants.HEAD + "^{tree}"); //$NON-NLS-1$ if (id == null) throw new NoHeadException( JGitText.get().cannotRebaseWithoutCurrentHead); treeWalk.addTree(id); treeWalk.setFilter(TreeFilter.ANY_DIFF); needsCommit = treeWalk.next(); } if (needsCommit) { try (Git git = new Git(repo)) { CommitCommand commit = git.commit(); commit.setMessage(rebaseState.readFile(MESSAGE)); commit.setAuthor(parseAuthor()); return commit.call(); } } return null; } private PersonIdent parseAuthor() throws IOException { File authorScriptFile = rebaseState.getFile(AUTHOR_SCRIPT); byte[] raw; try { raw = IO.readFully(authorScriptFile); } catch (FileNotFoundException notFound) { if (authorScriptFile.exists()) { throw notFound; } return null; } return parseAuthor(raw); } private RebaseResult stop(RevCommit commitToPick, RebaseResult.Status status) throws IOException { PersonIdent author = commitToPick.getAuthorIdent(); String authorScript = toAuthorScript(author); rebaseState.createFile(AUTHOR_SCRIPT, authorScript); rebaseState.createFile(MESSAGE, commitToPick.getFullMessage()); ByteArrayOutputStream bos = new ByteArrayOutputStream(); try (DiffFormatter df = new DiffFormatter(bos)) { df.setRepository(repo); df.format(commitToPick.getParent(0), commitToPick); } rebaseState.createFile(PATCH, new String(bos.toByteArray(), UTF_8)); rebaseState.createFile(STOPPED_SHA, repo.newObjectReader() .abbreviate( commitToPick).name()); // Remove cherry pick state file created by CherryPickCommand, it's not // needed for rebase repo.writeCherryPickHead(null); return RebaseResult.result(status, commitToPick); } String toAuthorScript(PersonIdent author) { StringBuilder sb = new StringBuilder(100); sb.append(GIT_AUTHOR_NAME); sb.append("='"); //$NON-NLS-1$ sb.append(author.getName()); sb.append("'\n"); //$NON-NLS-1$ sb.append(GIT_AUTHOR_EMAIL); sb.append("='"); //$NON-NLS-1$ sb.append(author.getEmailAddress()); sb.append("'\n"); //$NON-NLS-1$ // the command line uses the "external String" // representation for date and timezone sb.append(GIT_AUTHOR_DATE); sb.append("='"); //$NON-NLS-1$ sb.append("@"); // @ for time in seconds since 1970 //$NON-NLS-1$ String externalString = author.toExternalString(); sb .append(externalString.substring(externalString .lastIndexOf('>') + 2)); sb.append("'\n"); //$NON-NLS-1$ return sb.toString(); } /** * Removes the number of lines given in the parameter from the * git-rebase-todo file but preserves comments and other lines * that can not be parsed as steps * * @param numSteps * @throws IOException */ private void popSteps(int numSteps) throws IOException { if (numSteps == 0) return; List todoLines = new LinkedList<>(); List poppedLines = new LinkedList<>(); for (RebaseTodoLine line : repo.readRebaseTodo( rebaseState.getPath(GIT_REBASE_TODO), true)) { if (poppedLines.size() >= numSteps || RebaseTodoLine.Action.COMMENT.equals(line.getAction())) todoLines.add(line); else poppedLines.add(line); } repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO), todoLines, false); if (!poppedLines.isEmpty()) { repo.writeRebaseTodoFile(rebaseState.getPath(DONE), poppedLines, true); } } private RebaseResult initFilesAndRewind() throws IOException, GitAPIException { // we need to store everything into files so that we can implement // --skip, --continue, and --abort Ref head = getHead(); ObjectId headId = head.getObjectId(); if (headId == null) { throw new RefNotFoundException(MessageFormat.format( JGitText.get().refNotResolved, Constants.HEAD)); } String headName = getHeadName(head); RevCommit headCommit = walk.lookupCommit(headId); RevCommit upstream = walk.lookupCommit(upstreamCommit.getId()); if (!isInteractive() && walk.isMergedInto(upstream, headCommit)) return RebaseResult.UP_TO_DATE_RESULT; else if (!isInteractive() && walk.isMergedInto(headCommit, upstream)) { // head is already merged into upstream, fast-foward monitor.beginTask(MessageFormat.format( JGitText.get().resettingHead, upstreamCommit.getShortMessage()), ProgressMonitor.UNKNOWN); checkoutCommit(headName, upstreamCommit); monitor.endTask(); updateHead(headName, upstreamCommit, upstream); return RebaseResult.FAST_FORWARD_RESULT; } monitor.beginTask(JGitText.get().obtainingCommitsForCherryPick, ProgressMonitor.UNKNOWN); // create the folder for the meta information FileUtils.mkdir(rebaseState.getDir(), true); repo.writeOrigHead(headId); rebaseState.createFile(REBASE_HEAD, headId.name()); rebaseState.createFile(REBASE_HEAD_LEGACY, headId.name()); rebaseState.createFile(HEAD_NAME, headName); rebaseState.createFile(ONTO, upstreamCommit.name()); rebaseState.createFile(ONTO_NAME, upstreamCommitName); if (isInteractive() || preserveMerges) { // --preserve-merges is an interactive mode for native git. Without // this, native git rebase --continue after a conflict would fall // into merge mode. rebaseState.createFile(INTERACTIVE, ""); //$NON-NLS-1$ } rebaseState.createFile(QUIET, ""); //$NON-NLS-1$ ArrayList toDoSteps = new ArrayList<>(); toDoSteps.add(new RebaseTodoLine("# Created by EGit: rebasing " + headId.name() //$NON-NLS-1$ + " onto " + upstreamCommit.name())); //$NON-NLS-1$ // determine the commits to be applied List cherryPickList = calculatePickList(headCommit); ObjectReader reader = walk.getObjectReader(); for (RevCommit commit : cherryPickList) toDoSteps.add(new RebaseTodoLine(Action.PICK, reader .abbreviate(commit), commit.getShortMessage())); repo.writeRebaseTodoFile(rebaseState.getPath(GIT_REBASE_TODO), toDoSteps, false); monitor.endTask(); // we rewind to the upstream commit monitor.beginTask(MessageFormat.format(JGitText.get().rewinding, upstreamCommit.getShortMessage()), ProgressMonitor.UNKNOWN); boolean checkoutOk = false; try { checkoutOk = checkoutCommit(headName, upstreamCommit); } finally { if (!checkoutOk) FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE); } monitor.endTask(); return null; } private List calculatePickList(RevCommit headCommit) throws IOException { List cherryPickList = new ArrayList<>(); try (RevWalk r = new RevWalk(repo)) { r.sort(RevSort.TOPO_KEEP_BRANCH_TOGETHER, true); r.sort(RevSort.COMMIT_TIME_DESC, true); r.markUninteresting(r.lookupCommit(upstreamCommit)); r.markStart(r.lookupCommit(headCommit)); Iterator commitsToUse = r.iterator(); while (commitsToUse.hasNext()) { RevCommit commit = commitsToUse.next(); if (preserveMerges || commit.getParentCount() == 1) { cherryPickList.add(commit); } } } Collections.reverse(cherryPickList); if (preserveMerges) { // When preserving merges we only rewrite commits which have at // least one parent that is itself rewritten (or a merge base) File rewrittenDir = rebaseState.getRewrittenDir(); FileUtils.mkdir(rewrittenDir, false); walk.reset(); walk.setRevFilter(RevFilter.MERGE_BASE); walk.markStart(upstreamCommit); walk.markStart(headCommit); RevCommit base; while ((base = walk.next()) != null) RebaseState.createFile(rewrittenDir, base.getName(), upstreamCommit.getName()); Iterator iterator = cherryPickList.iterator(); pickLoop: while(iterator.hasNext()){ RevCommit commit = iterator.next(); for (int i = 0; i < commit.getParentCount(); i++) { boolean parentRewritten = new File(rewrittenDir, commit .getParent(i).getName()).exists(); if (parentRewritten) { new File(rewrittenDir, commit.getName()).createNewFile(); continue pickLoop; } } // commit is only merged in, needs not be rewritten iterator.remove(); } } return cherryPickList; } private static String getHeadName(Ref head) { String headName; if (head.isSymbolic()) { headName = head.getTarget().getName(); } else { ObjectId headId = head.getObjectId(); // the callers are checking this already assert headId != null; headName = headId.getName(); } return headName; } private Ref getHead() throws IOException, RefNotFoundException { Ref head = repo.exactRef(Constants.HEAD); if (head == null || head.getObjectId() == null) throw new RefNotFoundException(MessageFormat.format( JGitText.get().refNotResolved, Constants.HEAD)); return head; } private boolean isInteractive() { return interactiveHandler != null; } /** * Check if we can fast-forward and returns the new head if it is possible * * @param newCommit * a {@link org.eclipse.jgit.revwalk.RevCommit} object to check * if we can fast-forward to. * @return the new head, or null * @throws java.io.IOException * @throws org.eclipse.jgit.api.errors.GitAPIException */ public RevCommit tryFastForward(RevCommit newCommit) throws IOException, GitAPIException { Ref head = getHead(); ObjectId headId = head.getObjectId(); if (headId == null) throw new RefNotFoundException(MessageFormat.format( JGitText.get().refNotResolved, Constants.HEAD)); RevCommit headCommit = walk.lookupCommit(headId); if (walk.isMergedInto(newCommit, headCommit)) return newCommit; String headName = getHeadName(head); return tryFastForward(headName, headCommit, newCommit); } private RevCommit tryFastForward(String headName, RevCommit oldCommit, RevCommit newCommit) throws IOException, GitAPIException { boolean tryRebase = false; for (RevCommit parentCommit : newCommit.getParents()) if (parentCommit.equals(oldCommit)) tryRebase = true; if (!tryRebase) return null; CheckoutCommand co = new CheckoutCommand(repo); try { co.setProgressMonitor(monitor); co.setName(newCommit.name()).call(); if (headName.startsWith(Constants.R_HEADS)) { RefUpdate rup = repo.updateRef(headName); rup.setExpectedOldObjectId(oldCommit); rup.setNewObjectId(newCommit); rup.setRefLogMessage("Fast-forward from " + oldCommit.name() //$NON-NLS-1$ + " to " + newCommit.name(), false); //$NON-NLS-1$ Result res = rup.update(walk); switch (res) { case FAST_FORWARD: case NO_CHANGE: case FORCED: break; default: throw new IOException("Could not fast-forward"); //$NON-NLS-1$ } } return newCommit; } catch (RefAlreadyExistsException | RefNotFoundException | InvalidRefNameException | CheckoutConflictException e) { throw new JGitInternalException(e.getMessage(), e); } } private void checkParameters() throws WrongRepositoryStateException { if (this.operation == Operation.PROCESS_STEPS) { if (rebaseState.getFile(DONE).exists()) throw new WrongRepositoryStateException(MessageFormat.format( JGitText.get().wrongRepositoryState, repo .getRepositoryState().name())); } if (this.operation != Operation.BEGIN) { // these operations are only possible while in a rebasing state switch (repo.getRepositoryState()) { case REBASING_INTERACTIVE: case REBASING: case REBASING_REBASING: case REBASING_MERGE: break; default: throw new WrongRepositoryStateException(MessageFormat.format( JGitText.get().wrongRepositoryState, repo .getRepositoryState().name())); } } else switch (repo.getRepositoryState()) { case SAFE: if (this.upstreamCommit == null) throw new JGitInternalException(MessageFormat .format(JGitText.get().missingRequiredParameter, "upstream")); //$NON-NLS-1$ return; default: throw new WrongRepositoryStateException(MessageFormat.format( JGitText.get().wrongRepositoryState, repo .getRepositoryState().name())); } } private RebaseResult abort(RebaseResult result) throws IOException, GitAPIException { ObjectId origHead = getOriginalHead(); try { String commitId = origHead != null ? origHead.name() : null; monitor.beginTask(MessageFormat.format( JGitText.get().abortingRebase, commitId), ProgressMonitor.UNKNOWN); DirCacheCheckout dco; if (commitId == null) throw new JGitInternalException( JGitText.get().abortingRebaseFailedNoOrigHead); ObjectId id = repo.resolve(commitId); RevCommit commit = walk.parseCommit(id); if (result.getStatus().equals(Status.FAILED)) { RevCommit head = walk.parseCommit(repo.resolve(Constants.HEAD)); dco = new DirCacheCheckout(repo, head.getTree(), repo.lockDirCache(), commit.getTree()); } else { dco = new DirCacheCheckout(repo, repo.lockDirCache(), commit.getTree()); } dco.setFailOnConflict(false); dco.checkout(); walk.close(); } finally { monitor.endTask(); } try { String headName = rebaseState.readFile(HEAD_NAME); monitor.beginTask(MessageFormat.format( JGitText.get().resettingHead, headName), ProgressMonitor.UNKNOWN); Result res = null; RefUpdate refUpdate = repo.updateRef(Constants.HEAD, false); refUpdate.setRefLogMessage("rebase: aborting", false); //$NON-NLS-1$ if (headName.startsWith(Constants.R_REFS)) { // update the HEAD res = refUpdate.link(headName); } else { refUpdate.setNewObjectId(origHead); res = refUpdate.forceUpdate(); } switch (res) { case FAST_FORWARD: case FORCED: case NO_CHANGE: break; default: throw new JGitInternalException( JGitText.get().abortingRebaseFailed); } boolean stashConflicts = autoStashApply(); // cleanup the files FileUtils.delete(rebaseState.getDir(), FileUtils.RECURSIVE); repo.writeCherryPickHead(null); repo.writeMergeHeads(null); if (stashConflicts) return RebaseResult.STASH_APPLY_CONFLICTS_RESULT; return result; } finally { monitor.endTask(); } } private ObjectId getOriginalHead() throws IOException { try { return ObjectId.fromString(rebaseState.readFile(REBASE_HEAD)); } catch (FileNotFoundException e) { try { return ObjectId .fromString(rebaseState.readFile(REBASE_HEAD_LEGACY)); } catch (FileNotFoundException ex) { return repo.readOrigHead(); } } } private boolean checkoutCommit(String headName, RevCommit commit) throws IOException, CheckoutConflictException { try { RevCommit head = walk.parseCommit(repo.resolve(Constants.HEAD)); DirCacheCheckout dco = new DirCacheCheckout(repo, head.getTree(), repo.lockDirCache(), commit.getTree()); dco.setFailOnConflict(true); dco.setProgressMonitor(monitor); try { dco.checkout(); } catch (org.eclipse.jgit.errors.CheckoutConflictException cce) { throw new CheckoutConflictException(dco.getConflicts(), cce); } // update the HEAD RefUpdate refUpdate = repo.updateRef(Constants.HEAD, true); refUpdate.setExpectedOldObjectId(head); refUpdate.setNewObjectId(commit); refUpdate.setRefLogMessage( "checkout: moving from " //$NON-NLS-1$ + Repository.shortenRefName(headName) + " to " + commit.getName(), false); //$NON-NLS-1$ Result res = refUpdate.forceUpdate(); switch (res) { case FAST_FORWARD: case NO_CHANGE: case FORCED: break; default: throw new IOException( JGitText.get().couldNotRewindToUpstreamCommit); } } finally { walk.close(); monitor.endTask(); } return true; } /** * Set upstream {@code RevCommit} * * @param upstream * the upstream commit * @return {@code this} */ public RebaseCommand setUpstream(RevCommit upstream) { this.upstreamCommit = upstream; this.upstreamCommitName = upstream.name(); return this; } /** * Set the upstream commit * * @param upstream * id of the upstream commit * @return {@code this} */ public RebaseCommand setUpstream(AnyObjectId upstream) { try { this.upstreamCommit = walk.parseCommit(upstream); this.upstreamCommitName = upstream.name(); } catch (IOException e) { throw new JGitInternalException(MessageFormat.format( JGitText.get().couldNotReadObjectWhileParsingCommit, upstream.name()), e); } return this; } /** * Set the upstream branch * * @param upstream * the name of the upstream branch * @return {@code this} * @throws org.eclipse.jgit.api.errors.RefNotFoundException */ public RebaseCommand setUpstream(String upstream) throws RefNotFoundException { try { ObjectId upstreamId = repo.resolve(upstream); if (upstreamId == null) throw new RefNotFoundException(MessageFormat.format(JGitText .get().refNotResolved, upstream)); upstreamCommit = walk.parseCommit(repo.resolve(upstream)); upstreamCommitName = upstream; return this; } catch (IOException ioe) { throw new JGitInternalException(ioe.getMessage(), ioe); } } /** * Optionally override the name of the upstream. If this is used, it has to * come after any {@link #setUpstream} call. * * @param upstreamName * the name which will be used to refer to upstream in conflicts * @return {@code this} */ public RebaseCommand setUpstreamName(String upstreamName) { if (upstreamCommit == null) { throw new IllegalStateException( "setUpstreamName must be called after setUpstream."); //$NON-NLS-1$ } this.upstreamCommitName = upstreamName; return this; } /** * Set the operation to execute during rebase * * @param operation * the operation to perform * @return {@code this} */ public RebaseCommand setOperation(Operation operation) { this.operation = operation; return this; } /** * Set progress monitor * * @param monitor * a progress monitor * @return this instance */ public RebaseCommand setProgressMonitor(ProgressMonitor monitor) { if (monitor == null) { monitor = NullProgressMonitor.INSTANCE; } this.monitor = monitor; return this; } /** * Enable interactive rebase *

* Does not stop after initialization of interactive rebase. This is * equivalent to * {@link org.eclipse.jgit.api.RebaseCommand#runInteractively(InteractiveHandler, boolean) * runInteractively(handler, false)}; *

* * @param handler * the * {@link org.eclipse.jgit.api.RebaseCommand.InteractiveHandler} * to use * @return this */ public RebaseCommand runInteractively(InteractiveHandler handler) { return runInteractively(handler, false); } /** * Enable interactive rebase *

* If stopAfterRebaseInteractiveInitialization is {@code true} the rebase * stops after initialization of interactive rebase returning * {@link org.eclipse.jgit.api.RebaseResult#INTERACTIVE_PREPARED_RESULT} *

* * @param handler * the * {@link org.eclipse.jgit.api.RebaseCommand.InteractiveHandler} * to use * @param stopAfterRebaseInteractiveInitialization * if {@code true} the rebase stops after initialization * @return this instance * @since 3.2 */ public RebaseCommand runInteractively(InteractiveHandler handler, final boolean stopAfterRebaseInteractiveInitialization) { this.stopAfterInitialization = stopAfterRebaseInteractiveInitialization; this.interactiveHandler = handler; return this; } /** * Set the MergeStrategy. * * @param strategy * The merge strategy to use during this rebase operation. * @return {@code this} * @since 3.4 */ public RebaseCommand setStrategy(MergeStrategy strategy) { this.strategy = strategy; return this; } /** * Sets the content merge strategy to use if the * {@link #setStrategy(MergeStrategy) merge strategy} is "resolve" or * "recursive". * * @param strategy * the {@link ContentMergeStrategy} to be used * @return {@code this} * @since 5.12 */ public RebaseCommand setContentMergeStrategy(ContentMergeStrategy strategy) { this.contentStrategy = strategy; return this; } /** * Whether to preserve merges during rebase * * @param preserve * {@code true} to re-create merges during rebase. Defaults to * {@code false}, a flattening rebase. * @return {@code this} * @since 3.5 */ public RebaseCommand setPreserveMerges(boolean preserve) { this.preserveMerges = preserve; return this; } /** * Allows to configure the interactive rebase process steps and to modify * commit messages. */ public interface InteractiveHandler { /** * Callback API to modify the initial list of interactive rebase steps. * * @param steps * initial configuration of interactive rebase */ void prepareSteps(List steps); /** * Used for editing commit message on REWORD or SQUASH. * * @param message * existing commit message * @return new commit message */ String modifyCommitMessage(String message); } /** * Extends {@link InteractiveHandler} with an enhanced callback for editing * commit messages. * * @since 6.1 */ public interface InteractiveHandler2 extends InteractiveHandler { /** * Callback API for editing a commit message on REWORD or SQUASH. *

* The callback gets the comment character currently set, and the * clean-up mode. It can use this information when presenting the * message to the user, and it also has the possibility to clean the * message itself (in which case the returned {@link ModifyResult} * should have {@link CleanupMode#VERBATIM} set lest JGit cleans the * message again). It can also override the initial clean-up mode by * returning clean-up mode other than {@link CleanupMode#DEFAULT}. If it * does return {@code DEFAULT}, the passed-in {@code mode} will be * applied. *

* * @param message * existing commit message * @param mode * {@link CleanupMode} currently set * @param commentChar * comment character used * @return a {@link ModifyResult} */ @NonNull ModifyResult editCommitMessage(@NonNull String message, @NonNull CleanupMode mode, char commentChar); @Override default String modifyCommitMessage(String message) { // Should actually not be called; but do something reasonable anyway ModifyResult result = editCommitMessage( message == null ? "" : message, CleanupMode.STRIP, //$NON-NLS-1$ '#'); return result.getMessage(); } /** * Describes the result of editing a commit message: the new message, * and how it should be cleaned. */ interface ModifyResult { /** * Retrieves the new commit message. * * @return the message */ @NonNull String getMessage(); /** * Tells how the message returned by {@link #getMessage()} should be * cleaned. * * @return the {@link CleanupMode} */ @NonNull CleanupMode getCleanupMode(); /** * Tells whether a Gerrit Change-Id should be computed and added to * the commit message, as with * {@link CommitCommand#setInsertChangeId(boolean)}. * * @return {@code true} if a Change-Id should be handled, * {@code false} otherwise */ boolean shouldAddChangeId(); } } PersonIdent parseAuthor(byte[] raw) { if (raw.length == 0) return null; Map keyValueMap = new HashMap<>(); for (int p = 0; p < raw.length;) { int end = RawParseUtils.nextLF(raw, p); if (end == p) break; int equalsIndex = RawParseUtils.next(raw, p, '='); if (equalsIndex == end) break; String key = RawParseUtils.decode(raw, p, equalsIndex - 1); String value = RawParseUtils.decode(raw, equalsIndex + 1, end - 2); p = end; keyValueMap.put(key, value); } String name = keyValueMap.get(GIT_AUTHOR_NAME); String email = keyValueMap.get(GIT_AUTHOR_EMAIL); String time = keyValueMap.get(GIT_AUTHOR_DATE); // the time is saved as int timeStart = 0; if (time.startsWith("@")) //$NON-NLS-1$ timeStart = 1; else timeStart = 0; long when = Long .parseLong(time.substring(timeStart, time.indexOf(' '))) * 1000; String tzOffsetString = time.substring(time.indexOf(' ') + 1); int multiplier = -1; if (tzOffsetString.charAt(0) == '+') multiplier = 1; int hours = Integer.parseInt(tzOffsetString.substring(1, 3)); int minutes = Integer.parseInt(tzOffsetString.substring(3, 5)); // this is in format (+/-)HHMM (hours and minutes) // we need to convert into minutes int tz = (hours * 60 + minutes) * multiplier; if (name != null && email != null) return new PersonIdent(name, email, when, tz); return null; } private static class RebaseState { private final File repoDirectory; private File dir; public RebaseState(File repoDirectory) { this.repoDirectory = repoDirectory; } public File getDir() { if (dir == null) { File rebaseApply = new File(repoDirectory, REBASE_APPLY); if (rebaseApply.exists()) { dir = rebaseApply; } else { File rebaseMerge = new File(repoDirectory, REBASE_MERGE); dir = rebaseMerge; } } return dir; } /** * @return Directory with rewritten commit hashes, usually exists if * {@link RebaseCommand#preserveMerges} is true **/ public File getRewrittenDir() { return new File(getDir(), REWRITTEN); } public String readFile(String name) throws IOException { try { return readFile(getDir(), name); } catch (FileNotFoundException e) { if (ONTO_NAME.equals(name)) { // Older JGit mistakenly wrote a file "onto-name" instead of // "onto_name". Try that wrong name just in case somebody // upgraded while a rebase started by JGit was in progress. File oldFile = getFile(ONTO_NAME.replace('_', '-')); if (oldFile.exists()) { return readFile(oldFile); } } throw e; } } public void createFile(String name, String content) throws IOException { createFile(getDir(), name, content); } public File getFile(String name) { return new File(getDir(), name); } public String getPath(String name) { return (getDir().getName() + "/" + name); //$NON-NLS-1$ } private static String readFile(File file) throws IOException { byte[] content = IO.readFully(file); // strip off the last LF int end = RawParseUtils.prevLF(content, content.length); return RawParseUtils.decode(content, 0, end + 1); } private static String readFile(File directory, String fileName) throws IOException { return readFile(new File(directory, fileName)); } private static void createFile(File parentDir, String name, String content) throws IOException { File file = new File(parentDir, name); try (FileOutputStream fos = new FileOutputStream(file)) { fos.write(content.getBytes(UTF_8)); fos.write('\n'); } } private static void appendToFile(File file, String content) throws IOException { try (FileOutputStream fos = new FileOutputStream(file, true)) { fos.write(content.getBytes(UTF_8)); fos.write('\n'); } } } }