# HG changeset patch # Parent cf5097d8403d0d95729e2122047a0a99f6240ef1 # User Ondrej Vrabec #244660 - Support for combining commits api for an interactive rebase. diff -r cf5097d8403d -r 4e51687a157b libs.git/apichanges.xml --- a/libs.git/apichanges.xml Wed Jul 09 15:41:59 2014 +0200 +++ b/libs.git/apichanges.xml Thu Jul 10 10:26:45 2014 +0200 @@ -112,6 +112,26 @@ New method for updating a reference (branch) to a new commit id. + + + + + +

New method GitClient.rebaseInteractively allows clients to run + rebase in an interactive mode and reorder picked commits or + change the way the are applied before the rebase starts. +
+ The command takes as parameter an implementation of GitRebaseInteractiveHandler + allowing clients to setup the rebase steps before it starts. +

+
+ + + +
+ + + New method for updating a reference (branch) to a new commit id. diff -r cf5097d8403d -r 4e51687a157b libs.git/manifest.mf --- a/libs.git/manifest.mf Wed Jul 09 15:41:59 2014 +0200 +++ b/libs.git/manifest.mf Thu Jul 10 10:26:45 2014 +0200 @@ -1,4 +1,4 @@ Manifest-Version: 1.0 OpenIDE-Module: org.netbeans.libs.git/1 OpenIDE-Module-Localizing-Bundle: org/netbeans/libs/git/Bundle.properties -OpenIDE-Module-Specification-Version: 1.27 +OpenIDE-Module-Specification-Version: 1.28 diff -r cf5097d8403d -r 4e51687a157b libs.git/src/org/netbeans/libs/git/GitClassFactoryImpl.java --- a/libs.git/src/org/netbeans/libs/git/GitClassFactoryImpl.java Wed Jul 09 15:41:59 2014 +0200 +++ b/libs.git/src/org/netbeans/libs/git/GitClassFactoryImpl.java Thu Jul 10 10:26:45 2014 +0200 @@ -50,6 +50,7 @@ import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.RebaseTodoLine; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevObject; @@ -70,6 +71,7 @@ */ final class GitClassFactoryImpl extends GitClassFactory { private static GitClassFactoryImpl instance; + private Object GitRebaseInteractiveHandler; static synchronized GitClassFactory getInstance () { if (instance == null) { @@ -200,4 +202,9 @@ return new GitCherryPickResult(status, conflicts, failures, head, cherryPickedCommits); } + @Override + public GitRebaseInteractiveHandler.GitRebaseStep createRebaseStep (RebaseTodoLine step, GitRevisionInfo commit) { + return new GitRebaseInteractiveHandler.GitRebaseStep(step, commit); + } + } diff -r cf5097d8403d -r 4e51687a157b libs.git/src/org/netbeans/libs/git/GitClient.java --- a/libs.git/src/org/netbeans/libs/git/GitClient.java Wed Jul 09 15:41:59 2014 +0200 +++ b/libs.git/src/org/netbeans/libs/git/GitClient.java Thu Jul 10 10:26:45 2014 +0200 @@ -1018,8 +1018,30 @@ * @since 1.8 */ public GitRebaseResult rebase (RebaseOperationType operation, String revision, ProgressMonitor monitor) throws GitException { + return rebaseInteractively(operation, revision, null, monitor); + } + + /** + * Rewrites recent commits and updates the history the same way as + * interactive rebase. Allows users to specify operations with the commits. + * Before the command starts caller is asked to set actions and reorder + * commits overall via the given {@code handler}. + * + * @param operation kind of rebase operation you want to perform + * @param revision id of a destination commit. Considered only + * when operation is set + * to RebaseOperationType.BEGIN otherwise it's meaningless. + * @param handler called before the command starts to reorder and setup actions + * for the picked commits. + * @param monitor progress monitor + * @return result of the rebase + * @throws GitException an unexpected error occurs + * @since 1.28 + */ + public GitRebaseResult rebaseInteractively (RebaseOperationType operation, String revision, + GitRebaseInteractiveHandler handler, ProgressMonitor monitor) throws GitException { Repository repository = gitRepository.getRepository(); - RebaseCommand cmd = new RebaseCommand(repository, getClassFactory(), revision, operation, monitor); + RebaseCommand cmd = new RebaseCommand(repository, getClassFactory(), revision, operation, handler, monitor); cmd.execute(); return cmd.getResult(); } diff -r cf5097d8403d -r 4e51687a157b libs.git/src/org/netbeans/libs/git/GitRebaseInteractiveHandler.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libs.git/src/org/netbeans/libs/git/GitRebaseInteractiveHandler.java Thu Jul 10 10:26:45 2014 +0200 @@ -0,0 +1,168 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2014 Oracle and/or its affiliates. All rights reserved. + * + * Oracle and Java are registered trademarks of Oracle and/or its affiliates. + * Other names may be trademarks of their respective owners. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2014 Sun Microsystems, Inc. + */ +package org.netbeans.libs.git; + +import java.util.List; +import org.eclipse.jgit.errors.IllegalTodoFileModification; +import org.eclipse.jgit.lib.RebaseTodoLine; + +/** + * An interface used by an interactive rebase to reorder picked commits and set + * their appropriate actions. Before the rebase starts + * {@link #prepareSteps(java.util.List)} is called with a list of picked commits + * and their default actions. An implementor is allowed to reorder the list, set + * the items' actions as in the interactive rebase on CLI. + * + * When needed the rebase command also calls {@link #updateCommitMessage(java.lang.String) + * } + * to let user specify the exact new commit message for commits with actions set + * to {@link Action#REWORD} or {@link Action#SQUASH}. + * + * @author Ondrej Vrabec + * @since 1.28 + */ +public interface GitRebaseInteractiveHandler { + + /** + * Action selected for a rebased commit. + */ + public static enum Action { + + /** + * Use commit as it is. + */ + PICK, + /** + * Use commit, but edit the commit message. + */ + REWORD, + /** + * Use commit, but stop for amending. + */ + EDIT, + /** + * Use commit, but meld into previous commit. + */ + SQUASH, + /** + * Like "squash", but discard this commit's log message. + */ + FIXUP; + } + + /** + * Describes a single rebase step. May be slightly updated by user by + * changing its action. + */ + public static final class GitRebaseStep { + + private Action action; + private final GitRevisionInfo commit; + private final RebaseTodoLine delegate; + + GitRebaseStep (RebaseTodoLine step, GitRevisionInfo commit) { + this.delegate = step; + this.commit = commit; + this.action = Action.valueOf(step.getAction().name()); + } + + /** + * Selected action for the commit. + * + * @return rebase step action + */ + public Action getAction () { + return action; + } + + /** + * Changes the step's action. + * + * @param action new action + * @throws IllegalArgumentException if the action is not allowed or + * null. + */ + public void setAction (Action action) throws IllegalArgumentException { + if (action == null) { + throw new IllegalArgumentException("Action cannot be null"); + } + try { + this.delegate.setAction(RebaseTodoLine.Action.valueOf(action.name())); + } catch (IllegalTodoFileModification ex) { + throw new IllegalArgumentException(ex); + } + this.action = action; + } + + /** + * The original commit to be rebased. + * @return the to-rebase commit. + */ + public GitRevisionInfo getCommit () { + return commit; + } + + } + + /** + * Implement to reorder the picked steps or set different actions for them. + * + * @param steps list of picked steps for the rebase. + */ + public void prepareSteps (List steps); + + /** + * Returns the updated commit message for the currently rebased commit. + * + * Note that removing some steps from the list will mean the + * relevant commit will not be rebased. + * + * @param currentStep the last rebased commit that requires the commit + * message rewrite. + * @param originalCommitMessage original commit message constructed by + * rebase. + * @return user tuned commit message that will be used as the final commit's + * message. + */ + public String updateCommitMessage (GitRebaseStep currentStep, String originalCommitMessage); + +} diff -r cf5097d8403d -r 4e51687a157b libs.git/src/org/netbeans/libs/git/GitRebaseResult.java --- a/libs.git/src/org/netbeans/libs/git/GitRebaseResult.java Wed Jul 09 15:41:59 2014 +0200 +++ b/libs.git/src/org/netbeans/libs/git/GitRebaseResult.java Thu Jul 10 10:26:45 2014 +0200 @@ -84,6 +84,17 @@ } }, /** + * Stopped for amend during an interactive rebase. Commit (with amend) if + * required and continue the rebase again when ready. + * @since 1.28 + */ + EDIT { + @Override + public boolean isSuccessful () { + return false; + } + }, + /** * Stopped due to a conflict. Must be either aborted, resolved or the * commit must be skipped from the rebase. */ @@ -210,8 +221,6 @@ static RebaseStatus parseRebaseStatus (RebaseResult.Status rebaseStatus) { switch (rebaseStatus) { - case EDIT: - return RebaseStatus.STOPPED; case UNCOMMITTED_CHANGES: return RebaseStatus.FAILED; case INTERACTIVE_PREPARED: diff -r cf5097d8403d -r 4e51687a157b libs.git/src/org/netbeans/libs/git/jgit/GitClassFactory.java --- a/libs.git/src/org/netbeans/libs/git/jgit/GitClassFactory.java Wed Jul 09 15:41:59 2014 +0200 +++ b/libs.git/src/org/netbeans/libs/git/jgit/GitClassFactory.java Thu Jul 10 10:26:45 2014 +0200 @@ -51,6 +51,7 @@ import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.lib.RebaseTodoLine; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevObject; @@ -68,6 +69,7 @@ import org.netbeans.libs.git.GitMergeResult; import org.netbeans.libs.git.GitPullResult; import org.netbeans.libs.git.GitPushResult; +import org.netbeans.libs.git.GitRebaseInteractiveHandler; import org.netbeans.libs.git.GitRebaseResult; import org.netbeans.libs.git.GitRemoteConfig; import org.netbeans.libs.git.GitRevertResult; @@ -138,4 +140,6 @@ public abstract void setBranchTracking (GitBranch branch, GitBranch trackedBranch); + public abstract GitRebaseInteractiveHandler.GitRebaseStep createRebaseStep (RebaseTodoLine step, GitRevisionInfo commit); + } diff -r cf5097d8403d -r 4e51687a157b libs.git/src/org/netbeans/libs/git/jgit/commands/RebaseCommand.java --- a/libs.git/src/org/netbeans/libs/git/jgit/commands/RebaseCommand.java Wed Jul 09 15:41:59 2014 +0200 +++ b/libs.git/src/org/netbeans/libs/git/jgit/commands/RebaseCommand.java Thu Jul 10 10:26:45 2014 +0200 @@ -44,22 +44,31 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.RebaseCommand.InteractiveHandler; import org.eclipse.jgit.api.RebaseCommand.Operation; import org.eclipse.jgit.api.RebaseResult; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.lib.AbbreviatedObjectId; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.RebaseTodoLine; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.merge.ResolveMerger; import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; import org.netbeans.libs.git.GitClient; import org.netbeans.libs.git.GitException; +import org.netbeans.libs.git.GitRebaseInteractiveHandler; import org.netbeans.libs.git.GitRebaseResult; import org.netbeans.libs.git.GitRevisionInfo; import org.netbeans.libs.git.GitStatus; @@ -80,13 +89,24 @@ private GitRebaseResult result; private final ProgressMonitor monitor; private final GitClient.RebaseOperationType operation; + private final GitRebaseInteractiveHandler handler; + private static final Logger LOG = Logger.getLogger(RebaseCommand.class.getName()); + private ObjectReader or; + private RevWalk revWalk; + private GitException internalPrepException; + + private static final String REBASE_MERGE = "rebase-merge"; //NOI18N + private static final String REBASE_APPLY = "rebase-apply"; //NOI18N + private static final String DONE = "done"; //NOI18N public RebaseCommand (Repository repository, GitClassFactory gitFactory, String revision, - GitClient.RebaseOperationType operation, ProgressMonitor monitor) { + GitClient.RebaseOperationType operation, GitRebaseInteractiveHandler handler, + ProgressMonitor monitor) { super(repository, gitFactory, monitor); this.revision = revision; this.operation = operation; this.monitor = monitor; + this.handler = handler; } @Override @@ -110,8 +130,17 @@ } command.setOperation(getOperation(operation)); command.setProgressMonitor(new DelegatingProgressMonitor(monitor)); + if (handler == null && operation != GitClient.RebaseOperationType.BEGIN) { + // run interactively in case the scheduled rebase is already interactive + command.runInteractively(new DefaultHandler()); + } else if (handler != null) { + command.runInteractively(createDelegate(handler)); + } try { RebaseResult res = command.call(); + if (internalPrepException != null) { + throw internalPrepException; + } result = createResult(res); } catch (GitAPIException ex) { throw new GitException(ex); @@ -211,4 +240,125 @@ } return Collections.unmodifiableList(files); } + + private GitRevisionInfo findCommit (RebaseTodoLine line) throws GitException { + try { + Repository repository = getRepository(); + AbbreviatedObjectId aid = line.getCommit(); + if (or == null) { + or = repository.newObjectReader(); + revWalk = new RevWalk(or); + } + Collection ids = or.resolve(aid); + RevCommit commit = null; + for (ObjectId oid : ids) { + try { + commit = Utils.findCommit(repository, oid, revWalk); + } catch (GitException ex) { + LOG.log(Level.INFO, null, ex); + } + } + if (commit == null) { + throw new GitException("No commit found for abbreviated id " + aid.name()); + } else { + return getClassFactory().createRevisionInfo(commit, repository); + } + } catch (IOException ex) { + throw new GitException(ex); + } + } + + private org.eclipse.jgit.api.RebaseCommand.InteractiveHandler createDelegate (final GitRebaseInteractiveHandler handler) { + + return new org.eclipse.jgit.api.RebaseCommand.InteractiveHandler() { + + private File dir; + + @Override + public void prepareSteps (List lines) { + if (operation != GitClient.RebaseOperationType.BEGIN) { + // preparing steps once is enough + return; + } + try { + // map to public API steps + List delegatingSteps = new ArrayList<>(lines.size()); + Map> stepsMap = new HashMap<>(lines.size()); + List commentSteps = new ArrayList<>(); + for (RebaseTodoLine line : lines) { + commentSteps.add(line); + if (line.getAction() != RebaseTodoLine.Action.COMMENT) { + GitRebaseInteractiveHandler.GitRebaseStep step = getClassFactory().createRebaseStep(line, findCommit(line)); + stepsMap.put(step, commentSteps); + delegatingSteps.add(step); + commentSteps = new ArrayList<>(); + } + } + + // ask user to reorganize commits + handler.prepareSteps(delegatingSteps); + + // map back to JGit steps + lines.clear(); + for (GitRebaseInteractiveHandler.GitRebaseStep step : delegatingSteps) { + List partialLines = stepsMap.get(step); + if (partialLines != null) { + lines.addAll(partialLines); + } + } + lines.addAll(commentSteps); + } catch (GitException ex) { + internalPrepException = ex; + lines.clear(); + } + } + + @Override + public String modifyCommitMessage (String originalCommitMessage) { + return handler.updateCommitMessage(pickLastRebasedCommit(), originalCommitMessage); + } + + private GitRebaseInteractiveHandler.GitRebaseStep pickLastRebasedCommit () { + Repository repo = getRepository(); + if (dir == null) { + File rebaseApply = new File(repo.getDirectory(), REBASE_APPLY); + if (rebaseApply.exists()) { + dir = rebaseApply; + } else { + File rebaseMerge = new File(repo.getDirectory(), REBASE_MERGE); + dir = rebaseMerge; + } + } + File doneFile = new File(dir, DONE); + if (doneFile.exists()) { + String path = dir.getName() + File.separator + DONE; + try { + List doneSteps = repo.readRebaseTodo(path, false); + if (!doneSteps.isEmpty()) { + RebaseTodoLine lastStep = doneSteps.get(doneSteps.size() - 1); + return getClassFactory().createRebaseStep(lastStep, findCommit(lastStep)); + } + } catch (IOException | GitException ex) { + LOG.log(Level.INFO, null, ex); + } + } + return null; + } + + }; + } + + private static class DefaultHandler implements InteractiveHandler { + + @Override + public void prepareSteps (List steps) { + // no op + } + + @Override + public String modifyCommitMessage (String commit) { + return commit; + } + + } } diff -r cf5097d8403d -r 4e51687a157b libs.git/test/unit/src/org/netbeans/libs/git/GitEnumsStateTest.java --- a/libs.git/test/unit/src/org/netbeans/libs/git/GitEnumsStateTest.java Wed Jul 09 15:41:59 2014 +0200 +++ b/libs.git/test/unit/src/org/netbeans/libs/git/GitEnumsStateTest.java Thu Jul 10 10:26:45 2014 +0200 @@ -46,6 +46,7 @@ import org.eclipse.jgit.api.CherryPickResult; import org.eclipse.jgit.api.MergeResult; import org.eclipse.jgit.api.RebaseResult; +import org.eclipse.jgit.lib.RebaseTodoLine; import org.eclipse.jgit.lib.RefUpdate; import org.eclipse.jgit.lib.RepositoryState; import org.eclipse.jgit.submodule.SubmoduleStatusType; @@ -103,4 +104,12 @@ assertNotNull(GitCherryPickResult.CherryPickStatus.valueOf(status.name())); } } + + public void testRebaseStepsAction () { + for (RebaseTodoLine.Action action : RebaseTodoLine.Action.values()) { + if (action != RebaseTodoLine.Action.COMMENT) { + assertNotNull(GitRebaseInteractiveHandler.Action.valueOf(action.name())); + } + } + } } diff -r cf5097d8403d -r 4e51687a157b libs.git/test/unit/src/org/netbeans/libs/git/jgit/commands/RebaseTest.java --- a/libs.git/test/unit/src/org/netbeans/libs/git/jgit/commands/RebaseTest.java Wed Jul 09 15:41:59 2014 +0200 +++ b/libs.git/test/unit/src/org/netbeans/libs/git/jgit/commands/RebaseTest.java Thu Jul 10 10:26:45 2014 +0200 @@ -45,6 +45,8 @@ import java.io.File; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Map; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; @@ -52,9 +54,11 @@ import static junit.framework.Assert.assertTrue; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryState; import org.netbeans.libs.git.GitBranch; import org.netbeans.libs.git.GitClient; import org.netbeans.libs.git.GitClient.RebaseOperationType; +import org.netbeans.libs.git.GitRebaseInteractiveHandler; import org.netbeans.libs.git.GitRebaseResult; import org.netbeans.libs.git.GitRebaseResult.RebaseStatus; import org.netbeans.libs.git.GitRevisionInfo; @@ -381,4 +385,352 @@ // resets HEAD assertEquals(branchInfo.getRevision(), getRepository(client).resolve(Constants.HEAD).name()); } + + public void testRebaseInteractiveNoChange () throws Exception { + File f = new File(workDir, "file"); + write(f, "init"); + add(f); + commit(f); + + GitClient client = getClient(workDir); + String head = getRepository(client).resolve(Constants.HEAD).getName(); + GitBranch branch = client.createBranch(BRANCH_NAME, Constants.MASTER, NULL_PROGRESS_MONITOR); + client.checkoutRevision(BRANCH_NAME, true, NULL_PROGRESS_MONITOR); + + GitRebaseInteractiveHandler handler = new GitRebaseInteractiveHandler() { + + @Override + public void prepareSteps (List list) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public String updateCommitMessage (GitRebaseStep currentStep, String originalCommitMessage) { + throw new UnsupportedOperationException("Not supported yet."); + } + }; + + // rebase branch to master + GitRebaseResult result = client.rebaseInteractively(RebaseOperationType.BEGIN, Constants.MASTER, handler, NULL_PROGRESS_MONITOR); + assertEquals(RebaseStatus.OK, result.getRebaseStatus()); + assertEquals(head, result.getCurrentHead()); + + // do a commit and + write(f, "change"); + add(f); + GitRevisionInfo commit = client.commit(new File[] { f }, "change on branch", null, null, NULL_PROGRESS_MONITOR); + + // rebase branch to master, they are now different, handler passes the same results, no rebase should take action + handler = new GitRebaseInteractiveHandler() { + + @Override + public void prepareSteps (List list) { + assertEquals(1, list.size()); + assertEquals(Action.PICK, list.get(0).getAction()); + } + + @Override + public String updateCommitMessage (GitRebaseStep currentStep, String originalCommitMessage) { + throw new UnsupportedOperationException("Not supported yet."); + } + }; + result = client.rebaseInteractively(RebaseOperationType.BEGIN, Constants.MASTER, handler, NULL_PROGRESS_MONITOR); + // maybe up_to_date would be better + assertEquals(RebaseStatus.FAST_FORWARD, result.getRebaseStatus()); + // branch stays the same + assertEquals(commit.getRevision(), result.getCurrentHead()); + // master stays the same as well, right + assertEquals(0, client.log(Constants.MASTER, NULL_PROGRESS_MONITOR).getParents().length); + } + + public void testRebaseInteractiveEditLastCommit () throws Exception { + File f = new File(workDir, "file"); + write(f, "init"); + add(f); + commit(f); + + GitClient client = getClient(workDir); + String head = getRepository(client).resolve(Constants.HEAD).getName(); + GitBranch branch = client.createBranch(BRANCH_NAME, Constants.MASTER, NULL_PROGRESS_MONITOR); + client.checkoutRevision(BRANCH_NAME, true, NULL_PROGRESS_MONITOR); + + // do a commit and + write(f, "change"); + add(f); + GitRevisionInfo commit = client.commit(new File[] { f }, "change on branch", null, null, NULL_PROGRESS_MONITOR); + + final String rebasedCommitId = commit.getRevision(); + GitRebaseInteractiveHandler handler = new GitRebaseInteractiveHandler() { + + @Override + public void prepareSteps (List list) { + assertEquals(1, list.size()); + list.get(0).setAction(Action.REWORD); + } + + @Override + public String updateCommitMessage (GitRebaseStep currentStep, String originalCommitMessage) { + assertEquals(rebasedCommitId, currentStep.getCommit().getRevision()); + assertEquals(Action.REWORD, currentStep.getAction()); + assertEquals("change on branch", originalCommitMessage); + return "change on branch reworded"; + } + }; + + Thread.sleep(1100); + + GitRebaseResult result = client.rebaseInteractively(RebaseOperationType.BEGIN, Constants.MASTER, handler, NULL_PROGRESS_MONITOR); + assertEquals(RebaseStatus.FAST_FORWARD, result.getRebaseStatus()); + // master stays the same as well, right + assertEquals(0, client.log(Constants.MASTER, NULL_PROGRESS_MONITOR).getParents().length); + // branch does not stay the same + assertEquals("change on branch", commit.getFullMessage()); + GitRevisionInfo branchHead = client.log(BRANCH_NAME, NULL_PROGRESS_MONITOR); + assertEquals(branchHead.getRevision(), result.getCurrentHead()); + assertEquals("change on branch reworded", branchHead.getFullMessage()); + // but parents are the same + assertEquals(commit.getParents()[0], branchHead.getParents()[0]); + assertEquals(commit.getCommitTime(), branchHead.getCommitTime()); + + // try edit + commit = branchHead; + handler = new GitRebaseInteractiveHandler() { + + @Override + public void prepareSteps (List list) { + assertEquals(1, list.size()); + list.get(0).setAction(Action.EDIT); + } + + @Override + public String updateCommitMessage (GitRebaseStep currentStep, String originalCommitMessage) { + throw new UnsupportedOperationException("Not supported yet."); + } + }; + + Thread.sleep(1100); + result = client.rebaseInteractively(RebaseOperationType.BEGIN, Constants.MASTER, handler, NULL_PROGRESS_MONITOR); + assertEquals(RebaseStatus.EDIT, result.getRebaseStatus()); + assertEquals(commit.getRevision(), result.getCurrentHead()); + assertEquals(RepositoryState.REBASING_INTERACTIVE, getRepository(client).getRepositoryState()); + + // master stays the same as well, right + assertEquals(0, client.log(Constants.MASTER, NULL_PROGRESS_MONITOR).getParents().length); + // branch stays the same + branchHead = client.log(BRANCH_NAME, NULL_PROGRESS_MONITOR); + assertEquals(commit.getRevision(), branchHead.getRevision()); + assertEquals(branchHead.getRevision(), result.getCurrentHead()); + assertEquals("change on branch reworded", branchHead.getFullMessage()); + + // now amend the last commit + branchHead = client.commit(new File[0], "change on branch edit", null, null, true, NULL_PROGRESS_MONITOR); + assertEquals("change on branch edit", branchHead.getFullMessage()); + assertEquals(RepositoryState.REBASING_INTERACTIVE, getRepository(client).getRepositoryState()); + + // finish interactive rebase + commit = branchHead; + client.rebaseInteractively(RebaseOperationType.CONTINUE, null, handler, NULL_PROGRESS_MONITOR); + assertEquals(RepositoryState.SAFE, getRepository(client).getRepositoryState()); + branchHead = client.log(BRANCH_NAME, NULL_PROGRESS_MONITOR); + assertEquals(commit.getRevision(), branchHead.getRevision()); + } + + public void testRebaseInteractiveSquash () throws Exception { + File f = new File(workDir, "file"); + File f2 = new File(workDir, "file2"); + write(f, "init"); + add(f); + commit(f); + + GitClient client = getClient(workDir); + write(f2, Constants.MASTER); + add(f2); + GitRevisionInfo master = client.commit(new File[] { f2 }, "change on master", null, null, NULL_PROGRESS_MONITOR); + client.createBranch(BRANCH_NAME, "master^1", NULL_PROGRESS_MONITOR); + + client.checkoutRevision(BRANCH_NAME, true, NULL_PROGRESS_MONITOR); + assertFalse(f2.exists()); + write(f, BRANCH_NAME); + add(f); + final GitRevisionInfo info = client.commit(new File[] { f }, "change 1 on branch", null, null, NULL_PROGRESS_MONITOR); + write(f, "another change"); + add(f); + final GitRevisionInfo info2 = client.commit(new File[] { f }, "change 2 on branch", null, null, NULL_PROGRESS_MONITOR); + + // sleep to change time + Thread.sleep(1100); + + GitRebaseInteractiveHandler handler = new GitRebaseInteractiveHandler() { + + @Override + public void prepareSteps (List list) { + assertEquals(2, list.size()); + list.get(1).setAction(Action.SQUASH); + } + + @Override + public String updateCommitMessage (GitRebaseStep currentStep, String originalCommitMessage) { + assertEquals(info2.getRevision(), currentStep.getCommit().getRevision()); + assertEquals(Action.SQUASH, currentStep.getAction()); + assertEquals("# This is a combination of 2 commits.\n" + + "# The first commit's message is:\n" + + info.getFullMessage() + + "\n# This is the 2nd commit message:\n" + + info2.getFullMessage(), originalCommitMessage); + return "change 1+2 on branch"; + } + }; + + GitRebaseResult result = client.rebaseInteractively(RebaseOperationType.BEGIN, Constants.MASTER, handler, NULL_PROGRESS_MONITOR); + assertEquals(RebaseStatus.OK, result.getRebaseStatus()); + assertEquals("another change", read(f)); + assertEquals(Constants.MASTER, read(f2)); + + SearchCriteria sc = new SearchCriteria(); + sc.setRevisionTo(BRANCH_NAME); + GitRevisionInfo[] log = client.log(sc, NULL_PROGRESS_MONITOR); + // branch commits were squashed + assertEquals(3, log.length); + assertEquals(master.getRevision(), log[1].getRevision()); + assertEquals(log[0].getRevision(), result.getCurrentHead()); + assertEquals("change 1+2 on branch", log[0].getFullMessage()); + } + + public void testRebaseInteractiveFixup () throws Exception { + File f = new File(workDir, "file"); + File f2 = new File(workDir, "file2"); + write(f, "init"); + add(f); + commit(f); + + GitClient client = getClient(workDir); + write(f2, Constants.MASTER); + add(f2); + GitRevisionInfo master = client.commit(new File[] { f2 }, "change on master", null, null, NULL_PROGRESS_MONITOR); + client.createBranch(BRANCH_NAME, "master^1", NULL_PROGRESS_MONITOR); + + client.checkoutRevision(BRANCH_NAME, true, NULL_PROGRESS_MONITOR); + assertFalse(f2.exists()); + write(f, BRANCH_NAME); + add(f); + final GitRevisionInfo info = client.commit(new File[] { f }, "change 1 on branch", null, null, NULL_PROGRESS_MONITOR); + write(f, "another change"); + add(f); + final GitRevisionInfo info2 = client.commit(new File[] { f }, "change 2 on branch", null, null, NULL_PROGRESS_MONITOR); + + // sleep to change time + Thread.sleep(1100); + + GitRebaseInteractiveHandler handler = new GitRebaseInteractiveHandler() { + + @Override + public void prepareSteps (List list) { + assertEquals(2, list.size()); + list.get(1).setAction(Action.FIXUP); + } + + @Override + public String updateCommitMessage (GitRebaseStep currentStep, String originalCommitMessage) { + throw new UnsupportedOperationException("Not supported yet."); + } + }; + + GitRebaseResult result = client.rebaseInteractively(RebaseOperationType.BEGIN, Constants.MASTER, handler, NULL_PROGRESS_MONITOR); + assertEquals(RebaseStatus.OK, result.getRebaseStatus()); + assertEquals("another change", read(f)); + assertEquals(Constants.MASTER, read(f2)); + + SearchCriteria sc = new SearchCriteria(); + sc.setRevisionTo(BRANCH_NAME); + GitRevisionInfo[] log = client.log(sc, NULL_PROGRESS_MONITOR); + // branch commits were squashed + assertEquals(3, log.length); + assertEquals(master.getRevision(), log[1].getRevision()); + assertEquals(log[0].getRevision(), result.getCurrentHead()); + assertEquals(info.getFullMessage(), log[0].getFullMessage()); + } + + public void testRebaseInteractiveReorder () throws Exception { + File f = new File(workDir, "file"); + File f2 = new File(workDir, "file2"); + write(f, "init"); + add(f); + commit(f); + + GitClient client = getClient(workDir); + write(f2, Constants.MASTER); + add(f2); + GitRevisionInfo master = client.commit(new File[] { f2 }, "change on master", null, null, NULL_PROGRESS_MONITOR); + client.createBranch(BRANCH_NAME, "master^1", NULL_PROGRESS_MONITOR); + + client.checkoutRevision(BRANCH_NAME, true, NULL_PROGRESS_MONITOR); + assertFalse(f2.exists()); + write(f, BRANCH_NAME); + add(f); + final GitRevisionInfo info = client.commit(new File[] { f }, "change 1 on branch", null, null, NULL_PROGRESS_MONITOR); + write(f, "another change"); + add(f); + final GitRevisionInfo info2 = client.commit(new File[] { f }, "change 2 on branch", null, null, NULL_PROGRESS_MONITOR); + + // sleep to change time + Thread.sleep(1100); + + GitRebaseInteractiveHandler handler = new GitRebaseInteractiveHandler() { + + @Override + public void prepareSteps (List list) { + assertEquals(2, list.size()); + Collections.reverse(list); + } + + @Override + public String updateCommitMessage (GitRebaseStep currentStep, String originalCommitMessage) { + throw new UnsupportedOperationException("Not supported yet."); + } + }; + + // rebase and apply commits in reverse order + GitRebaseResult result = client.rebaseInteractively(RebaseOperationType.BEGIN, Constants.MASTER, handler, NULL_PROGRESS_MONITOR); + assertEquals(RebaseStatus.STOPPED, result.getRebaseStatus()); + assertEquals(Arrays.asList(f), result.getConflicts()); + assertEquals("<<<<<<< Upstream, based on master\ninit\n=======\nanother change\n>>>>>>> " + + info2.getRevision().substring(0, 7) + " " + info2.getShortMessage(), read(f)); + assertEquals(Constants.MASTER, read(f2)); + assertEquals(RepositoryState.REBASING_INTERACTIVE, repo.getRepositoryState()); + + // resolve conflicts + write(f, "another change"); + add(f); + + // continue rebase, should commit info2 but stop on another merge conflict + result = client.rebaseInteractively(RebaseOperationType.CONTINUE, Constants.MASTER, handler, NULL_PROGRESS_MONITOR); + assertEquals(RebaseStatus.STOPPED, result.getRebaseStatus()); + assertEquals(Arrays.asList(f), result.getConflicts()); + assertEquals("<<<<<<< Upstream, based on master\nanother change\n=======\n" + BRANCH_NAME + "\n>>>>>>> " + + info.getRevision().substring(0, 7) + " " + info.getShortMessage(), read(f)); + assertEquals(RepositoryState.REBASING_INTERACTIVE, repo.getRepositoryState()); + + // check that info2 was committed + SearchCriteria sc = new SearchCriteria(); + sc.setRevisionTo("HEAD"); + GitRevisionInfo[] log = client.log(sc, NULL_PROGRESS_MONITOR); + assertEquals(3, log.length); + assertEquals(master.getRevision(), log[1].getRevision()); + assertEquals(log[0].getRevision(), result.getCurrentHead()); + assertEquals(info2.getFullMessage(), log[0].getFullMessage()); + + write(f, BRANCH_NAME + "\nanother change"); + add(f); + + result = client.rebaseInteractively(RebaseOperationType.CONTINUE, Constants.MASTER, handler, NULL_PROGRESS_MONITOR); + assertEquals(RebaseStatus.OK, result.getRebaseStatus()); + assertEquals(RepositoryState.SAFE, repo.getRepositoryState()); + sc.setRevisionTo(BRANCH_NAME); + log = client.log(sc, NULL_PROGRESS_MONITOR); + assertEquals(4, log.length); + assertEquals(master.getRevision(), log[2].getRevision()); + assertEquals(log[0].getRevision(), result.getCurrentHead()); + assertEquals(info.getFullMessage(), log[0].getFullMessage()); + assertEquals(info2.getFullMessage(), log[1].getFullMessage()); + } }