blob: b4250a0d142b56bb821f2fe82c939cf0511df888 [file] [log] [blame]
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package git4idea.cherrypick;
import com.intellij.dvcs.DvcsUtil;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.ThrowableComputable;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.vcs.log.*;
import git4idea.GitLocalBranch;
import git4idea.GitPlatformFacade;
import git4idea.GitVcs;
import git4idea.commands.Git;
import git4idea.config.GitVcsSettings;
import git4idea.history.browser.GitHeavyCommit;
import git4idea.history.wholeTree.AbstractHash;
import git4idea.history.wholeTree.GitCommitDetailsProvider;
import git4idea.repo.GitRepository;
import icons.Git4ideaIcons;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author Kirill Likhodedov
*/
public class GitCherryPickAction extends DumbAwareAction {
private static final String NAME = "Cherry-Pick";
private static final Logger LOG = Logger.getInstance(GitCherryPickAction.class);
@NotNull private final GitPlatformFacade myPlatformFacade;
@NotNull private final Git myGit;
@NotNull private final Set<Hash> myIdsInProgress;
public GitCherryPickAction() {
super(NAME, null, Git4ideaIcons.CherryPick);
myGit = ServiceManager.getService(Git.class);
myPlatformFacade = ServiceManager.getService(GitPlatformFacade.class);
myIdsInProgress = ContainerUtil.newHashSet();
}
@Override
public void actionPerformed(AnActionEvent e) {
final Project project = e.getProject();
final List<? extends VcsFullCommitDetails> commits = getSelectedCommits(e);
if (project == null || commits == null || commits.isEmpty()) {
LOG.info(String.format("Cherry-pick action should be disabled. Project: %s, commits: %s", project, commits));
return;
}
for (VcsFullCommitDetails commit : commits) {
myIdsInProgress.add(commit.getId());
}
FileDocumentManager.getInstance().saveAllDocuments();
myPlatformFacade.getChangeListManager(project).blockModalNotifications();
new Task.Backgroundable(project, "Cherry-picking", false) {
public void run(@NotNull ProgressIndicator indicator) {
try {
Map<GitRepository, List<VcsFullCommitDetails>> commitsInRoots = sortCommits(groupCommitsByRoots(project, commits));
new GitCherryPicker(project, myGit, myPlatformFacade, isAutoCommit(project)).cherryPick(commitsInRoots);
}
finally {
ApplicationManager.getApplication().invokeLater(new Runnable() {
public void run() {
myPlatformFacade.getChangeListManager(project).unblockModalNotifications();
for (VcsFullCommitDetails commit : commits) {
myIdsInProgress.remove(commit.getId());
}
}
});
}
}
}.queue();
}
/**
* Sort commits so that earliest ones come first: they need to be cherry-picked first.
*/
@NotNull
private static Map<GitRepository, List<VcsFullCommitDetails>> sortCommits(Map<GitRepository, List<VcsFullCommitDetails>> groupedCommits) {
for (List<VcsFullCommitDetails> gitCommits : groupedCommits.values()) {
Collections.reverse(gitCommits);
}
return groupedCommits;
}
@NotNull
private Map<GitRepository, List<VcsFullCommitDetails>> groupCommitsByRoots(@NotNull Project project,
@NotNull List<? extends VcsFullCommitDetails> commits) {
Map<GitRepository, List<VcsFullCommitDetails>> groupedCommits = ContainerUtil.newHashMap();
for (VcsFullCommitDetails commit : commits) {
GitRepository repository = myPlatformFacade.getRepositoryManager(project).getRepositoryForRoot(commit.getRoot());
if (repository == null) {
LOG.info("No repository found for commit " + commit);
continue;
}
List<VcsFullCommitDetails> commitsInRoot = groupedCommits.get(repository);
if (commitsInRoot == null) {
commitsInRoot = ContainerUtil.newArrayList();
groupedCommits.put(repository, commitsInRoot);
}
commitsInRoot.add(commit);
}
return groupedCommits;
}
private static boolean isAutoCommit(@NotNull Project project) {
return GitVcsSettings.getInstance(project).isAutoCommitOnCherryPick();
}
@Override
public void update(AnActionEvent e) {
super.update(e);
VcsLog log = getVcsLog(e);
Project project = getEventProject(e);
if (project == null || log == null || !DvcsUtil.logHasRootForVcs(log, GitVcs.getKey())) {
e.getPresentation().setEnabledAndVisible(false);
}
else {
e.getPresentation().setEnabled(enabled(e));
e.getPresentation().setText(isAutoCommit(project) ? NAME : NAME + "...");
}
}
private boolean enabled(AnActionEvent e) {
final List<? extends VcsFullCommitDetails> commits = getSelectedCommits(e);
final Project project = e.getProject();
if (commits == null || commits.isEmpty() || project == null) {
return false;
}
for (VcsFullCommitDetails commit : commits) {
if (myIdsInProgress.contains(commit.getId())) {
return false;
}
GitRepository repository = myPlatformFacade.getRepositoryManager(project).getRepositoryForRoot(commit.getRoot());
if (repository == null) {
return false;
}
GitLocalBranch currentBranch = repository.getCurrentBranch();
Collection<String> containingBranches = getContainingBranches(e, commit, repository);
if (currentBranch != null && containingBranches != null && containingBranches.contains(currentBranch.getName())) {
// already is contained in the current branch
return false;
}
}
return true;
}
// TODO remove after removing the old Vcs Log implementation
@Nullable
private List<? extends VcsFullCommitDetails> getSelectedCommits(AnActionEvent e) {
final Project project = e.getProject();
if (project == null) {
return null;
}
List<GitHeavyCommit> commits = e.getData(GitVcs.SELECTED_COMMITS);
if (commits != null) {
return convertHeavyCommitToFullDetails(commits, project);
}
final VcsLog log = getVcsLog(e);
if (log == null) {
return null;
}
List<Hash> selectedCommits = log.getSelectedCommits();
List<VcsFullCommitDetails> selectedDetails = ContainerUtil.newArrayList();
for (Hash commit : selectedCommits) {
VcsFullCommitDetails details = log.getDetailsIfAvailable(commit);
if (details == null) { // let the action be unavailable until all details are loaded
return null;
}
GitRepository root = myPlatformFacade.getRepositoryManager(project).getRepositoryForRoot(details.getRoot());
// don't allow to cherry-pick if a non-Git commit was selected
// we could cherry-pick just Git commits filtered from the list, but it might provide confusion
if (root == null) {
return null;
}
selectedDetails.add(details);
}
return selectedDetails;
}
private static List<? extends VcsFullCommitDetails> convertHeavyCommitToFullDetails(List<GitHeavyCommit> commits, final Project project) {
return ContainerUtil.map(commits, new Function<GitHeavyCommit, VcsFullCommitDetails>() {
@Override
public VcsFullCommitDetails fun(GitHeavyCommit commit) {
final VcsLogObjectsFactory factory = ServiceManager.getService(project, VcsLogObjectsFactory.class);
List<Hash> parents = ContainerUtil.map(commit.getParentsHashes(), new Function<String, Hash>() {
@Override
public Hash fun(String hashValue) {
return factory.createHash(hashValue);
}
});
final List<Change> changes = commit.getChanges();
return factory.createFullDetails(
factory.createHash(commit.getHash().getValue()), parents, commit.getAuthorTime(), commit.getRoot(), commit.getSubject(),
commit.getAuthor(), commit.getAuthorEmail(), commit.getDescription(), commit.getCommitter(), commit.getCommitterEmail(),
commit.getDate().getTime(), new ThrowableComputable<Collection<Change>, Exception>() {
@Override
public Collection<Change> compute() throws Exception {
return changes;
}
}
);
}
});
}
private static VcsLog getVcsLog(@NotNull AnActionEvent event) {
return event.getData(VcsLogDataKeys.VSC_LOG);
}
// TODO remove after removing the old Vcs Log implementation
@Nullable
private static Collection<String> getContainingBranches(AnActionEvent event, VcsFullCommitDetails commit, GitRepository repository) {
GitCommitDetailsProvider detailsProvider = event.getData(GitVcs.COMMIT_DETAILS_PROVIDER);
if (detailsProvider != null) {
return detailsProvider.getContainingBranches(repository.getRoot(), AbstractHash.create(commit.getId().toShortString()));
}
if (event.getProject() == null) {
return null;
}
VcsLog log = getVcsLog(event);
if (log == null) {
return null;
}
return log.getContainingBranches(commit.getId());
}
}