blob: d0f051c5047548db6adf6df27c7acf1888348171 [file] [log] [blame]
/*
* Copyright 2000-2014 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.history;
import com.intellij.dvcs.DvcsUtil;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.impl.SimpleDataContext;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.ui.popup.ListPopup;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.history.BaseDiffFromHistoryHandler;
import com.intellij.openapi.vcs.history.DiffFromHistoryHandler;
import com.intellij.openapi.vcs.history.VcsFileRevision;
import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Consumer;
import com.intellij.util.containers.ContainerUtil;
import git4idea.GitFileRevision;
import git4idea.GitRevisionNumber;
import git4idea.GitUtil;
import git4idea.changes.GitChangeUtils;
import git4idea.commands.Git;
import git4idea.commands.GitCommandResult;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* {@link DiffFromHistoryHandler#showDiffForTwo(FilePath, VcsFileRevision, VcsFileRevision) "Show Diff" for 2 revision} calls the common code.
* {@link DiffFromHistoryHandler#showDiffForOne(com.intellij.openapi.actionSystem.AnActionEvent, com.intellij.openapi.vcs.FilePath, com.intellij.openapi.vcs.history.VcsFileRevision, com.intellij.openapi.vcs.history.VcsFileRevision) "Show diff" for 1 revision}
* behaves differently for merge commits: for them it shown a popup displaying the parents of the selected commit. Selecting a parent
* from the popup shows the difference with this parent.
* If an ordinary (not merge) revision with 1 parent, it is the same as usual: just compare with the parent;
*
* @author Kirill Likhodedov
*/
public class GitDiffFromHistoryHandler extends BaseDiffFromHistoryHandler<GitFileRevision> {
private static final Logger LOG = Logger.getInstance(GitDiffFromHistoryHandler.class);
@NotNull private final Git myGit;
@NotNull private final GitRepositoryManager myRepositoryManager;
public GitDiffFromHistoryHandler(@NotNull Project project) {
super(project);
myGit = ServiceManager.getService(project, Git.class);
myRepositoryManager = GitUtil.getRepositoryManager(project);
}
@Override
public void showDiffForOne(@NotNull AnActionEvent e,
@NotNull FilePath filePath,
@NotNull VcsFileRevision previousRevision,
@NotNull VcsFileRevision revision) {
GitFileRevision rev = (GitFileRevision)revision;
Collection<String> parents = rev.getParents();
if (parents.size() < 2) {
super.showDiffForOne(e, filePath, previousRevision, revision);
}
else { // merge
showDiffForMergeCommit(e, filePath, rev, parents);
}
}
@NotNull
@Override
protected List<Change> getChangesBetweenRevisions(@NotNull FilePath path, @NotNull GitFileRevision rev1, @Nullable GitFileRevision rev2)
throws VcsException {
GitRepository repository = getRepository(path);
String hash1 = rev1.getHash();
String hash2 = rev2 != null ? rev2.getHash() : null;
return ContainerUtil
.newArrayList(GitChangeUtils.getDiff(repository.getProject(), repository.getRoot(), hash1, hash2, Collections.singletonList(path)));
}
@NotNull
@Override
protected List<Change> getAffectedChanges(@NotNull FilePath path, @NotNull GitFileRevision rev) throws VcsException {
GitRepository repository = getRepository(path);
return ContainerUtil.newArrayList(
GitChangeUtils.getRevisionChanges(repository.getProject(), repository.getRoot(), rev.getHash(), false, true, true).getChanges());
}
@NotNull
@Override
protected String getPresentableName(@NotNull GitFileRevision revision) {
return DvcsUtil.getShortHash(revision.getHash());
}
@NotNull
private GitRepository getRepository(@NotNull FilePath path) {
GitRepository repository = myRepositoryManager.getRepositoryForFile(path);
LOG.assertTrue(repository != null, "Repository is null for " + path);
return repository;
}
private void showDiffForMergeCommit(@NotNull final AnActionEvent event, @NotNull final FilePath filePath,
@NotNull final GitFileRevision rev, @NotNull final Collection<String> parents) {
checkIfFileWasTouchedAndFindParentsInBackground(filePath, rev, parents, new Consumer<MergeCommitPreCheckInfo>() {
@Override
public void consume(MergeCommitPreCheckInfo info) {
if (!info.wasFileTouched()) {
String message = String.format("There were no changes in %s in this merge commit, besides those which were made in both branches",
filePath.getName());
VcsBalloonProblemNotifier.showOverVersionControlView(GitDiffFromHistoryHandler.this.myProject, message, MessageType.INFO);
}
showPopup(event, rev, filePath, info.getParents());
}
});
}
private static class MergeCommitPreCheckInfo {
private final boolean myWasFileTouched;
private final Collection<GitFileRevision> myParents;
private MergeCommitPreCheckInfo(boolean touched, Collection<GitFileRevision> parents) {
myWasFileTouched = touched;
myParents = parents;
}
public boolean wasFileTouched() {
return myWasFileTouched;
}
public Collection<GitFileRevision> getParents() {
return myParents;
}
}
private void checkIfFileWasTouchedAndFindParentsInBackground(@NotNull final FilePath filePath, @NotNull final GitFileRevision rev,
@NotNull final Collection<String> parentHashes,
@NotNull final Consumer<MergeCommitPreCheckInfo> resultHandler) {
new Task.Backgroundable(myProject, "Loading changes...", false) {
private MergeCommitPreCheckInfo myInfo;
@Override public void run(@NotNull ProgressIndicator indicator) {
try {
GitRepository repository = getRepository(filePath);
boolean fileTouched = wasFileTouched(repository, rev);
Collection<GitFileRevision> parents = findParentRevisions(repository, rev, parentHashes);
myInfo = new MergeCommitPreCheckInfo(fileTouched, parents);
}
catch (VcsException e) {
String logMessage = "Error happened while executing git show " + rev + ":" + filePath;
showError(e, logMessage);
}
}
@Override
public void onSuccess() {
if (myInfo != null) { // if info == null => an exception happened
resultHandler.consume(myInfo);
}
}
}.queue();
}
@NotNull
private Collection<GitFileRevision> findParentRevisions(@NotNull GitRepository repository, @NotNull GitFileRevision currentRevision,
@NotNull Collection<String> parentHashes) throws VcsException {
// currentRevision is a merge revision.
// the file could be renamed in one of the branches, i.e. the name in one of the parent revisions may be different from the name
// in currentRevision. It can be different even in both parents, but it would a rename-rename conflict, and we don't handle such anyway.
Collection<GitFileRevision> parents = new ArrayList<GitFileRevision>(parentHashes.size());
for (String parentHash : parentHashes) {
parents.add(createParentRevision(repository, currentRevision, parentHash));
}
return parents;
}
@NotNull
private GitFileRevision createParentRevision(@NotNull GitRepository repository, @NotNull GitFileRevision currentRevision,
@NotNull String parentHash) throws VcsException {
FilePath currentRevisionPath = currentRevision.getPath();
if (currentRevisionPath.isDirectory()) {
// for directories the history doesn't follow renames
return makeRevisionFromHash(currentRevisionPath, parentHash);
}
// can't limit by the path: in that case rename information will be missed
Collection<Change> changes = GitChangeUtils.getDiff(myProject, repository.getRoot(), parentHash, currentRevision.getHash(), null);
for (Change change : changes) {
ContentRevision afterRevision = change.getAfterRevision();
ContentRevision beforeRevision = change.getBeforeRevision();
if (afterRevision != null && afterRevision.getFile().equals(currentRevisionPath)) {
// if the file was renamed, taking the path how it was in the parent; otherwise the path didn't change
FilePath path = (beforeRevision != null ? beforeRevision.getFile() : afterRevision.getFile());
return new GitFileRevision(myProject, path, new GitRevisionNumber(parentHash));
}
}
LOG.error(String.format("Could not find parent revision. Will use the path from parent revision. Current revision: %s, parent hash: %s",
currentRevision, parentHash));
return makeRevisionFromHash(currentRevisionPath, parentHash);
}
private void showPopup(@NotNull AnActionEvent event, @NotNull GitFileRevision rev, @NotNull FilePath filePath,
@NotNull Collection<GitFileRevision> parents) {
ActionGroup parentActions = createActionGroup(rev, filePath, parents);
DataContext dataContext = SimpleDataContext.getProjectContext(myProject);
ListPopup popup = JBPopupFactory.getInstance().createActionGroupPopup("Choose parent to compare", parentActions, dataContext,
JBPopupFactory.ActionSelectionAid.NUMBERING, true);
showPopupInBestPosition(popup, event, dataContext);
}
private static void showPopupInBestPosition(@NotNull ListPopup popup, @NotNull AnActionEvent event, @NotNull DataContext dataContext) {
if (event.getInputEvent() instanceof MouseEvent) {
if (!event.getPlace().equals(ActionPlaces.UPDATE_POPUP)) {
popup.show(new RelativePoint((MouseEvent)event.getInputEvent()));
}
else { // quick fix for invoking from the context menu: coordinates are calculated incorrectly there.
popup.showInBestPositionFor(dataContext);
}
}
else {
popup.showInBestPositionFor(dataContext);
}
}
@NotNull
private ActionGroup createActionGroup(@NotNull GitFileRevision rev, @NotNull FilePath filePath, @NotNull Collection<GitFileRevision> parents) {
Collection<AnAction> actions = new ArrayList<AnAction>(2);
for (GitFileRevision parent : parents) {
actions.add(createParentAction(rev, filePath, parent));
}
return new DefaultActionGroup(ArrayUtil.toObjectArray(actions, AnAction.class));
}
@NotNull
private AnAction createParentAction(@NotNull GitFileRevision rev, @NotNull FilePath filePath, @NotNull GitFileRevision parent) {
return new ShowDiffWithParentAction(filePath, rev, parent);
}
@NotNull
private GitFileRevision makeRevisionFromHash(@NotNull FilePath filePath, @NotNull String hash) {
return new GitFileRevision(myProject, filePath, new GitRevisionNumber(hash));
}
private boolean wasFileTouched(@NotNull GitRepository repository, @NotNull GitFileRevision rev) throws VcsException {
GitCommandResult result = myGit.show(repository, rev.getHash());
if (result.success()) {
return isFilePresentInOutput(repository, rev.getPath(), result.getOutput());
}
throw new VcsException(result.getErrorOutputAsJoinedString());
}
private static boolean isFilePresentInOutput(@NotNull GitRepository repository, @NotNull FilePath path, @NotNull List<String> output) {
String relativePath = getRelativePath(repository, path);
for (String line : output) {
if (line.startsWith("---") || line.startsWith("+++")) {
if (line.contains(relativePath)) {
return true;
}
}
}
return false;
}
@Nullable
private static String getRelativePath(@NotNull GitRepository repository, @NotNull FilePath path) {
return FileUtil.getRelativePath(repository.getRoot().getPath(), path.getPath(), '/');
}
private class ShowDiffWithParentAction extends AnAction {
@NotNull private final FilePath myFilePath;
@NotNull private final GitFileRevision myRevision;
@NotNull private final GitFileRevision myParentRevision;
public ShowDiffWithParentAction(@NotNull FilePath filePath, @NotNull GitFileRevision rev, @NotNull GitFileRevision parent) {
super(DvcsUtil.getShortHash(parent.getHash()));
myFilePath = filePath;
myRevision = rev;
myParentRevision = parent;
}
@Override
public void actionPerformed(AnActionEvent e) {
doShowDiff(myFilePath, myParentRevision, myRevision, false);
}
}
}