--- a/libs.git/apichanges.xml Mon Jun 30 12:40:30 2014 +0200 +++ a/libs.git/apichanges.xml Tue Jul 01 13:16:35 2014 +0200 @@ -112,6 +112,29 @@ New method for updating a reference (branch) to a new commit id. + + + + + +
    +
  • New method GitClient.cherryPick used to cherry-pick commits and apply them in the + current branch. The command may interrupt its progress and require some user actions + (such as resolve conflicts or commit) and may be continued with different kinds + of operation types passed as its arguments.
  • +
  • Introduciong new repository state: CHERRY_PICKING and CHERRY_PICKING_RESOLVED + marking the states of the repository when a cherry-picking is not finished or + it requires resolving conflicts.
  • +
+
+ + + + +
+ + + New method for updating a reference (branch) to a new commit id. --- a/libs.git/manifest.mf Mon Jun 30 12:40:30 2014 +0200 +++ a/libs.git/manifest.mf Tue Jul 01 13:16:35 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.26 +OpenIDE-Module-Specification-Version: 1.27 --- a/libs.git/src/org/netbeans/libs/git/Bundle.properties Mon Jun 30 12:40:30 2014 +0200 +++ a/libs.git/src/org/netbeans/libs/git/Bundle.properties Tue Jul 01 13:16:35 2014 +0200 @@ -53,6 +53,8 @@ LBL_RepositoryInfo_Rebasing = Rebasing LBL_RepositoryInfo_Apply = Apply LBL_RepositoryInfo_Bisecting = Bisecting +LBL_RepositoryInfo_CherryPicking=Cherry-picking +LBL_RepositoryInfo_CherryPickingResolved=Cherry-picked #GitException MSG_Exception_ObjectDoesNotExist = {0} [{1}] does not exist --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ d68b118f158f Tue Jul 01 13:16:35 2014 +0200 @@ -0,0 +1,150 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2011 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 2011 Sun Microsystems, Inc. + */ +package org.netbeans.libs.git; + +import java.io.File; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +/** + * Returned by a git cherry-pick command, represents its result. + * + * @author Ondra Vrabec + * @since 1.27 + */ +public final class GitCherryPickResult { + + private final CherryPickStatus status; + private final List conflicts; + private final List failures; + private final GitRevisionInfo currentHead; + private final List cherryPickedCommits; + + /** + * The status rebase resulted in. + */ + public enum CherryPickStatus { + /** + * Command successfully finished. No action is required. + */ + OK, + /** + * Command was aborted and reset to the original state. No action is + * required. + */ + ABORTED, + /** + * Failed because a dirty working tree prevents from starting the command. + * Local modifications preventing from applying commit changes must be + * reverted. + */ + FAILED, + /** + * The cherry-picking stopped in a state where it requires a manual commit. + * E.g. after resolving conflicts client is required to commit the changes + * before continuing with rebase. + */ + UNCOMMITTED, + /** + * Conflicts when merging the cherry-picked commits. + * Conflicts must be resolved and the command must be continued/aborted. + */ + CONFLICTING; + } + + GitCherryPickResult (CherryPickStatus status, List conflicts, List failures, + GitRevisionInfo currentHead, List cherryPickedCommits) { + this.status = status; + this.currentHead = currentHead; + this.conflicts = conflicts; + this.failures = failures; + this.cherryPickedCommits = cherryPickedCommits; + } + + /** + * @return result of the cherry-pick command. + */ + public CherryPickStatus getCherryPickStatus () { + return status; + } + + /** + * @return current HEAD commit after the cherry-pick command. + */ + public GitRevisionInfo getCurrentHead () { + return currentHead; + } + + /** + * If the cherry-pick started but was unable to finish because of unresolved + * conflicts then the method returns a collection of such files in conflict. + * To complete the command you need to resolve the conflicts and continue the + * unfinished command. + * + * @return files in conflict + */ + public Collection getConflicts () { + return Collections.unmodifiableList(conflicts); + } + + /** + * When the command fails because of local modifications then this method + * returns a collections of files causing the failure. + * + * @return files that cause the cherry-pick to fail. + */ + public Collection getFailures () { + return Collections.unmodifiableList(failures); + } + + /** + * Returns commits cherry-picked to the current branch by the last run of the + * cherry-pick command. + * + * @return array of commits cherry-picked to head. + */ + public GitRevisionInfo[] getCherryPickedCommits () { + return cherryPickedCommits.toArray(new GitRevisionInfo[cherryPickedCommits.size()]); + } + +} --- a/libs.git/src/org/netbeans/libs/git/GitClassFactoryImpl.java Mon Jun 30 12:40:30 2014 +0200 +++ a/libs.git/src/org/netbeans/libs/git/GitClassFactoryImpl.java Tue Jul 01 13:16:35 2014 +0200 @@ -193,4 +193,11 @@ return new GitSubmoduleStatus(status, folder); } + @Override + public GitCherryPickResult createCherryPickResult (GitCherryPickResult.CherryPickStatus status, + List conflicts, List failures, GitRevisionInfo head, + List cherryPickedCommits) { + return new GitCherryPickResult(status, conflicts, failures, head, cherryPickedCommits); + } + } --- a/libs.git/src/org/netbeans/libs/git/GitClient.java Mon Jun 30 12:40:30 2014 +0200 +++ a/libs.git/src/org/netbeans/libs/git/GitClient.java Tue Jul 01 13:16:35 2014 +0200 @@ -63,6 +63,7 @@ import org.netbeans.libs.git.jgit.commands.CatCommand; import org.netbeans.libs.git.jgit.commands.CheckoutIndexCommand; import org.netbeans.libs.git.jgit.commands.CheckoutRevisionCommand; +import org.netbeans.libs.git.jgit.commands.CherryPickCommand; import org.netbeans.libs.git.jgit.commands.CleanCommand; import org.netbeans.libs.git.jgit.commands.CommitCommand; import org.netbeans.libs.git.jgit.commands.CompareCommand; @@ -254,6 +255,52 @@ }; } + /** + * Used as a parameter of {@link #cherryPick(org.netbeans.libs.git.GitClient.CherryPickOperation, java.lang.String[], org.netbeans.libs.git.progress.ProgressMonitor) to set the behavior of the command. + * @since 1.27 + */ + public enum CherryPickOperation { + + /** + * A fresh cherry-pick command will be started. + */ + BEGIN, + /** + * Continues an interrupted cherry-pick command after conflicts are resolved. + */ + CONTINUE { + + @Override + public String toString () { + return "--continue"; //NOI18N + } + + }, + /** + * Tries to finish cherry-picking the current commit but stops in + * cherry-picking other scheduled commits. + */ + QUIT { + + @Override + public String toString () { + return "--quit"; //NOI18N + } + + }, + /** + * Aborts and resets an interrupted cherry-pick command. + */ + ABORT { + + @Override + public String toString () { + return "--abort"; //NOI18N + } + + }; + } + private final JGitRepository gitRepository; private final Set listeners; private JGitCredentialsProvider credentialsProvider; @@ -371,6 +418,26 @@ CheckoutRevisionCommand cmd = new CheckoutRevisionCommand(repository, getClassFactory(), revision, failOnConflict, monitor, delegateListener); cmd.execute(); } + + /** + * Cherry-picks (transplants) selected revisions (commits) onto the current + * HEAD. + * + * @param operation kind of cherry-pick operation you want to perform + * @param revisions commits you want to cherry-pick. Makes sense only when + * operation is set to CherryPickOperation.BEGIN + * otherwise it's meaningless. + * @param monitor progress monitor + * @return result of the command + * @throws GitException an unexpected error occurs + * @since 1.27 + */ + public GitCherryPickResult cherryPick (CherryPickOperation operation, String[] revisions, ProgressMonitor monitor) throws GitException { + Repository repository = gitRepository.getRepository(); + CherryPickCommand cmd = new CherryPickCommand(repository, getClassFactory(), revisions, operation, monitor, delegateListener); + cmd.execute(); + return cmd.getResult(); + } /** * Cleans the working tree by recursively removing files that are not under --- a/libs.git/src/org/netbeans/libs/git/GitRepositoryState.java Mon Jun 30 12:40:30 2014 +0200 +++ a/libs.git/src/org/netbeans/libs/git/GitRepositoryState.java Tue Jul 01 13:16:35 2014 +0200 @@ -78,7 +78,8 @@ public String toString () { return Utils.getBundle(GitRepositoryState.class).getString("LBL_RepositoryInfo_Safe"); } //NOI18N }, - /** An unfinished merge or cherry-picking. Must resolve or reset before continuing normally + /** + * An unfinished merge. Must resolve or reset before continuing normally */ MERGING { @Override @@ -107,6 +108,37 @@ }, /** + * An unfinished cherry-pick. Must resolve or reset before continuing normally + * @since 1.27 + */ + CHERRY_PICKING { + @Override + public boolean canCheckout () { return false; } + @Override + public boolean canResetHead () { return true; } + @Override + public boolean canCommit () { return false; } + @Override + public String toString () { return Utils.getBundle(GitRepositoryState.class).getString("LBL_RepositoryInfo_CherryPicking"); } //NOI18N + }, + + /** + * A cherry-picked commit where all conflicts have been resolved. The index does not + * contain any unmerged paths and the repository requires a commit. + * @since 1.27 + */ + CHERRY_PICKING_RESOLVED { + @Override + public boolean canCheckout () { return true; } + @Override + public boolean canResetHead () { return true; } + @Override + public boolean canCommit () { return true; } + @Override + public String toString () { return Utils.getBundle(GitRepositoryState.class).getString("LBL_RepositoryInfo_CherryPickingResolved"); } //NOI18N + }, + + /** * An unfinished rebase or am. Must resolve, skip or abort before normal work can take place */ REBASING { @@ -178,11 +210,13 @@ case BISECTING: return GitRepositoryState.BISECTING; case MERGING: - case CHERRY_PICKING: case REVERTING: return GitRepositoryState.MERGING; + case CHERRY_PICKING: + return GitRepositoryState.CHERRY_PICKING; + case CHERRY_PICKING_RESOLVED: + return GitRepositoryState.CHERRY_PICKING_RESOLVED; case MERGING_RESOLVED: - case CHERRY_PICKING_RESOLVED: case REVERTING_RESOLVED: return GitRepositoryState.MERGING_RESOLVED; case REBASING: --- a/libs.git/src/org/netbeans/libs/git/jgit/GitClassFactory.java Mon Jun 30 12:40:30 2014 +0200 +++ a/libs.git/src/org/netbeans/libs/git/jgit/GitClassFactory.java Tue Jul 01 13:16:35 2014 +0200 @@ -62,6 +62,7 @@ import org.eclipse.jgit.transport.URIish; import org.netbeans.libs.git.GitBlameResult; import org.netbeans.libs.git.GitBranch; +import org.netbeans.libs.git.GitCherryPickResult; import org.netbeans.libs.git.GitConflictDescriptor; import org.netbeans.libs.git.GitConflictDescriptor.Type; import org.netbeans.libs.git.GitMergeResult; @@ -89,6 +90,10 @@ public abstract GitBranch createBranch (String name, boolean remote, boolean active, ObjectId id); + public abstract GitCherryPickResult createCherryPickResult ( + GitCherryPickResult.CherryPickStatus status, List conflicts, + List failures, GitRevisionInfo head, List cherryPickedCommits); + public abstract GitConflictDescriptor createConflictDescriptor (Type type); public abstract GitFileInfo createFileInfo (File file, String oldPath, GitFileInfo.Status status, File originalFile, String originalPath); --- a/libs.git/src/org/netbeans/libs/git/jgit/Utils.java Mon Jun 30 12:40:30 2014 +0200 +++ a/libs.git/src/org/netbeans/libs/git/jgit/Utils.java Tue Jul 01 13:16:35 2014 +0200 @@ -263,16 +263,20 @@ } public static RevCommit findCommit (Repository repository, String revision, RevWalk walk) throws GitException.MissingObjectException, GitException { + ObjectId commitId = parseObjectId(repository, revision); + if (commitId == null) { + throw new GitException.MissingObjectException(revision, GitObjectType.COMMIT); + } + return findCommit(repository, commitId, walk); + } + + public static RevCommit findCommit (Repository repository, ObjectId commitId, RevWalk walk) throws GitException.MissingObjectException, GitException { try { - ObjectId commitId = parseObjectId(repository, revision); - if (commitId == null) { - throw new GitException.MissingObjectException(revision, GitObjectType.COMMIT); - } return (walk == null ? new RevWalk(repository) : walk).parseCommit(commitId); } catch (MissingObjectException ex) { - throw new GitException.MissingObjectException(revision, GitObjectType.COMMIT, ex); + throw new GitException.MissingObjectException(commitId.name(), GitObjectType.COMMIT, ex); } catch (IncorrectObjectTypeException ex) { - throw new GitException(MessageFormat.format(Utils.getBundle(Utils.class).getString("MSG_Exception_IdNotACommit"), revision)); //NOI18N + throw new GitException(MessageFormat.format(Utils.getBundle(Utils.class).getString("MSG_Exception_IdNotACommit"), commitId.name())); //NOI18N } catch (IOException ex) { throw new GitException(ex); } --- a/libs.git/src/org/netbeans/libs/git/jgit/commands/Bundle.properties Mon Jun 30 12:40:30 2014 +0200 +++ a/libs.git/src/org/netbeans/libs/git/jgit/commands/Bundle.properties Tue Jul 01 13:16:35 2014 +0200 @@ -54,6 +54,7 @@ MSG_Error_RepositoryExists = Git repository already exists at {0}: {1} MSG_Error_CannotCatRoot = Cannot cat root: {0} MSG_Error_Commit_ConflictsInIndex = Index contains files in conflict, please resolve them before commit +MSG_Error_Commit_CannotAmend=Cannot amend a commit in this repository state. MSG_Error_Commit_PartialCommitAfterMerge = Cannot do a partial commit during a merge. MSG_Error_Commit_NotAllowedInCurrentState = Cannot commit in current state: {0} MSG_Error_UpdateTracking_InvalidReference=Invalid reference: {0} --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ d68b118f158f Tue Jul 01 13:16:35 2014 +0200 @@ -0,0 +1,426 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2010 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 2010 Sun Microsystems, Inc. + */ +package org.netbeans.libs.git.jgit.commands; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.eclipse.jgit.api.CherryPickResult; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.RebaseCommand.Operation; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.JGitInternalException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.RebaseTodoFile; +import org.eclipse.jgit.lib.RebaseTodoLine; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryState; +import org.eclipse.jgit.merge.ResolveMerger; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.util.IO; +import org.eclipse.jgit.util.io.SafeBufferedOutputStream; +import org.netbeans.libs.git.GitCherryPickResult; +import org.netbeans.libs.git.GitClient; +import org.netbeans.libs.git.GitException; +import org.netbeans.libs.git.GitRevisionInfo; +import org.netbeans.libs.git.GitStatus; +import org.netbeans.libs.git.jgit.DelegatingGitProgressMonitor; +import org.netbeans.libs.git.jgit.GitClassFactory; +import org.netbeans.libs.git.jgit.Utils; +import org.netbeans.libs.git.progress.FileListener; +import org.netbeans.libs.git.progress.ProgressMonitor; +import org.netbeans.libs.git.progress.StatusListener; + +/** + * + * @author ondra + */ +public class CherryPickCommand extends GitCommand { + + private final String[] revisions; + private GitCherryPickResult result; + private final ProgressMonitor monitor; + private final GitClient.CherryPickOperation operation; + private final FileListener listener; + private static final String SEQUENCER = "sequencer"; + private static final String SEQUENCER_HEAD = "head"; + private static final String SEQUENCER_TODO = "todo"; + + public CherryPickCommand (Repository repository, GitClassFactory gitFactory, String[] revisions, + GitClient.CherryPickOperation operation, ProgressMonitor monitor, FileListener listener) { + super(repository, gitFactory, monitor); + this.revisions = revisions; + this.operation = operation; + this.monitor = monitor; + this.listener = listener; + } + + @Override + protected void run () throws GitException { + Repository repository = getRepository(); + ObjectId originalCommit = getOriginalCommit(); + ObjectId head = getHead(); + List steps; + try { + switch (operation) { + case BEGIN: + // initialize sequencer and cherry-pick steps if there are + // more commits to cherry-pick + steps = prepareCommand(head); + // apply the selected steps + applySteps(steps, false); + break; + case ABORT: + // delete the sequencer and reset to the original head + if (repository.getRepositoryState() == RepositoryState.CHERRY_PICKING + || repository.getRepositoryState() == RepositoryState.CHERRY_PICKING_RESOLVED) { + if (originalCommit == null) { + // maybe the sequencer is not created in that case simply reset to HEAD + originalCommit = head; + } + } + Utils.deleteRecursively(getSequencerFolder()); + if (originalCommit != null) { + ResetCommand reset = new ResetCommand(repository, getClassFactory(), + originalCommit.name(), GitClient.ResetType.HARD, new DelegatingGitProgressMonitor(monitor), listener); + reset.execute(); + } + result = createCustomResult(GitCherryPickResult.CherryPickStatus.ABORTED); + break; + case QUIT: + // used to reset the sequencer only + Utils.deleteRecursively(getSequencerFolder()); + switch (repository.getRepositoryState()) { + case CHERRY_PICKING: + // unresolved conflicts + result = createResult(CherryPickResult.CONFLICT); + break; + case CHERRY_PICKING_RESOLVED: + result = createCustomResult(GitCherryPickResult.CherryPickStatus.UNCOMMITTED); + break; + default: + result = createCustomResult(GitCherryPickResult.CherryPickStatus.OK); + break; + } + break; + case CONTINUE: + switch (repository.getRepositoryState()) { + case CHERRY_PICKING: + // unresolved conflicts, cannot continue + result = createResult(CherryPickResult.CONFLICT); + break; + case CHERRY_PICKING_RESOLVED: + // cannot continue without manual commit + result = createCustomResult(GitCherryPickResult.CherryPickStatus.UNCOMMITTED); + break; + default: + // read steps from sequencer and apply them + // if sequencer is empty this will be a noop + steps = readTodoFile(repository); + applySteps(steps, true); + break; + } + break; + default: + throw new IllegalStateException("Unexpected operation " + operation.name()); + } + } catch (GitAPIException | IOException ex) { + throw new GitException(ex); + } + } + + @Override + protected String getCommandDescription () { + StringBuilder sb = new StringBuilder(); + sb.append("git cherry-pick "); //NOI18N + if (operation == GitClient.CherryPickOperation.BEGIN) { + for (String rev : revisions) { + sb.append(rev).append(" "); //NOI18N + } + } else { + sb.append(operation.toString()); + } + return sb.toString(); + } + + public GitCherryPickResult getResult () { + return result; + } + + static Operation getOperation (GitClient.RebaseOperationType operation) { + return Operation.valueOf(operation.name()); + } + + private void applySteps (List steps, boolean skipFirstStep) throws GitAPIException, IOException { + Repository repository = getRepository(); + ObjectReader or = repository.newObjectReader(); + CherryPickResult res = null; + boolean skipped = false; + List cherryPickedRefs = new ArrayList<>(); + for (Iterator it = steps.iterator(); it.hasNext(); ) { + RebaseTodoLine step = it.next(); + if (step.getAction() == RebaseTodoLine.Action.PICK) { + if (skipFirstStep && !skipped) { + it.remove(); + skipped = true; + continue; + } + Collection ids = or.resolve(step.getCommit()); + if (ids.size() != 1) { + throw new JGitInternalException("Could not resolve uniquely the abbreviated object ID"); + } + org.eclipse.jgit.api.CherryPickCommand command = new Git(repository).cherryPick(); + command.include(ids.iterator().next()); + res = command.call(); + if (res.getStatus() == CherryPickResult.CherryPickStatus.OK) { + it.remove(); + writeTodoFile(repository, steps); + cherryPickedRefs.addAll(res.getCherryPickedRefs()); + } else { + break; + } + } else { + it.remove(); + } + } + if (res == null) { + result = createCustomResult(GitCherryPickResult.CherryPickStatus.OK, cherryPickedRefs); + } else { + result = createResult(res, cherryPickedRefs); + } + if (steps.isEmpty()) { + // sequencer no longer needed + Utils.deleteRecursively(getSequencerFolder()); + } + } + + private GitCherryPickResult createResult (CherryPickResult res) { + return createResult(res, Collections.emptyList()); + } + + private GitCherryPickResult createResult (CherryPickResult res, List cherryPickedRefs) { + GitRevisionInfo currHead = getCurrentHead(); + + GitCherryPickResult.CherryPickStatus status = GitCherryPickResult.CherryPickStatus.valueOf(res.getStatus().name()); + List conflicts; + if (res.getStatus() == CherryPickResult.CherryPickStatus.CONFLICTING) { + conflicts = getConflicts(currHead); + } else { + conflicts = Collections.emptyList(); + } + List commits = toCommits(cherryPickedRefs); + return getClassFactory().createCherryPickResult(status, conflicts, getFailures(res), currHead, commits); + } + + private List toCommits (List cherryPickedRefs) { + List commits = new ArrayList<>(cherryPickedRefs.size()); + Repository repository = getRepository(); + RevWalk walk = new RevWalk(repository); + for (Ref ref : cherryPickedRefs) { + try { + commits.add(getClassFactory().createRevisionInfo(Utils.findCommit(repository, + ref.getLeaf().getObjectId(), walk), repository)); + } catch (GitException ex) { + Logger.getLogger(CherryPickCommand.class.getName()).log(Level.INFO, null, ex); + } + } + return commits; + } + + private GitRevisionInfo getCurrentHead () { + GitRevisionInfo currHead; + Repository repository = getRepository(); + try { + currHead = getClassFactory().createRevisionInfo(Utils.findCommit(repository, Constants.HEAD), repository); + } catch (GitException ex) { + currHead = null; + } + return currHead; + } + + private GitCherryPickResult createCustomResult (GitCherryPickResult.CherryPickStatus status) { + return createCustomResult(status, Collections.emptyList()); + } + + private GitCherryPickResult createCustomResult (GitCherryPickResult.CherryPickStatus status, List cherryPickedRefs) { + return getClassFactory().createCherryPickResult(status, Collections.emptyList(), + Collections.emptyList(), getCurrentHead(), toCommits(cherryPickedRefs)); + } + + private List getConflicts (GitRevisionInfo info) { + List conflicts; + try { + Repository repository = getRepository(); + Map modifiedFiles = info.getModifiedFiles(); + ConflictCommand cmd = new ConflictCommand(repository, getClassFactory(), modifiedFiles.keySet().toArray( + new File[modifiedFiles.keySet().size()]), + new DelegatingGitProgressMonitor(monitor), + new StatusListener() { + @Override + public void notifyStatus (GitStatus status) { } + }); + cmd.execute(); + Map statuses = cmd.getStatuses(); + conflicts = new ArrayList<>(statuses.size()); + for (Map.Entry e : statuses.entrySet()) { + if (e.getValue().isConflict()) { + conflicts.add(e.getKey()); + } + } + } catch (GitException ex) { + Logger.getLogger(CherryPickCommand.class.getName()).log(Level.INFO, null, ex); + conflicts = Collections.emptyList(); + } + return conflicts; + } + + private List getFailures (CherryPickResult result) { + List files = new ArrayList<>(); + File workDir = getRepository().getWorkTree(); + if (result.getStatus() == CherryPickResult.CherryPickStatus.FAILED) { + Map obstructions = result.getFailingPaths(); + if (obstructions != null) { + for (Map.Entry failure : obstructions.entrySet()) { + files.add(new File(workDir, failure.getKey())); + } + } + } + return Collections.unmodifiableList(files); + } + + private File getSequencerFolder () { + return new File(getRepository().getDirectory(), SEQUENCER); + } + + private ObjectId getOriginalCommit () throws GitException { + Repository repository = getRepository(); + File seqHead = new File(getSequencerFolder(), SEQUENCER_HEAD); + ObjectId originalCommitId = null; + if (seqHead.canRead()) { + try { + byte[] content = IO.readFully(seqHead); + if (content.length > 0) { + originalCommitId = ObjectId.fromString(content, 0); + } + if (originalCommitId != null) { + originalCommitId = repository.resolve(originalCommitId.getName() + "^{commit}"); + } + } catch (IOException e) { + } + } + return originalCommitId; + } + + private ObjectId getHead () throws GitException { + return Utils.findCommit(getRepository(), Constants.HEAD); + } + + private List prepareCommand (ObjectId head) throws GitException, IOException { + Repository repository = getRepository(); + ObjectReader or = repository.newObjectReader(); + RevWalk walk = new RevWalk(or); + List commits = new ArrayList<>(revisions.length); + for (String rev : revisions) { + RevCommit commit = Utils.findCommit(repository, rev, walk); + commits.add(commit); + } + List steps = new ArrayList<>(commits.size()); + if (commits.size() == 1) { + RevCommit commit = commits.get(0); + steps.add(new RebaseTodoLine(RebaseTodoLine.Action.PICK, + or.abbreviate(commit), commit.getShortMessage())); + } else if (!commits.isEmpty()) { + File sequencer = getSequencerFolder(); + sequencer.mkdirs(); + try { + for (RevCommit commit : commits) { + steps.add(new RebaseTodoLine(RebaseTodoLine.Action.PICK, + or.abbreviate(commit), commit.getShortMessage())); + } + writeTodoFile(repository, steps); + writeFile(new File(sequencer, SEQUENCER_HEAD), head); + } catch (IOException ex) { + Utils.deleteRecursively(sequencer); + throw new GitException(ex); + } + } + return steps; + } + + private void writeFile (File file, ObjectId id) throws IOException { + try (BufferedOutputStream bos = new SafeBufferedOutputStream(new FileOutputStream(file))) { + id.copyTo(bos); + bos.write('\n'); + } + } + + private void writeTodoFile (Repository repository, List steps) throws IOException { + File f = new File(repository.getDirectory(), SEQUENCER); + if (f.canWrite()) { + RebaseTodoFile todoFile = new RebaseTodoFile(repository); + todoFile.writeRebaseTodoFile(SEQUENCER + File.separator + SEQUENCER_TODO, steps, false); + } + } + + private List readTodoFile (Repository repository) throws IOException { + String path = SEQUENCER + File.separator + SEQUENCER_TODO; + File f = new File(repository.getDirectory(), path); + if (f.canRead()) { + RebaseTodoFile todoFile = new RebaseTodoFile(repository); + return todoFile.readRebaseTodo(SEQUENCER + File.separator + SEQUENCER_TODO, true); + } + return Collections.emptyList(); + } +} --- a/libs.git/src/org/netbeans/libs/git/jgit/commands/CommitCommand.java Mon Jun 30 12:40:30 2014 +0200 +++ a/libs.git/src/org/netbeans/libs/git/jgit/commands/CommitCommand.java Tue Jul 01 13:16:35 2014 +0200 @@ -57,7 +57,6 @@ import org.eclipse.jgit.dircache.DirCacheIterator; import org.eclipse.jgit.errors.CorruptObjectException; import org.eclipse.jgit.errors.NoWorkTreeException; -import org.eclipse.jgit.errors.UnmergedPathException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.FileMode; import org.eclipse.jgit.lib.ObjectId; @@ -105,11 +104,17 @@ boolean retval = super.prepareCommand(); if (retval) { RepositoryState state = getRepository().getRepositoryState(); - if (RepositoryState.MERGING.equals(state)) { + if (amend && !state.canAmend()) { + String errorMessage = Utils.getBundle(CommitCommand.class).getString("MSG_Error_Commit_CannotAmend"); //NOI18N + monitor.preparationsFailed(errorMessage); + throw new GitException(errorMessage); + } + if (RepositoryState.MERGING.equals(state) || RepositoryState.CHERRY_PICKING.equals(state)) { String errorMessage = Utils.getBundle(CommitCommand.class).getString("MSG_Error_Commit_ConflictsInIndex"); //NOI18N monitor.preparationsFailed(errorMessage); throw new GitException(errorMessage); - } else if (RepositoryState.MERGING_RESOLVED.equals(state) && roots.length > 0) { + } else if ((RepositoryState.MERGING_RESOLVED.equals(state) + || RepositoryState.CHERRY_PICKING_RESOLVED.equals(state)) && roots.length > 0) { boolean fullWorkingTree = false; File repositoryRoot = getRepository().getWorkTree(); for (File root : roots) { @@ -149,10 +154,7 @@ if(commiter != null) { commit.setCommitter(commiter.getName(), commiter.getEmailAddress()); } - if (amend) { - RevCommit lastCommit = Utils.findCommit(repository, "HEAD^{commit}"); - transferTimestamp(commit, lastCommit); - } + setAuthorshipIfNeeded(repository, commit); commit.setMessage(message); commit.setAmend(amend); @@ -170,18 +172,20 @@ } } } - } catch (GitAPIException ex) { + } catch (GitAPIException | JGitInternalException | NoWorkTreeException | IOException ex) { throw new GitException(ex); - } catch (UnmergedPathException ex) { - throw new GitException(ex); - } catch (JGitInternalException ex) { - throw new GitException(ex); - } catch (NoWorkTreeException ex) { - throw new GitException(ex); - } catch (CorruptObjectException ex) { - throw new GitException(ex); - } catch (IOException ex) { - throw new GitException(ex); + } + } + + private void setAuthorshipIfNeeded (Repository repository, org.eclipse.jgit.api.CommitCommand cmd) + throws GitException, NoWorkTreeException, IOException { + if (amend) { + RevCommit lastCommit = Utils.findCommit(repository, "HEAD^{commit}"); + transferTimestamp(cmd, lastCommit); + } + if (repository.getRepositoryState() == RepositoryState.CHERRY_PICKING_RESOLVED) { + RevCommit lastCommit = Utils.findCommit(repository, repository.readCherryPickHead(), null); + transferTimestamp(cmd, lastCommit); } } --- a/libs.git/src/org/netbeans/libs/git/jgit/commands/RebaseCommand.java Mon Jun 30 12:40:30 2014 +0200 +++ a/libs.git/src/org/netbeans/libs/git/jgit/commands/RebaseCommand.java Tue Jul 01 13:16:35 2014 +0200 @@ -162,8 +162,8 @@ Repository repository = getRepository(); GitRevisionInfo info = getClassFactory().createRevisionInfo(currentCommit, repository); Map modifiedFiles = info.getModifiedFiles(); - StatusCommand cmd = new StatusCommand(repository, Constants.HEAD, modifiedFiles.keySet().toArray( - new File[modifiedFiles.keySet().size()]), getClassFactory(), + ConflictCommand cmd = new ConflictCommand(repository, getClassFactory(), modifiedFiles.keySet().toArray( + new File[modifiedFiles.keySet().size()]), new DelegatingGitProgressMonitor(monitor), new StatusListener() { @Override --- a/libs.git/test/unit/src/org/netbeans/libs/git/GitEnumsStateTest.java Mon Jun 30 12:40:30 2014 +0200 +++ a/libs.git/test/unit/src/org/netbeans/libs/git/GitEnumsStateTest.java Tue Jul 01 13:16:35 2014 +0200 @@ -43,6 +43,7 @@ package org.netbeans.libs.git; import java.io.IOException; +import org.eclipse.jgit.api.CherryPickResult; import org.eclipse.jgit.api.MergeResult; import org.eclipse.jgit.api.RebaseResult; import org.eclipse.jgit.lib.RefUpdate; @@ -96,4 +97,10 @@ assertNotNull(GitSubmoduleStatus.parseStatus(status)); } } + + public void testCherryPickStatus () { + for (CherryPickResult.CherryPickStatus status : CherryPickResult.CherryPickStatus.values()) { + assertNotNull(GitCherryPickResult.CherryPickStatus.valueOf(status.name())); + } + } } --- a/libs.git/test/unit/src/org/netbeans/libs/git/jgit/CommandsTestSuite.java Mon Jun 30 12:40:30 2014 +0200 +++ a/libs.git/test/unit/src/org/netbeans/libs/git/jgit/CommandsTestSuite.java Tue Jul 01 13:16:35 2014 +0200 @@ -51,6 +51,7 @@ import org.netbeans.libs.git.jgit.commands.BranchTest; import org.netbeans.libs.git.jgit.commands.CatTest; import org.netbeans.libs.git.jgit.commands.CheckoutTest; +import org.netbeans.libs.git.jgit.commands.CherryPickTest; import org.netbeans.libs.git.jgit.commands.CleanTest; import org.netbeans.libs.git.jgit.commands.CommitTest; import org.netbeans.libs.git.jgit.commands.CompareCommitTest; @@ -100,6 +101,7 @@ suite.addTestSuite(BranchTest.class); suite.addTestSuite(CatTest.class); suite.addTestSuite(CheckoutTest.class); + suite.addTestSuite(CherryPickTest.class); suite.addTestSuite(CleanTest.class); suite.addTestSuite(CommitTest.class); suite.addTestSuite(CompareCommitTest.class); --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ d68b118f158f Tue Jul 01 13:16:35 2014 +0200 @@ -0,0 +1,359 @@ +package org.netbeans.libs.git.jgit.commands; + +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2010 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 2010 Sun Microsystems, Inc. + */ + +import java.io.File; +import java.io.IOException; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.lib.RepositoryState; +import org.eclipse.jgit.revwalk.RevCommit; +import org.netbeans.libs.git.GitCherryPickResult; +import org.netbeans.libs.git.GitClient; +import org.netbeans.libs.git.GitRevisionInfo; +import org.netbeans.libs.git.GitStatus.Status; +import org.netbeans.libs.git.SearchCriteria; +import org.netbeans.libs.git.jgit.AbstractGitTestCase; +import org.netbeans.libs.git.jgit.Utils; + +/** + * + * @author ondra + */ +public class CherryPickTest extends AbstractGitTestCase { + + private File workDir; + private Repository repository; + private static final String BRANCH = "b"; + + public CherryPickTest (String testName) throws IOException { + super(testName); + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + workDir = getWorkingDirectory(); + repository = getRepository(getLocalGitRepository()); + } + + public void testCherryPickCommit () throws Exception { + File f = new File(workDir, "f"); + write(f, "init"); + add(f); + commit(f); + + GitClient client = getClient(workDir); + client.createBranch(BRANCH, Constants.MASTER, NULL_PROGRESS_MONITOR); + client.checkoutRevision(BRANCH, true, NULL_PROGRESS_MONITOR); + + write(f, "change on branch"); + add(f); + GitRevisionInfo c = client.commit(new File[] { f }, "on branch", null, null, NULL_PROGRESS_MONITOR); + + client.checkoutRevision(Constants.MASTER, true, NULL_PROGRESS_MONITOR); + GitRevisionInfo initCommit = client.log("HEAD", NULL_PROGRESS_MONITOR); + + Thread.sleep(1100); + + GitCherryPickResult res = client.cherryPick(GitClient.CherryPickOperation.BEGIN, new String[] { BRANCH }, NULL_PROGRESS_MONITOR); + assertEquals(GitCherryPickResult.CherryPickStatus.OK, res.getCherryPickStatus()); + assertEquals(initCommit.getRevision(), res.getCurrentHead().getParents()[0]); + assertEquals(c.getCommitTime(), res.getCurrentHead().getCommitTime()); + assertEquals(1, res.getCherryPickedCommits().length); + assertEquals(c.getRevision(), res.getCherryPickedCommits()[0].getRevision()); + assertFalse(new File(workDir, ".git/sequencer").exists()); + } + + public void testCherryPickCommits () throws Exception { + File f = new File(workDir, "f"); + write(f, "init"); + add(f); + commit(f); + + File[] roots = new File[] { f }; + + GitClient client = getClient(workDir); + client.createBranch(BRANCH, Constants.MASTER, NULL_PROGRESS_MONITOR); + client.checkoutRevision(BRANCH, true, NULL_PROGRESS_MONITOR); + + write(f, "change 1 on branch"); + add(f); + GitRevisionInfo c1 = client.commit(roots, "on branch 1", null, null, NULL_PROGRESS_MONITOR); + write(f, "change 2 on branch"); + add(f); + GitRevisionInfo c2 = client.commit(roots, "on branch 2", null, null, NULL_PROGRESS_MONITOR); + + client.checkoutRevision(Constants.MASTER, true, NULL_PROGRESS_MONITOR); + GitRevisionInfo initCommit = client.log("HEAD", NULL_PROGRESS_MONITOR); + + Thread.sleep(1100); + + GitCherryPickResult res = client.cherryPick(GitClient.CherryPickOperation.BEGIN, + new String[] { c1.getRevision(), c2.getRevision() }, NULL_PROGRESS_MONITOR); + assertEquals(GitCherryPickResult.CherryPickStatus.OK, res.getCherryPickStatus()); + SearchCriteria sc = new SearchCriteria(); + sc.setRevisionTo("HEAD"); + GitRevisionInfo[] logs = client.log(sc, NULL_PROGRESS_MONITOR); + assertEquals(3, logs.length); + assertEquals(c2.getFullMessage(), logs[0].getFullMessage()); + assertEquals(c2.getCommitTime(), logs[0].getCommitTime()); + assertEquals(c1.getFullMessage(), logs[1].getFullMessage()); + assertEquals(c1.getCommitTime(), logs[1].getCommitTime()); + assertEquals(initCommit.getRevision(), logs[2].getRevision()); + + assertEquals(2, res.getCherryPickedCommits().length); + assertEquals(c1.getRevision(), res.getCherryPickedCommits()[0].getRevision()); + assertEquals(c2.getRevision(), res.getCherryPickedCommits()[1].getRevision()); + assertFalse(new File(workDir, ".git/sequencer").exists()); + } + + public void testCherryPickFailure () throws Exception { + File f = new File(workDir, "f"); + write(f, "init"); + add(f); + commit(f); + + File[] roots = new File[] { f }; + + GitClient client = getClient(workDir); + client.createBranch(BRANCH, Constants.MASTER, NULL_PROGRESS_MONITOR); + client.checkoutRevision(BRANCH, true, NULL_PROGRESS_MONITOR); + + write(f, "change on branch"); + add(f); + GitRevisionInfo c = client.commit(roots, "on branch", null, null, NULL_PROGRESS_MONITOR); + + client.checkoutRevision(Constants.MASTER, true, NULL_PROGRESS_MONITOR); + // make modification so cherry-pick cannot start + write(f, "change in master"); + + GitCherryPickResult res = client.cherryPick(GitClient.CherryPickOperation.BEGIN, new String[] { BRANCH }, NULL_PROGRESS_MONITOR); + assertEquals(GitCherryPickResult.CherryPickStatus.FAILED, res.getCherryPickStatus()); + assertEquals(0, res.getCurrentHead().getParents().length); + } + + public void testCherryPickCommitsConflictAbort () throws Exception { + File f = new File(workDir, "f"); + write(f, "init"); + add(f); + commit(f); + + File[] roots = new File[] { f }; + + GitClient client = getClient(workDir); + client.createBranch(BRANCH, Constants.MASTER, NULL_PROGRESS_MONITOR); + client.checkoutRevision(BRANCH, true, NULL_PROGRESS_MONITOR); + + write(f, "change 1 on branch"); + add(f); + GitRevisionInfo c1 = client.commit(roots, "on branch 1", null, null, NULL_PROGRESS_MONITOR); + write(f, "change 2 on branch"); + add(f); + GitRevisionInfo c2 = client.commit(roots, "on branch 2", null, null, NULL_PROGRESS_MONITOR); + write(f, "change 3 on branch"); + add(f); + GitRevisionInfo c3 = client.commit(roots, "on branch 3", null, null, NULL_PROGRESS_MONITOR); + + client.checkoutRevision(Constants.MASTER, true, NULL_PROGRESS_MONITOR); + GitRevisionInfo initCommit = client.log("HEAD", NULL_PROGRESS_MONITOR); + GitCherryPickResult res = client.cherryPick(GitClient.CherryPickOperation.BEGIN, + new String[] { c1.getRevision(), c3.getRevision() }, NULL_PROGRESS_MONITOR); + assertEquals(GitCherryPickResult.CherryPickStatus.CONFLICTING, res.getCherryPickStatus()); + assertEquals(initCommit.getRevision(), res.getCurrentHead().getParents()[0]); + + res = client.cherryPick(GitClient.CherryPickOperation.ABORT, null, NULL_PROGRESS_MONITOR); + assertEquals(GitCherryPickResult.CherryPickStatus.ABORTED, res.getCherryPickStatus()); + assertEquals(initCommit.getRevision(), res.getCurrentHead().getRevision()); + assertStatus(client.getStatus(roots, NULL_PROGRESS_MONITOR), workDir, f, true, + Status.STATUS_NORMAL, Status.STATUS_NORMAL, Status.STATUS_NORMAL, false); + } + + public void testCherryPickCommitsConflictQuit () throws Exception { + File f = new File(workDir, "f"); + write(f, "init"); + add(f); + commit(f); + + File[] roots = new File[] { f }; + + GitClient client = getClient(workDir); + client.createBranch(BRANCH, Constants.MASTER, NULL_PROGRESS_MONITOR); + client.checkoutRevision(BRANCH, true, NULL_PROGRESS_MONITOR); + + write(f, "change 1 on branch"); + add(f); + GitRevisionInfo c1 = client.commit(roots, "on branch 1\nblablabla", null, null, NULL_PROGRESS_MONITOR); + write(f, "change 2 on branch"); + add(f); + GitRevisionInfo c2 = client.commit(roots, "on branch 2", null, null, NULL_PROGRESS_MONITOR); + write(f, "change 3 on branch"); + add(f); + GitRevisionInfo c3 = client.commit(roots, "on branch 3\nBLABLABLA", null, null, NULL_PROGRESS_MONITOR); + + client.checkoutRevision(Constants.MASTER, true, NULL_PROGRESS_MONITOR); + GitRevisionInfo initCommit = client.log("HEAD", NULL_PROGRESS_MONITOR); + GitCherryPickResult res = client.cherryPick(GitClient.CherryPickOperation.BEGIN, + new String[] { c1.getRevision(), c3.getRevision() }, NULL_PROGRESS_MONITOR); + assertEquals(GitCherryPickResult.CherryPickStatus.CONFLICTING, res.getCherryPickStatus()); + assertEquals(initCommit.getRevision(), res.getCurrentHead().getParents()[0]); + + ObjectReader or = repository.newObjectReader(); + RevCommit commit = Utils.findCommit(repository, c3.getRevision()); + assertEquals("pick " + or.abbreviate(commit).name() + " " + commit.getShortMessage(), + read(new File(repository.getDirectory(), "sequencer/todo"))); + + res = client.cherryPick(GitClient.CherryPickOperation.QUIT, null, NULL_PROGRESS_MONITOR); + assertEquals(GitCherryPickResult.CherryPickStatus.CONFLICTING, res.getCherryPickStatus()); + assertEquals(initCommit.getRevision(), res.getCurrentHead().getParents()[0]); + assertStatus(client.getStatus(roots, NULL_PROGRESS_MONITOR), workDir, f, true, + Status.STATUS_NORMAL, Status.STATUS_NORMAL, Status.STATUS_NORMAL, true); + assertEquals(RepositoryState.CHERRY_PICKING, repository.getRepositoryState()); + + res = client.cherryPick(GitClient.CherryPickOperation.ABORT, null, NULL_PROGRESS_MONITOR); + assertEquals(GitCherryPickResult.CherryPickStatus.ABORTED, res.getCherryPickStatus()); + assertEquals(initCommit.getRevision(), res.getCurrentHead().getParents()[0]); + assertStatus(client.getStatus(roots, NULL_PROGRESS_MONITOR), workDir, f, true, + Status.STATUS_NORMAL, Status.STATUS_NORMAL, Status.STATUS_NORMAL, false); + } + + public void testCherryPickCommitConflictResolve () throws Exception { + File f = new File(workDir, "f"); + write(f, "init"); + add(f); + commit(f); + + File[] roots = new File[] { f }; + + GitClient client = getClient(workDir); + client.createBranch(BRANCH, Constants.MASTER, NULL_PROGRESS_MONITOR); + client.checkoutRevision(BRANCH, true, NULL_PROGRESS_MONITOR); + + write(f, "change 1 on branch"); + add(f); + GitRevisionInfo c1 = client.commit(roots, "on branch 1", null, null, NULL_PROGRESS_MONITOR); + write(f, "change 2 on branch"); + add(f); + GitRevisionInfo c2 = client.commit(roots, "on branch 2", null, null, NULL_PROGRESS_MONITOR); + + client.checkoutRevision(Constants.MASTER, true, NULL_PROGRESS_MONITOR); + GitRevisionInfo initCommit = client.log("HEAD", NULL_PROGRESS_MONITOR); + GitCherryPickResult res = client.cherryPick(GitClient.CherryPickOperation.BEGIN, + new String[] { c2.getRevision() }, NULL_PROGRESS_MONITOR); + assertEquals(GitCherryPickResult.CherryPickStatus.CONFLICTING, res.getCherryPickStatus()); + assertEquals(initCommit.getRevision(), res.getCurrentHead().getRevision()); + assertFalse(new File(repository.getDirectory(), "sequencer").exists()); + + write(f, "init\nchange 2 on branch"); + add(f); + + // try continue, should interrupt and ask for commit + res = client.cherryPick(GitClient.CherryPickOperation.CONTINUE, null, NULL_PROGRESS_MONITOR); + assertEquals(GitCherryPickResult.CherryPickStatus.UNCOMMITTED, res.getCherryPickStatus()); + assertEquals(initCommit.getRevision(), res.getCurrentHead().getRevision()); + assertFalse(new File(repository.getDirectory(), "sequencer").exists()); + + GitRevisionInfo commit = client.commit(new File[0], c2.getFullMessage(), null, null, NULL_PROGRESS_MONITOR); + assertEquals(c2.getCommitTime(), commit.getCommitTime()); + assertEquals(RepositoryState.SAFE, repository.getRepositoryState()); + assertFalse(new File(repository.getDirectory(), "sequencer").exists()); + + res = client.cherryPick(GitClient.CherryPickOperation.CONTINUE, null, NULL_PROGRESS_MONITOR); + assertEquals(GitCherryPickResult.CherryPickStatus.OK, res.getCherryPickStatus()); + } + + public void testCherryPickCommitsConflictResolve () throws Exception { + File f = new File(workDir, "f"); + write(f, "init"); + add(f); + commit(f); + + File[] roots = new File[] { f }; + + GitClient client = getClient(workDir); + client.createBranch(BRANCH, Constants.MASTER, NULL_PROGRESS_MONITOR); + client.checkoutRevision(BRANCH, true, NULL_PROGRESS_MONITOR); + + write(f, "change 1 on branch"); + add(f); + GitRevisionInfo c1 = client.commit(roots, "on branch 1", null, null, NULL_PROGRESS_MONITOR); + write(f, "init\nchange 2 on branch"); + add(f); + GitRevisionInfo c2 = client.commit(roots, "on branch 2", null, null, NULL_PROGRESS_MONITOR); + write(f, "init\nchange 3 on branch"); + add(f); + GitRevisionInfo c3 = client.commit(roots, "on branch 3", null, null, NULL_PROGRESS_MONITOR); + + Thread.sleep(1100); + + client.checkoutRevision(Constants.MASTER, true, NULL_PROGRESS_MONITOR); + GitRevisionInfo initCommit = client.log("HEAD", NULL_PROGRESS_MONITOR); + GitCherryPickResult res = client.cherryPick(GitClient.CherryPickOperation.BEGIN, + new String[] { c2.getRevision(), c3.getRevision() }, NULL_PROGRESS_MONITOR); + assertEquals(GitCherryPickResult.CherryPickStatus.CONFLICTING, res.getCherryPickStatus()); + assertEquals(initCommit.getRevision(), res.getCurrentHead().getRevision()); + + write(f, "init\nchange 2 on branch"); + add(f); + + // try continue, should interrupt and ask for commit + res = client.cherryPick(GitClient.CherryPickOperation.CONTINUE, null, NULL_PROGRESS_MONITOR); + assertEquals(GitCherryPickResult.CherryPickStatus.UNCOMMITTED, res.getCherryPickStatus()); + assertEquals(initCommit.getRevision(), res.getCurrentHead().getRevision()); + assertTrue(new File(repository.getDirectory(), "sequencer").exists()); + + GitRevisionInfo commit = client.commit(new File[0], c2.getFullMessage(), null, null, NULL_PROGRESS_MONITOR); + assertEquals(c2.getCommitTime(), commit.getCommitTime()); + assertEquals(RepositoryState.SAFE, repository.getRepositoryState()); + assertTrue(new File(repository.getDirectory(), "sequencer").exists()); + + res = client.cherryPick(GitClient.CherryPickOperation.CONTINUE, null, NULL_PROGRESS_MONITOR); + assertEquals(GitCherryPickResult.CherryPickStatus.OK, res.getCherryPickStatus()); + assertEquals(commit.getRevision(), res.getCurrentHead().getParents()[0]); + assertEquals(c3.getCommitTime(), res.getCurrentHead().getCommitTime()); + assertFalse(new File(repository.getDirectory(), "sequencer").exists()); + assertEquals(1, res.getCherryPickedCommits().length); + assertEquals(c3.getRevision(), res.getCherryPickedCommits()[0].getRevision()); + } +} --- a/libs.git/test/unit/src/org/netbeans/libs/git/jgit/commands/CommitTest.java Mon Jun 30 12:40:30 2014 +0200 +++ a/libs.git/test/unit/src/org/netbeans/libs/git/jgit/commands/CommitTest.java Tue Jul 01 13:16:35 2014 +0200 @@ -679,4 +679,43 @@ assertEquals(info.getRevision(), lastCommit.getParents()[0]); assertEquals(lastCommit.getRevision(), client.getBranches(false, NULL_PROGRESS_MONITOR).get("master").getId()); } + + public void testCherryPickCommit () throws Exception { + repository.getConfig().setString("user", null, "name", "John"); + repository.getConfig().setString("user", null, "email", "john@git.com"); + repository.getConfig().save(); + + File f = new File(workDir, "f"); + write(f, "init"); + File[] files = new File[] { f }; + + add(f); + commit(f); + + GitClient client = getClient(workDir); + write(f, "change"); + add(f); + GitRevisionInfo info = client.commit(files, "change to CherryPick", null, null, NULL_PROGRESS_MONITOR); + + Thread.sleep(1100); + + client.reset("HEAD~1", GitClient.ResetType.MIXED, NULL_PROGRESS_MONITOR); + repository.writeCherryPickHead(repository.resolve(info.getRevision())); + + // now we are cherry-picking + // amend is not allowed + try { + client.commit(new File[0], info.getFullMessage(), null, null, true, NULL_PROGRESS_MONITOR); + fail("Amend not allowed"); + } catch (GitException ex) { + assertEquals(Utils.getBundle(CommitCommand.class).getString("MSG_Error_Commit_CannotAmend"), ex.getMessage()); + } + + // doing commit should preserve authorship of the original commit (info) + GitRevisionInfo commit = client.commit(new File[0], info.getFullMessage(), null, null, NULL_PROGRESS_MONITOR); + assertEquals(info.getAuthor(), commit.getAuthor()); + assertEquals(info.getCommitTime(), commit.getCommitTime()); + assertEquals(Utils.findCommit(repository, info.getRevision()).getAuthorIdent().getWhen(), + Utils.findCommit(repository, commit.getRevision()).getAuthorIdent().getWhen()); + } }