/*
* Copyright 2013 Urs Wolfer
* Copyright 2000-2010 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 com.urswolfer.intellij.plugin.gerrit.git;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.Lists;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.inject.Inject;
import com.intellij.openapi.application.Application;
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.Project;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vcs.merge.MergeDialogCustomizer;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import com.intellij.vcs.log.Hash;
import com.intellij.vcs.log.VcsShortCommitDetails;
import com.intellij.vcs.log.VcsUser;
import com.intellij.vcs.log.impl.HashImpl;
import com.intellij.vcs.log.impl.VcsShortCommitDetailsImpl;
import com.intellij.vcs.log.impl.VcsUserImpl;
import com.urswolfer.intellij.plugin.gerrit.GerritSettings;
import com.urswolfer.intellij.plugin.gerrit.util.NotificationBuilder;
import com.urswolfer.intellij.plugin.gerrit.util.NotificationService;
import com.urswolfer.intellij.plugin.gerrit.util.UrlUtils;
import git4idea.*;
import git4idea.commands.*;
import git4idea.history.GitHistoryUtils;
import git4idea.merge.GitConflictResolver;
import git4idea.repo.GitRemote;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
import git4idea.reset.GitResetMode;
import git4idea.update.GitFetchResult;
import git4idea.util.GitCommitCompareInfo;
import git4idea.util.UntrackedFilesNotifier;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicReference;
import static git4idea.commands.GitSimpleEventDetector.Event.CHERRY_PICK_CONFLICT;
import static git4idea.commands.GitSimpleEventDetector.Event.LOCAL_CHANGES_OVERWRITTEN_BY_CHERRY_PICK;
/**
* @author Urs Wolfer
*/
public class GerritGitUtil {
@Inject
private Logger log;
@Inject
private Git git;
@Inject
private GitPlatformFacade platformFacade;
@Inject
private FileDocumentManager fileDocumentManager;
@Inject
private Application application;
@Inject
private VirtualFileManager virtualFileManager;
@Inject
private GerritSettings gerritSettings;
@Inject
private NotificationService notificationService;
public Iterable<GitRepository> getRepositories(Project project) {
GitRepositoryManager repositoryManager = GitUtil.getRepositoryManager(project);
return repositoryManager.getRepositories();
}
public Optional<GitRepository> getRepositoryForGerritProject(Project project, String gerritProjectName) {
final Iterable<GitRepository> repositoriesFromRoots = getRepositories(project);
for (GitRepository repository : repositoriesFromRoots) {
for (GitRemote remote : repository.getRemotes()) {
if (remote.getName().equals(gerritProjectName)) {
return Optional.of(repository);
}
for (String remoteUrl : remote.getUrls()) {
remoteUrl = UrlUtils.stripGitExtension(remoteUrl);
if (remoteUrl != null && remoteUrl.endsWith(gerritProjectName)) {
return Optional.of(repository);
}
}
}
}
return Optional.absent();
}
public void fetchChange(final Project project,
final GitRepository gitRepository,
final String url,
final String branch,
@Nullable final Callable<Void> successCallable) {
GitVcs.runInBackground(new Task.Backgroundable(project, "Fetching...", false) {
@Override
public void onSuccess() {
super.onSuccess();
try {
if (successCallable != null) {
successCallable.call();
}
} catch (Exception e) {
throw Throwables.propagate(e);
}
}
@Override
public void run(@NotNull ProgressIndicator indicator) {
for (GitRemote remote : gitRepository.getRemotes()) {
for (String repositoryUrl : remote.getUrls()) {
if (UrlUtils.urlHasSameHost(repositoryUrl, url)
|| UrlUtils.urlHasSameHost(repositoryUrl, gerritSettings.getHost())) {
fetchNatively(gitRepository.getGitDir(), remote, repositoryUrl, branch, project, indicator);
return;
}
}
}
NotificationBuilder notification = new NotificationBuilder(project, "Error",
String.format("Could not fetch commit because no remote url matches Gerrit host.<br/>" +
"Git repository: '%s'.", gitRepository.getPresentableUrl()));
notificationService.notifyError(notification);
}
});
}
public void cherryPickChange(final Project project, final ChangeInfo changeInfo, final String revisionId) {
fileDocumentManager.saveAllDocuments();
platformFacade.getChangeListManager(project).blockModalNotifications();
new Task.Backgroundable(project, "Cherry-picking...", false) {
public void run(@NotNull ProgressIndicator indicator) {
try {
Optional<GitRepository> gitRepositoryOptional = getRepositoryForGerritProject(project, changeInfo.project);
if (!gitRepositoryOptional.isPresent()) {
NotificationBuilder notification = new NotificationBuilder(project, "Error",
String.format("No repository found for Gerrit project: '%s'.", changeInfo.project));
notificationService.notifyError(notification);
return;
}
GitRepository gitRepository = gitRepositoryOptional.get();
final VirtualFile virtualFile = gitRepository.getGitDir();
final String notLoaded = "Not loaded";
VcsUser notLoadedUser = new VcsUserImpl(notLoaded, notLoaded);
VcsShortCommitDetails gitCommit = new VcsShortCommitDetailsImpl(
HashImpl.build(revisionId), Collections.<Hash>emptyList(), 0, virtualFile, notLoaded, notLoadedUser, notLoadedUser, 0);
cherryPick(gitRepository, gitCommit, git, platformFacade, project);
} finally {
application.invokeLater(new Runnable() {
public void run() {
virtualFileManager.syncRefresh();
platformFacade.getChangeListManager(project).unblockModalNotifications();
}
});
}
}
}.queue();
}
/**
* A lot of this code is based on: git4idea.cherrypick.GitCherryPicker#cherryPick() (which is private)
*/
private boolean cherryPick(@NotNull GitRepository repository, @NotNull VcsShortCommitDetails commit,
@NotNull Git git, @NotNull GitPlatformFacade platformFacade, @NotNull Project project) {
GitSimpleEventDetector conflictDetector = new GitSimpleEventDetector(CHERRY_PICK_CONFLICT);
GitSimpleEventDetector localChangesOverwrittenDetector = new GitSimpleEventDetector(LOCAL_CHANGES_OVERWRITTEN_BY_CHERRY_PICK);
GitUntrackedFilesOverwrittenByOperationDetector untrackedFilesDetector =
new GitUntrackedFilesOverwrittenByOperationDetector(repository.getRoot());
GitCommandResult result = git.cherryPick(repository, commit.getId().asString(), false,
conflictDetector, localChangesOverwrittenDetector, untrackedFilesDetector);
if (result.success()) {
return true;
} else if (conflictDetector.hasHappened()) {
return new CherryPickConflictResolver(project, git, platformFacade, repository.getRoot(),
commit.getId().toShortString(), commit.getAuthor().getName(),
commit.getSubject()).merge();
} else if (untrackedFilesDetector.wasMessageDetected()) {
String description = "Some untracked working tree files would be overwritten by cherry-pick.<br/>" +
"Please move, remove or add them before you can cherry-pick. <a href='view'>View them</a>";
UntrackedFilesNotifier.notifyUntrackedFilesOverwrittenBy(project, repository.getRoot(),
untrackedFilesDetector.getRelativeFilePaths(),
"cherry-pick", description);
return false;
} else if (localChangesOverwrittenDetector.hasHappened()) {
notificationService.notifyError(new NotificationBuilder(project, "Cherry-Pick Error",
"Your local changes would be overwritten by cherry-pick.<br/>Commit your changes or stash them to proceed."));
return false;
} else {
notificationService.notifyError(new NotificationBuilder(project, "Cherry-Pick Error",
result.getErrorOutputAsHtmlString()));
return false;
}
}
/**
* Copy of: git4idea.cherrypick.GitCherryPicker.CherryPickConflictResolver (which is private)
*/
private static class CherryPickConflictResolver extends GitConflictResolver {
public CherryPickConflictResolver(@NotNull Project project, @NotNull Git git, @NotNull GitPlatformFacade facade, @NotNull VirtualFile root,
@NotNull String commitHash, @NotNull String commitAuthor, @NotNull String commitMessage) {
super(project, git, facade, Collections.singleton(root), makeParams(commitHash, commitAuthor, commitMessage));
}
private static Params makeParams(String commitHash, String commitAuthor, String commitMessage) {
Params params = new Params();
params.setErrorNotificationTitle("Cherry-picked with conflicts");
params.setMergeDialogCustomizer(new CherryPickMergeDialogCustomizer(commitHash, commitAuthor, commitMessage));
return params;
}
@Override
protected void notifyUnresolvedRemain() {
// we show a [possibly] compound notification after cherry-picking all commits.
}
}
/**
* Copy of: git4idea.cherrypick.GitCherryPicker.CherryPickMergeDialogCustomizer (which is private)
*/
private static class CherryPickMergeDialogCustomizer extends MergeDialogCustomizer {
private String myCommitHash;
private String myCommitAuthor;
private String myCommitMessage;
public CherryPickMergeDialogCustomizer(String commitHash, String commitAuthor, String commitMessage) {
myCommitHash = commitHash;
myCommitAuthor = commitAuthor;
myCommitMessage = commitMessage;
}
@Override
public String getMultipleFileMergeDescription(Collection<VirtualFile> files) {
return "<html>Conflicts during cherry-picking commit <code>" + myCommitHash + "</code> made by " + myCommitAuthor + "<br/>" +
"<code>\"" + myCommitMessage + "\"</code></html>";
}
@Override
public String getLeftPanelTitle(VirtualFile file) {
return "Local changes";
}
@Override
public String getRightPanelTitle(VirtualFile file, VcsRevisionNumber lastRevisionNumber) {
return "<html>Changes from cherry-pick <code>" + myCommitHash + "</code>";
}
}
@NotNull
public GitFetchResult fetchNatively(@NotNull VirtualFile root,
@NotNull GitRemote remote,
@NotNull String url,
@Nullable String branch,
Project project,
ProgressIndicator progressIndicator) {
final GitLineHandler h = new GitLineHandler(project, root, GitCommand.FETCH);
h.setUrl(url);
h.addProgressParameter();
String remoteName = remote.getName();
h.addParameters(remoteName);
if (branch != null) {
h.addParameters(branch);
}
final GitTask fetchTask = new GitTask(project, h, "Fetching " + remote.getFirstUrl());
fetchTask.setProgressIndicator(progressIndicator);
fetchTask.setProgressAnalyzer(new GitStandardProgressAnalyzer());
final AtomicReference<GitFetchResult> result = new AtomicReference<GitFetchResult>();
fetchTask.execute(true, false, new GitTaskResultHandlerAdapter() {
@Override
protected void onSuccess() {
result.set(GitFetchResult.success());
}
@Override
protected void onCancel() {
log.info("Cancelled fetch.");
result.set(GitFetchResult.cancel());
}
@Override
protected void onFailure() {
log.warn("Error fetching: " + h.errors());
Collection<Exception> errors = Lists.newArrayList();
errors.addAll(h.errors());
result.set(GitFetchResult.error(errors));
}
});
return result.get();
}
@NotNull
public Pair<List<GitCommit>, List<GitCommit>> loadCommitsToCompare(@NotNull GitRepository repository, @NotNull final String branchName, @NotNull final Project project) {
final List<GitCommit> headToBranch;
final List<GitCommit> branchToHead;
try {
headToBranch = GitHistoryUtils.history(project, repository.getRoot(), ".." + branchName);
branchToHead = GitHistoryUtils.history(project, repository.getRoot(), branchName + "..");
} catch (VcsException e) {
// we treat it as critical and report an error
throw new GitExecutionException("Couldn't get [git log .." + branchName + "] on repository [" + repository.getRoot() + "]", e);
}
return Pair.create(headToBranch, branchToHead);
}
@NotNull
public GitCommitCompareInfo loadCommitsToCompare(Collection<GitRepository> repositories, String branchName, @NotNull final Project project) {
GitCommitCompareInfo compareInfo = new GitCommitCompareInfo();
for (GitRepository repository : repositories) {
compareInfo.put(repository, loadCommitsToCompare(repository, branchName, project));
// compareInfo.put(repository, loadTotalDiff(repository, branchName));
}
return compareInfo;
}
public boolean checkoutNewBranch(GitRepository repository, String branch) throws VcsException {
FormattedGitLineHandlerListener listener = new FormattedGitLineHandlerListener();
GitCommandResult gitCommandResult = git.checkout(repository, "FETCH_HEAD", branch, false, listener);
if (gitCommandResult.success()) {
return true;
} else if (gitCommandResult.getErrorOutputAsJoinedString().contains("already exists")){
return false;
} else {
throw new VcsException(listener.getHtmlMessage());
}
}
private static class FormattedGitLineHandlerListener implements GitLineHandlerListener {
private List<String> messages = new ArrayList<String>();
@Override
public void onLineAvailable(String s, Key key) {
if ( s.startsWith("\t") ) {
s = "<b>" + s.substring(1) + "</b>";
}
messages.add(s);
}
@Override
public void processTerminated(int i) {
}
@Override
public void startFailed(Throwable throwable) {
}
public String getHtmlMessage() {
return StringUtil.join(messages, "<br/>");
}
}
}