blob: c372b09805a17435d5b06ee0a4715fc3d407a423 [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 org.jetbrains.plugins.github;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
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.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vcs.ProjectLevelVcsManager;
import com.intellij.openapi.vcs.VcsDataKeys;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.changes.ui.SelectFilesDialog;
import com.intellij.openapi.vcs.ui.CommitMessage;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ThrowableConvertor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashSet;
import com.intellij.vcsUtil.VcsFileUtil;
import git4idea.DialogManager;
import git4idea.GitLocalBranch;
import git4idea.GitUtil;
import git4idea.actions.BasicAction;
import git4idea.actions.GitInit;
import git4idea.commands.*;
import git4idea.i18n.GitBundle;
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
import git4idea.util.GitFileUtils;
import git4idea.util.GitUIUtil;
import icons.GithubIcons;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.github.api.GithubApiUtil;
import org.jetbrains.plugins.github.api.GithubConnection;
import org.jetbrains.plugins.github.api.GithubRepo;
import org.jetbrains.plugins.github.api.GithubUserDetailed;
import org.jetbrains.plugins.github.ui.GithubShareDialog;
import org.jetbrains.plugins.github.util.*;
import javax.swing.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static org.jetbrains.plugins.github.util.GithubUtil.setVisibleEnabled;
/**
* @author oleg
*/
public class GithubShareAction extends DumbAwareAction {
private static final Logger LOG = GithubUtil.LOG;
public GithubShareAction() {
super("Share Project on GitHub", "Easily share project on GitHub", GithubIcons.Github_icon);
}
public void update(AnActionEvent e) {
final Project project = e.getData(CommonDataKeys.PROJECT);
if (project == null || project.isDefault()) {
setVisibleEnabled(e, false, false);
return;
}
setVisibleEnabled(e, true, true);
}
// get gitRepository
// check for existing git repo
// check available repos and privateRepo access (net)
// Show dialog (window)
// create GitHub repo (net)
// create local git repo (if not exist)
// add GitHub as a remote host
// make first commit
// push everything (net)
@Override
public void actionPerformed(final AnActionEvent e) {
final Project project = e.getData(CommonDataKeys.PROJECT);
final VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE);
if (project == null || project.isDisposed() || !GithubUtil.testGitExecutable(project)) {
return;
}
shareProjectOnGithub(project, file);
}
public static void shareProjectOnGithub(@NotNull final Project project, @Nullable final VirtualFile file) {
BasicAction.saveAll();
// get gitRepository
final GitRepository gitRepository = GithubUtil.getGitRepository(project, file);
final boolean gitDetected = gitRepository != null;
final VirtualFile root = gitDetected ? gitRepository.getRoot() : project.getBaseDir();
// check for existing git repo
boolean externalRemoteDetected = false;
if (gitDetected) {
final String githubRemote = GithubUtil.findGithubRemoteUrl(gitRepository);
if (githubRemote != null) {
GithubNotifications.showInfoURL(project, "Project is already on GitHub", "GitHub", githubRemote);
return;
}
externalRemoteDetected = !gitRepository.getRemotes().isEmpty();
}
final GithubAuthDataHolder authHolder = GithubAuthDataHolder.createFromSettings();
// get available GitHub repos with modal progress
final GithubInfo githubInfo = loadGithubInfoWithModal(authHolder, project);
if (githubInfo == null) {
return;
}
// Show dialog (window)
final GithubShareDialog shareDialog =
new GithubShareDialog(project, githubInfo.getRepositoryNames(), githubInfo.getUser().canCreatePrivateRepo());
DialogManager.show(shareDialog);
if (!shareDialog.isOK()) {
return;
}
final boolean isPrivate = shareDialog.isPrivate();
final String name = shareDialog.getRepositoryName();
final String description = shareDialog.getDescription();
// finish the job in background
final boolean finalExternalRemoteDetected = externalRemoteDetected;
new Task.Backgroundable(project, "Sharing project on GitHub...") {
@Override
public void run(@NotNull ProgressIndicator indicator) {
// create GitHub repo (network)
LOG.info("Creating GitHub repository");
indicator.setText("Creating GitHub repository...");
final String url = createGithubRepository(project, authHolder, indicator, name, description, isPrivate);
if (url == null) {
return;
}
LOG.info("Successfully created GitHub repository");
// creating empty git repo if git is not initialized
LOG.info("Binding local project with GitHub");
if (!gitDetected) {
LOG.info("No git detected, creating empty git repo");
indicator.setText("Creating empty git repo...");
if (!createEmptyGitRepository(project, root, indicator)) {
return;
}
}
GitRepositoryManager repositoryManager = GitUtil.getRepositoryManager(project);
final GitRepository repository = repositoryManager.getRepositoryForRoot(root);
LOG.assertTrue(repository != null, "GitRepository is null for root " + root);
if (repository == null) {
GithubNotifications.showError(project, "Failed to create GitHub Repository", "Can't find Git repository");
return;
}
final String remoteUrl = GithubUrlUtil.getCloneUrl(githubInfo.getUser().getLogin(), name);
final String remoteName = finalExternalRemoteDetected ? "github" : "origin";
//git remote add origin git@github.com:login/name.git
LOG.info("Adding GitHub as a remote host");
indicator.setText("Adding GitHub as a remote host...");
if (!GithubUtil.addGithubRemote(project, repository, remoteName, remoteUrl)) {
return;
}
// create sample commit for binding project
if (!performFirstCommitIfRequired(project, root, repository, indicator, name, url)) {
return;
}
//git push origin master
LOG.info("Pushing to github master");
indicator.setText("Pushing to github master...");
if (!pushCurrentBranch(project, repository, remoteName, remoteUrl, name, url)) {
return;
}
GithubNotifications.showInfoURL(project, "Successfully shared project on GitHub", name, url);
}
}.queue();
}
@Nullable
private static GithubInfo loadGithubInfoWithModal(@NotNull final GithubAuthDataHolder authHolder, @NotNull final Project project) {
try {
return GithubUtil
.computeValueInModal(project, "Access to GitHub", new ThrowableConvertor<ProgressIndicator, GithubInfo, IOException>() {
@NotNull
@Override
public GithubInfo convert(ProgressIndicator indicator) throws IOException {
// get existing github repos (network) and validate auth data
return GithubUtil.runTask(project, authHolder, indicator, new ThrowableConvertor<GithubConnection, GithubInfo, IOException>() {
@NotNull
@Override
public GithubInfo convert(@NotNull GithubConnection connection) throws IOException {
// check access to private repos (network)
GithubUserDetailed userInfo = GithubApiUtil.getCurrentUserDetailed(connection);
HashSet<String> names = new HashSet<String>();
for (GithubRepo info : GithubApiUtil.getUserRepos(connection)) {
names.add(info.getName());
}
return new GithubInfo(userInfo, names);
}
});
}
});
}
catch (IOException e) {
GithubNotifications.showErrorDialog(project, "Failed to connect to GitHub", e);
return null;
}
}
@Nullable
private static String createGithubRepository(@NotNull Project project,
@NotNull GithubAuthDataHolder authHolder,
@NotNull ProgressIndicator indicator,
@NotNull final String name,
@NotNull final String description,
final boolean isPrivate) {
try {
return GithubUtil.runTask(project, authHolder, indicator, new ThrowableConvertor<GithubConnection, GithubRepo, IOException>() {
@NotNull
@Override
public GithubRepo convert(@NotNull GithubConnection connection) throws IOException {
return GithubApiUtil.createRepo(connection, name, description, isPrivate);
}
}).getHtmlUrl();
}
catch (IOException e) {
GithubNotifications.showError(project, "Failed to create GitHub Repository", e);
return null;
}
}
private static boolean createEmptyGitRepository(@NotNull Project project,
@NotNull VirtualFile root,
@NotNull ProgressIndicator indicator) {
final GitLineHandler h = new GitLineHandler(project, root, GitCommand.INIT);
h.setStdoutSuppressed(false);
GitHandlerUtil.runInCurrentThread(h, indicator, true, GitBundle.getString("initializing.title"));
if (!h.errors().isEmpty()) {
GitUIUtil.showOperationErrors(project, h.errors(), "git init");
LOG.info("Failed to create empty git repo: " + h.errors());
return false;
}
GitInit.refreshAndConfigureVcsMappings(project, root, root.getPath());
return true;
}
private static boolean performFirstCommitIfRequired(@NotNull final Project project,
@NotNull VirtualFile root,
@NotNull GitRepository repository,
@NotNull ProgressIndicator indicator,
@NotNull String name,
@NotNull String url) {
// check if there is no commits
if (!repository.isFresh()) {
return true;
}
LOG.info("Trying to commit");
try {
LOG.info("Adding files for commit");
indicator.setText("Adding files to git...");
// ask for files to add
final List<VirtualFile> trackedFiles = ChangeListManager.getInstance(project).getAffectedFiles();
final Collection<VirtualFile> untrackedFiles =
filterOutIgnored(project, repository.getUntrackedFilesHolder().retrieveUntrackedFiles());
trackedFiles.removeAll(untrackedFiles); // fix IDEA-119855
final List<VirtualFile> allFiles = new ArrayList<VirtualFile>();
allFiles.addAll(trackedFiles);
allFiles.addAll(untrackedFiles);
final Ref<GithubUntrackedFilesDialog> dialogRef = new Ref<GithubUntrackedFilesDialog>();
ApplicationManager.getApplication().invokeAndWait(new Runnable() {
@Override
public void run() {
GithubUntrackedFilesDialog dialog = new GithubUntrackedFilesDialog(project, allFiles);
if (!trackedFiles.isEmpty()) {
dialog.setSelectedFiles(trackedFiles);
}
DialogManager.show(dialog);
dialogRef.set(dialog);
}
}, indicator.getModalityState());
final GithubUntrackedFilesDialog dialog = dialogRef.get();
final Collection<VirtualFile> files2commit = dialog.getSelectedFiles();
if (!dialog.isOK() || files2commit.isEmpty()) {
GithubNotifications.showInfoURL(project, "Successfully created empty repository on GitHub", name, url);
return false;
}
Collection<VirtualFile> files2add = ContainerUtil.intersection(untrackedFiles, files2commit);
Collection<VirtualFile> files2rm = ContainerUtil.subtract(trackedFiles, files2commit);
Collection<VirtualFile> modified = new HashSet<VirtualFile>(trackedFiles);
modified.addAll(files2commit);
GitFileUtils.addFiles(project, root, files2add);
GitFileUtils.deleteFilesFromCache(project, root, files2rm);
// commit
LOG.info("Performing commit");
indicator.setText("Performing commit...");
GitSimpleHandler handler = new GitSimpleHandler(project, root, GitCommand.COMMIT);
handler.setStdoutSuppressed(false);
handler.addParameters("-m", dialog.getCommitMessage());
handler.endOptions();
handler.run();
VcsFileUtil.refreshFiles(project, modified);
}
catch (VcsException e) {
LOG.warn(e);
GithubNotifications.showErrorURL(project, "Can't finish GitHub sharing process", "Successfully created project ", "'" + name + "'",
" on GitHub, but initial commit failed:<br/>" + GithubUtil.getErrorTextFromException(e), url);
return false;
}
LOG.info("Successfully created initial commit");
return true;
}
@NotNull
private static Collection<VirtualFile> filterOutIgnored(@NotNull Project project, @NotNull Collection<VirtualFile> files) {
final ChangeListManager changeListManager = ChangeListManager.getInstance(project);
final ProjectLevelVcsManager vcsManager = ProjectLevelVcsManager.getInstance(project);
return ContainerUtil.filter(files, new Condition<VirtualFile>() {
@Override
public boolean value(VirtualFile file) {
return !changeListManager.isIgnoredFile(file) && !vcsManager.isIgnored(file);
}
});
}
private static boolean pushCurrentBranch(@NotNull Project project,
@NotNull GitRepository repository,
@NotNull String remoteName,
@NotNull String remoteUrl,
@NotNull String name,
@NotNull String url) {
Git git = ServiceManager.getService(Git.class);
GitLocalBranch currentBranch = repository.getCurrentBranch();
if (currentBranch == null) {
GithubNotifications.showErrorURL(project, "Can't finish GitHub sharing process", "Successfully created project ", "'" + name + "'",
" on GitHub, but initial push failed: no current branch", url);
return false;
}
GitCommandResult result = git.push(repository, remoteName, remoteUrl, currentBranch.getName(), true);
if (!result.success()) {
GithubNotifications.showErrorURL(project, "Can't finish GitHub sharing process", "Successfully created project ", "'" + name + "'",
" on GitHub, but initial push failed:<br/>" + result.getErrorOutputAsHtmlString(), url);
return false;
}
return true;
}
public static class GithubUntrackedFilesDialog extends SelectFilesDialog implements TypeSafeDataProvider {
@NotNull private final Project myProject;
private CommitMessage myCommitMessagePanel;
public GithubUntrackedFilesDialog(@NotNull Project project, @NotNull List<VirtualFile> untrackedFiles) {
super(project, untrackedFiles, null, null, true, false, false);
myProject = project;
setTitle("Add Files For Initial Commit");
init();
}
@Override
protected JComponent createNorthPanel() {
return null;
}
@Override
protected JComponent createCenterPanel() {
final JComponent tree = super.createCenterPanel();
myCommitMessagePanel = new CommitMessage(myProject);
myCommitMessagePanel.setCommitMessage("Initial commit");
Splitter splitter = new Splitter(true);
splitter.setHonorComponentsMinimumSize(true);
splitter.setFirstComponent(tree);
splitter.setSecondComponent(myCommitMessagePanel);
splitter.setProportion(0.7f);
return splitter;
}
@NotNull
public String getCommitMessage() {
return myCommitMessagePanel.getComment();
}
@Override
public void calcData(DataKey key, DataSink sink) {
if (key == VcsDataKeys.COMMIT_MESSAGE_CONTROL) {
sink.put(VcsDataKeys.COMMIT_MESSAGE_CONTROL, myCommitMessagePanel);
}
}
@Override
protected String getDimensionServiceKey() {
return "Github.UntrackedFilesDialog";
}
}
private static class GithubInfo {
@NotNull private final GithubUserDetailed myUser;
@NotNull private final HashSet<String> myRepositoryNames;
GithubInfo(@NotNull GithubUserDetailed user, @NotNull HashSet<String> repositoryNames) {
myUser = user;
myRepositoryNames = repositoryNames;
}
@NotNull
public GithubUserDetailed getUser() {
return myUser;
}
@NotNull
public HashSet<String> getRepositoryNames() {
return myRepositoryNames;
}
}
}