/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2014 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 2014 Sun Microsystems, Inc. */ package com.junichi11.netbeans.modules.github.issues.issue; import com.junichi11.netbeans.modules.github.issues.GitHubCache; import com.junichi11.netbeans.modules.github.issues.GitHubIssues; import com.junichi11.netbeans.modules.github.issues.issue.ui.CommentTabbedPanel; import com.junichi11.netbeans.modules.github.issues.issue.ui.CommentsPanel; import com.junichi11.netbeans.modules.github.issues.issue.ui.CreatePullRequestPanel; import com.junichi11.netbeans.modules.github.issues.issue.ui.GitHubIssuePanel; import com.junichi11.netbeans.modules.github.issues.repository.GitHubRepository; import com.junichi11.netbeans.modules.github.issues.utils.StringUtils; import com.junichi11.netbeans.modules.github.issues.utils.UiUtils; import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.JComponent; import javax.swing.SwingUtilities; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.eclipse.egit.github.core.Comment; import org.eclipse.egit.github.core.Issue; import org.eclipse.egit.github.core.Milestone; import org.eclipse.egit.github.core.PullRequest; import org.eclipse.egit.github.core.PullRequestMarker; import org.eclipse.egit.github.core.Repository; import org.eclipse.egit.github.core.RepositoryBranch; import org.eclipse.egit.github.core.RepositoryCommitCompare; import org.eclipse.egit.github.core.User; import org.eclipse.egit.github.core.client.GitHubClient; import org.eclipse.egit.github.core.service.RepositoryService; import org.netbeans.api.progress.BaseProgressUtils; import org.netbeans.modules.bugtracking.spi.IssueController; import org.openide.DialogDisplayer; import org.openide.NotifyDescriptor; import org.openide.awt.StatusDisplayer; import org.openide.util.Exceptions; import org.openide.util.HelpCtx; import org.openide.util.NbBundle; import org.openide.util.RequestProcessor; /** * * @author junichi11 */ public class GitHubIssueController implements IssueController, ChangeListener, PropertyChangeListener { private GitHubIssuePanel panel; private String errorMessage; private final String repositoryId; public GitHubIssueController(GitHubIssue gitHubIssue) { repositoryId = gitHubIssue.getRepository().getID(); getPanel().setIssue(gitHubIssue); Runnable runnable = new Runnable() { @Override public void run() { getPanel().update(); } }; if (EventQueue.isDispatchThread()) { runnable.run(); } else { SwingUtilities.invokeLater(runnable); } } @Override public JComponent getComponent() { return getPanel(); } @Override public HelpCtx getHelpCtx() { return null; } @Override public void opened() { } @Override public void closed() { } @Override public boolean saveChanges() { return true; } @Override public boolean discardUnsavedChanges() { return true; } @Override public boolean isChanged() { return false; } @Override public void addPropertyChangeListener(PropertyChangeListener listener) { getPanel().getIssue().addPropertyChangeListener(listener); } @Override public void removePropertyChangeListener(PropertyChangeListener listener) { getPanel().getIssue().removePropertyChangeListener(listener); } private GitHubIssuePanel getPanel() { if (panel == null) { panel = new GitHubIssuePanel(repositoryId); panel.addChangeListener(this); panel.addAction(getSubmitIssueAction()); panel.addAction(getCommentAction()); panel.addAction(getCloseReopenAction()); panel.addAction(getCreatePullRequestAction()); panel.addCommentsChangeListener(this); } return panel; } @Override public void stateChanged(ChangeEvent e) { validate(); } private void validate() { if (!isValid()) { getPanel().setErrorMessage(errorMessage); } else { getPanel().setErrorMessage(""); // NOI18N } } @NbBundle.Messages({ "GitHubIssueController.message.empty.title=Title must be set." }) private boolean isValid() { // title String title = getPanel().getTitle(); if (StringUtils.isEmpty(title)) { errorMessage = Bundle.GitHubIssueController_message_empty_title(); return false; } // everything ok errorMessage = null; return true; } private SubmitIssueAction getSubmitIssueAction() { return new SubmitIssueAction(); } private CommentAction getCommentAction() { return new CommentAction(); } private CloseReopenAction getCloseReopenAction() { return new CloseReopenAction(); } private CreatePullRequestAction getCreatePullRequestAction() { return new CreatePullRequestAction(); } @Override public void propertyChange(PropertyChangeEvent evt) { switch (evt.getPropertyName()) { case CommentsPanel.PROP_COMMENT_QUOTE: quoteComment(); break; case CommentsPanel.PROP_COMMENT_EDITED: editComment(); break; case CommentsPanel.PROP_COMMENT_DELETED: deleteComment(); break; default: break; } } private void quoteComment() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { GitHubIssuePanel p = getPanel(); String quoteComment = StringUtils.toQuoteComment(p.getQuoteComment()) + "\n"; // NOI18N p.appendNewComment(quoteComment); } }); } @NbBundle.Messages({ "GitHubIssueController.edit.comment.title=Edit Comment", "GitHubIssueController.edit.comment.fail=Can't edit this comment." }) private void editComment() { final Comment comment = getPanel().getEditedComment(); final String editedBody = CommentTabbedPanel.showDialog(Bundle.GitHubIssueController_edit_comment_title(), comment.getBody()); if (editedBody != null) { final GitHubIssue issue = getPanel().getIssue(); if (issue != null) { RequestProcessor rp = GitHubIssues.getInstance().getRequestProcessor(); rp.post(new Runnable() { @Override public void run() { Comment editedComment = issue.editComment(comment, editedBody); if (editedComment == null) { UiUtils.showErrorDialog(Bundle.GitHubIssueController_edit_comment_fail()); return; } SwingUtilities.invokeLater(new Runnable() { @Override public void run() { getPanel().loadComments(); } }); } }); } } } @NbBundle.Messages({ "GitHubIssueController.delete.comment.fail=Can't delete this issue." }) private void deleteComment() { final Comment deletedComment = getPanel().getDeletedComment(); if (deletedComment == null) { return; } RequestProcessor rp = GitHubIssues.getInstance().getRequestProcessor(); rp.post(new Runnable() { @Override public void run() { GitHubRepository repository = getPanel().getIssue().getRepository(); final boolean success = GitHubIssueSupport.deleteComment(repository, deletedComment); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { if (success) { // remove comment panel getPanel().removeDeletedComment(); } else { // show error message UiUtils.showErrorDialog(Bundle.GitHubIssueController_delete_comment_fail()); } } }); } }); } //~ inner classes public class SubmitIssueAction implements ActionListener { private SubmitIssueAction() { } @NbBundle.Messages({ "SubmitIssueAction.message.pull.request.added.fail=The pull request has not been added.", "SubmitIssueAction.message.issue.added.fail=The issue has not been added.", "SubmitIssueAction.message.issue.updated.fail=The issue has not been updated." }) @Override public void actionPerformed(ActionEvent e) { final GitHubIssuePanel p = getPanel(); p.setSubmitButtonEnabled(false); RequestProcessor rp = GitHubIssues.getInstance().getRequestProcessor(); rp.post(new Runnable() { @Override public void run() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { GitHubIssue issue = p.getIssue(); CreateIssueParams issueParams = getCreateIssueParams(issue.isNew(), p); if (issue.isNew()) { if (p.isNewPullRequestSelected()) { // add pull request // can be added only title and body to a pull request // add other than those after the pull request was created PullRequest newPullRequest = p.getNewPullRequest(); if (newPullRequest != null) { newPullRequest.setTitle(issueParams.getTitle()) .setBody(issueParams.getBody()); try { PullRequest createdPullRequest = issue.createPullRequest(newPullRequest); if (createdPullRequest != null) { issue.editIssue(issueParams); p.update(); } else { // show dialog UiUtils.showErrorDialog(Bundle.SubmitIssueAction_message_pull_request_added_fail()); } } catch (IOException ex) { UiUtils.showErrorDialog(ex.getMessage()); } } } else { // add issue Issue newIssue = issue.submitNewIssue(issueParams); if (newIssue != null) { p.update(); } else { // show dialog UiUtils.showErrorDialog(Bundle.SubmitIssueAction_message_issue_added_fail()); } } } else { // edit issue Issue editIssue = issue.editIssue(issueParams); if (editIssue != null) { p.update(); } else { // show dialog UiUtils.showErrorDialog(Bundle.SubmitIssueAction_message_issue_updated_fail()); } } p.setSubmitButtonEnabled(true); } }); } }); } private CreateIssueParams getCreateIssueParams(boolean isNew, GitHubIssuePanel p) { User assignee = p.getAssignee(); if (!isNew && assignee == null) { assignee = new User(); assignee.setLogin(""); // NOI18N } Milestone milestone = p.getMilestone(); if (milestone == null) { milestone = new Milestone(); } CreateIssueParams createIssueParams = new CreateIssueParams(p.getTitle()) .body(p.getDescription()) .milestone(milestone) .labels(p.getLabels()); if (assignee != null) { createIssueParams = createIssueParams.assignee(assignee); } return createIssueParams; } } public class CommentAction implements ActionListener { private CommentAction() { } @Override public void actionPerformed(ActionEvent e) { final GitHubIssuePanel p = getPanel(); GitHubIssues gitHubIssues = GitHubIssues.getInstance(); RequestProcessor rp = gitHubIssues.getRequestProcessor(); p.setNewCommentEnabled(false); rp.post(new Runnable() { @Override public void run() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { try { String comment = p.getNewComment(); if (StringUtils.isEmpty(comment)) { return; } comment(comment); p.update(); } finally { p.setNewCommentEnabled(true); } } }); } }); } @NbBundle.Messages({ "CommentAction.message.comment.added=Comment has been added", "CommentAction.message.comment.added.fail=Comment has not been added." }) protected Comment comment(String comment) { GitHubIssuePanel p = getPanel(); if (StringUtils.isEmpty(comment)) { return null; } Comment newComment = GitHubIssueSupport.comment(p.getIssue(), comment); if (newComment != null) { p.setNewComment(""); // NOI18N StatusDisplayer.getDefault().setStatusText(Bundle.CommentAction_message_comment_added()); } else { UiUtils.showErrorDialog(Bundle.CommentAction_message_comment_added_fail()); } return newComment; } } public class CloseReopenAction extends CommentAction { private CloseReopenAction() { } @Override public void actionPerformed(ActionEvent e) { final GitHubIssuePanel p = getPanel(); p.setNewCommentEnabled(false); GitHubIssues gitHubIssues = GitHubIssues.getInstance(); RequestProcessor rp = gitHubIssues.getRequestProcessor(); rp.post(new Runnable() { @Override public void run() { try { String comment = p.getNewComment(); if (StringUtils.isEmpty(comment)) { closeReopen(); } else { Comment newComment = comment(comment); if (newComment != null) { closeReopen(); } } SwingUtilities.invokeLater(new Runnable() { @Override public void run() { p.update(); } }); } finally { p.setNewCommentEnabled(true); } } }); } private boolean closeReopen() { return GitHubIssueSupport.toggleState(getPanel().getIssue()); } } @NbBundle.Messages({ "CreatePullRequestAction.confirmation.message=Do you want to change this issue to Pull Request?", "CreatePullRequestAction.error.message.same.branch=Another branch must be set.", "CreatePullRequestAction.error.message.cannot.find.base.head.repositories=Can't find base or your head repositories.", "CreatePullRequestAction.descriptor.title=Pull Request" }) public class CreatePullRequestAction implements ActionListener { @Override public void actionPerformed(ActionEvent e) { String actionCommand = e.getActionCommand(); final boolean isNewPullRequest = actionCommand.equals("New PR"); // NOI18N GitHubIssuePanel p = getPanel(); p.setCreatePullRequestButtonEnabled(false); final GitHubIssue issue = p.getIssue(); if (isNewPullRequest && !p.isNewPullRequestSelected()) { // remove the new pull request from the panle SwingUtilities.invokeLater(new Runnable() { @Override public void run() { getPanel().setNewPullRequest(null); } }); return; } RequestProcessor rp = GitHubIssues.getInstance().getRequestProcessor(); rp.post(new Runnable() { @Override public void run() { final GitHubRepository repository = issue.getRepository(); GitHubCache cache = GitHubCache.create(repository); final User mySelf = cache.getMySelf(); List<RepositoryBranch> baseBranches = cache.getBranches(true); final HashMap<Repository, List<RepositoryBranch>> baseRepositories = new HashMap<>(); baseRepositories.put(repository.getRepository(), baseBranches); final Map<Repository, List<RepositoryBranch>> headRepositories = getHeadRepositories(repository, baseBranches); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { try { if (baseRepositories.isEmpty() || headRepositories.isEmpty()) { UiUtils.showErrorDialog(Bundle.CreatePullRequestAction_error_message_cannot_find_base_head_repositories()); return; } // create descriptor final CreatePullRequestPanel createPullRequestPanel = new CreatePullRequestPanel(baseRepositories, headRepositories); createPullRequestPanel.setMessage(Bundle.CreatePullRequestAction_confirmation_message()); final NotifyDescriptor.Confirmation descriptor = new NotifyDescriptor.Confirmation( createPullRequestPanel, Bundle.CreatePullRequestAction_descriptor_title(), NotifyDescriptor.OK_CANCEL_OPTION, NotifyDescriptor.QUESTION_MESSAGE ); // add listeners ChangeListener changeListener = new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { descriptor.setValid(false); createPullRequestPanel.setErrorMessage(""); // NOI18N } }; createPullRequestPanel.addChangeListener(changeListener); ComparePullRequestPropertyChangeListener propertyChangeListener = new ComparePullRequestPropertyChangeListener(repository, createPullRequestPanel, descriptor); createPullRequestPanel.addPropertyChangeListener(propertyChangeListener); changeListener.stateChanged(null); // show dialog if (DialogDisplayer.getDefault().notify(descriptor) == NotifyDescriptor.OK_OPTION) { RepositoryBranch selectedBaseBranch = createPullRequestPanel.getSelectedBaseBranch(); RepositoryBranch selectedHeadBranch = createPullRequestPanel.getSelectedHeadBranch(); String baseBranch = selectedBaseBranch.getName(); String headBranch = mySelf.getLogin() + ":" + selectedHeadBranch.getName(); // NOI18N PullRequest pullRequest; try { if (isNewPullRequest) { // set new pull request to panel PullRequestMarker baseMarker = new PullRequestMarker(); baseMarker.setLabel(baseBranch); PullRequestMarker headMarker = new PullRequestMarker(); headMarker.setLabel(headBranch); PullRequest newPullRequest = new PullRequest() .setBase(baseMarker) .setHead(headMarker); getPanel().setNewPullRequest(newPullRequest); } else { pullRequest = issue.createPullRequest(headBranch, baseBranch); if (pullRequest != null) { getPanel().refresh(); } } } catch (IOException ex) { UiUtils.showErrorDialog("Can't create a pull request:" + ex.getMessage()); // NOI18N } } else if (isNewPullRequest) { getPanel().setNewPullRequestSelected(false); } // remove listeners createPullRequestPanel.removeChangeListener(changeListener); createPullRequestPanel.removePropertyChangeListener(propertyChangeListener); } finally { getPanel().setCreatePullRequestButtonEnabled(true); } } }); } }); } private Map<Repository, List<RepositoryBranch>> getHeadRepositories(GitHubRepository repository, List<RepositoryBranch> baseBranches) { Map<Repository, List<RepositoryBranch>> myRepositories = new HashMap<>(); GitHubCache cache = GitHubCache.create(repository); final User mySelf = cache.getMySelf(); String repositoryAuthor = repository.getRepositoryAuthor(); if (repositoryAuthor.equals(mySelf.getLogin())) { myRepositories.put(repository.getRepository(), baseBranches); } else { if (repository.isCollaborator()) { myRepositories.put(repository.getRepository(), baseBranches); } List<Repository> forks = cache.getForks(); Repository myRepository = null; for (Repository fork : forks) { User owner = fork.getOwner(); if (owner.getLogin().equals(mySelf.getLogin())) { myRepository = fork; break; } } if (myRepository == null) { return Collections.emptyMap(); } // get my branches GitHubClient client = repository.createGitHubClient(); List<RepositoryBranch> myRepositoryBranches = getMyRepositoryBranches(client, myRepository); myRepositories.put(myRepository, myRepositoryBranches); } return myRepositories; } private List<RepositoryBranch> getMyRepositoryBranches(GitHubClient client, Repository myRepository) { RepositoryService service = new RepositoryService(client); try { return service.getBranches(myRepository); } catch (IOException ex) { Exceptions.printStackTrace(ex); } return Collections.emptyList(); } } static class ComparePullRequestPropertyChangeListener implements PropertyChangeListener { private final GitHubRepository repository; private final CreatePullRequestPanel panel; private final NotifyDescriptor descriptor; private String errorMessage; private RepositoryCommitCompare compare; public ComparePullRequestPropertyChangeListener(GitHubRepository repository, CreatePullRequestPanel createPullRequestPanel, NotifyDescriptor descriptor) { this.repository = repository; this.panel = createPullRequestPanel; this.descriptor = descriptor; } @Override @NbBundle.Messages({ "ComparePullRequestPropertyChangeListener.message.no.compare=There isn't anything to compare.", "ComparePullRequestPropertyChangeListener.message.conflict=Can't automatically merge." }) public void propertyChange(PropertyChangeEvent evt) { if (evt != null && !evt.getPropertyName().equals(CreatePullRequestPanel.PROP_COMPARE_PULL_REQUEST)) { return; } errorMessage = null; compare = null; panel.setCompareButtonEnabled(false); try { // validate // same commit RepositoryBranch baseBranch = panel.getSelectedBaseBranch(); RepositoryBranch headBranch = panel.getSelectedHeadBranch(); if (baseBranch.getCommit().getSha().equals(headBranch.getCommit().getSha())) { panel.setErrorMessage(Bundle.ComparePullRequestPropertyChangeListener_message_no_compare()); descriptor.setValid(false); } // compare two commits compare(); if (compare == null) { panel.setErrorMessage(errorMessage); descriptor.setValid(false); return; } String status = compare.getStatus(); switch (status) { case "identical": // no break NOI18N case "behind": // NOI18N panel.setErrorMessage(Bundle.ComparePullRequestPropertyChangeListener_message_no_compare()); descriptor.setValid(false); return; case "diverged": // NOI18N panel.setErrorMessage(Bundle.ComparePullRequestPropertyChangeListener_message_conflict()); descriptor.setValid(true); return; case "ahead": // NOI18N break; default: break; } // everything ok panel.setErrorMessage(""); // NOI18N descriptor.setValid(true); } finally { panel.setCompareButtonEnabled(true); } } private void compare() { Repository headRepo = panel.getSelectedHeadRepository(); final User owner = headRepo.getOwner(); BaseProgressUtils.runOffEventDispatchThread(new Runnable() { @Override public void run() { try { compare = repository.compare( panel.getSelectedBaseBranch().getName(), owner.getLogin() + ":" + panel.getSelectedHeadBranch().getName()); } catch (IOException ex) { errorMessage = ex.getMessage(); } } }, "Comparing...", new AtomicBoolean(), false); // NOI18N } } }