/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.plugin.pullrequest.client; import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.HostingServiceTemplates; import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.NoCommitsInPullRequestException; import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.NoHistoryInCommonException; import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.NoPullRequestException; import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.NoUserForkException; import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.PullRequestAlreadyExistsException; import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.ServiceUtil; import org.eclipse.che.plugin.pullrequest.client.vcs.hosting.VcsHostingService; import org.eclipse.che.plugin.pullrequest.shared.dto.HostUser; import org.eclipse.che.plugin.pullrequest.shared.dto.PullRequest; import org.eclipse.che.plugin.pullrequest.shared.dto.Repository; import com.google.gwt.regexp.shared.RegExp; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import org.eclipse.che.api.core.model.workspace.Workspace; import org.eclipse.che.api.promises.client.Function; import org.eclipse.che.api.promises.client.FunctionException; import org.eclipse.che.api.promises.client.Promise; import org.eclipse.che.api.promises.client.PromiseError; import org.eclipse.che.api.promises.client.js.JsPromiseError; import org.eclipse.che.api.promises.client.js.Promises; import org.eclipse.che.ide.api.app.AppContext; import org.eclipse.che.ide.api.app.CurrentUser; import org.eclipse.che.ide.dto.DtoFactory; import org.eclipse.che.ide.rest.AsyncRequestCallback; import org.eclipse.che.ide.rest.DtoUnmarshallerFactory; import org.eclipse.che.ide.rest.RestContext; import org.eclipse.che.plugin.github.ide.GitHubClientService; import org.eclipse.che.plugin.github.shared.GitHubPullRequest; import org.eclipse.che.plugin.github.shared.GitHubPullRequestCreationInput; import org.eclipse.che.plugin.github.shared.GitHubPullRequestList; import org.eclipse.che.plugin.github.shared.GitHubRepository; import org.eclipse.che.plugin.github.shared.GitHubRepositoryList; import org.eclipse.che.plugin.github.shared.GitHubUser; import javax.inject.Inject; import javax.validation.constraints.NotNull; import java.util.ArrayList; import java.util.List; import static org.eclipse.che.ide.util.StringUtils.containsIgnoreCase; /** * {@link VcsHostingService} implementation for GitHub. * * @author Kevin Pollet */ public class GitHubHostingService implements VcsHostingService { public static final String SERVICE_NAME = "GitHub"; private static final String SSH_URL_PREFIX = "git@github.com:"; private static final String HTTPS_URL_PREFIX = "https://github.com/"; private static final String API_URL_PREFIX = "https://api.github.com/repos/"; private static final RegExp REPOSITORY_NAME_OWNER_PATTERN = RegExp.compile("([^\\/]+)\\/([^\\/]+)(?:\\.git)?"); private static final String REPOSITORY_GIT_EXTENSION = ".git"; private static final String NO_COMMITS_IN_PULL_REQUEST_ERROR_MESSAGE = "No commits between"; private static final String PULL_REQUEST_ALREADY_EXISTS_ERROR_MESSAGE = "A pull request already exists for "; private static final String NO_HISTORYIN_COMMON_ERROR_MESSAGE = "has no history in common with"; private final AppContext appContext; private final DtoUnmarshallerFactory dtoUnmarshallerFactory; private final DtoFactory dtoFactory; private final GitHubClientService gitHubClientService; private final HostingServiceTemplates templates; private final String baseUrl; @Inject public GitHubHostingService(@NotNull @RestContext final String baseUrl, @NotNull final AppContext appContext, @NotNull final DtoUnmarshallerFactory dtoUnmarshallerFactory, @NotNull final DtoFactory dtoFactory, @NotNull final GitHubClientService gitHubClientService, @NotNull final GitHubTemplates templates) { this.appContext = appContext; this.dtoUnmarshallerFactory = dtoUnmarshallerFactory; this.dtoFactory = dtoFactory; this.gitHubClientService = gitHubClientService; this.templates = templates; this.baseUrl = baseUrl; } @Override public Promise<HostUser> getUserInfo() { return gitHubClientService.getUserInfo() .then(new Function<GitHubUser, HostUser>() { @Override public HostUser apply(GitHubUser gitHubUser) throws FunctionException { return dtoFactory.createDto(HostUser.class) .withId(gitHubUser.getId()) .withLogin(gitHubUser.getLogin()) .withName(gitHubUser.getName()) .withUrl(gitHubUser.getUrl()); } }); } @Override public Promise<Repository> getRepository(String owner, String repositoryName) { return gitHubClientService.getRepository(owner, repositoryName) .then(new Function<GitHubRepository, Repository>() { @Override public Repository apply(GitHubRepository ghRepo) throws FunctionException { return valueOf(ghRepo); } }); } @NotNull @Override public String getRepositoryNameFromUrl(@NotNull final String url) { final String urlWithoutGitHubPrefix = removeGithubPrefix(url); final String namePart = REPOSITORY_NAME_OWNER_PATTERN.exec(urlWithoutGitHubPrefix).getGroup(2); if (namePart != null && namePart.endsWith(REPOSITORY_GIT_EXTENSION)) { return namePart.substring(0, namePart.length() - REPOSITORY_GIT_EXTENSION.length()); } else { return namePart; } } @NotNull @Override public String getRepositoryOwnerFromUrl(@NotNull final String url) { final String urlWithoutGitHubPrefix = removeGithubPrefix(url); return REPOSITORY_NAME_OWNER_PATTERN.exec(urlWithoutGitHubPrefix).getGroup(1); } private String removeGithubPrefix(final String url) { int start; if (url.startsWith(SSH_URL_PREFIX)) { start = SSH_URL_PREFIX.length(); } else if (url.startsWith(HTTPS_URL_PREFIX)) { start = HTTPS_URL_PREFIX.length(); } else if (url.startsWith(API_URL_PREFIX)) { start = API_URL_PREFIX.length(); } else { throw new IllegalArgumentException("Unknown github repo URL pattern"); } return url.substring(start); } @Override public Promise<Repository> fork(final String owner, final String repository) { return gitHubClientService.fork(owner, repository) .thenPromise(new Function<GitHubRepository, Promise<Repository>>() { @Override public Promise<Repository> apply(GitHubRepository repository) throws FunctionException { if (repository != null) { return Promises.resolve(valueOf(repository)); } else { return Promises.reject(JsPromiseError.create(new Exception("No repository."))); } } }); } @NotNull @Override public String makeSSHRemoteUrl(@NotNull final String username, @NotNull final String repository) { return templates.sshUrlTemplate(username, repository); } @NotNull @Override public String makeHttpRemoteUrl(@NotNull final String username, @NotNull final String repository) { return templates.httpUrlTemplate(username, repository); } @NotNull @Override public String makePullRequestUrl(@NotNull final String username, @NotNull final String repository, @NotNull final String pullRequestNumber) { return templates.pullRequestUrlTemplate(username, repository, pullRequestNumber); } @NotNull @Override public String formatReviewFactoryUrl(@NotNull final String reviewFactoryUrl) { final String protocol = Window.Location.getProtocol(); final String host = Window.Location.getHost(); return templates.formattedReviewFactoryUrlTemplate(protocol, host, reviewFactoryUrl); } @Override public VcsHostingService init(String remoteUrl) { return this; } @NotNull @Override public String getName() { return SERVICE_NAME; } @NotNull @Override public String getHost() { return "github.com"; } @Override public boolean isHostRemoteUrl(@NotNull final String remoteUrl) { return remoteUrl.startsWith(SSH_URL_PREFIX) || remoteUrl.startsWith(HTTPS_URL_PREFIX); } @Override public Promise<PullRequest> getPullRequest(String owner, String repository, String username, final String branchName) { return gitHubClientService.getPullRequests(owner, repository, username + ':' + branchName) .thenPromise(new Function<GitHubPullRequestList, Promise<PullRequest>>() { @Override public Promise<PullRequest> apply(GitHubPullRequestList prsList) throws FunctionException { if (prsList.getPullRequests().isEmpty()) { return Promises.reject(JsPromiseError.create(new NoPullRequestException(branchName))); } return Promises.resolve(valueOf(prsList.getPullRequests().get(0))); } }); } /** * Get all pull requests for given owner:repository * * @param owner * the username of the owner. * @param repository * the repository name. * @param callback * callback called when operation is done. */ private void getPullRequests(@NotNull final String owner, @NotNull final String repository, @NotNull final AsyncCallback<List<PullRequest>> callback) { gitHubClientService.getPullRequests(owner, repository, new AsyncRequestCallback<GitHubPullRequestList>( dtoUnmarshallerFactory.newUnmarshaller(GitHubPullRequestList.class)) { @Override protected void onSuccess(final GitHubPullRequestList result) { final List<PullRequest> pullRequests = new ArrayList<>(); for (final GitHubPullRequest oneGitHubPullRequest : result.getPullRequests()) { pullRequests.add(valueOf(oneGitHubPullRequest)); } callback.onSuccess(pullRequests); } @Override protected void onFailure(final Throwable exception) { callback.onFailure(exception); } }); } /** * Get all pull requests for given owner:repository * * @param owner * the username of the owner. * @param repository * the repository name. */ private Promise<List<PullRequest>> getPullRequests(String owner, String repository) { return gitHubClientService.getPullRequests(owner, repository) .then(new Function<GitHubPullRequestList, List<PullRequest>>() { @Override public List<PullRequest> apply(GitHubPullRequestList result) throws FunctionException { final List<PullRequest> pullRequests = new ArrayList<>(); for (final GitHubPullRequest oneGitHubPullRequest : result.getPullRequests()) { pullRequests.add(valueOf(oneGitHubPullRequest)); } return pullRequests; } }); } protected PullRequest getPullRequestByBranch(final String headBranch, final List<PullRequest> pullRequests) { for (final PullRequest onePullRequest : pullRequests) { if (headBranch.equals(onePullRequest.getHeadRef())) { return onePullRequest; } } return null; } @Override public Promise<PullRequest> createPullRequest(final String owner, final String repository, final String username, final String headBranchName, final String baseBranchName, final String title, final String body) { final String brName = username + ":" + headBranchName; final GitHubPullRequestCreationInput input = dtoFactory.createDto(GitHubPullRequestCreationInput.class) .withTitle(title) .withHead(brName) .withBase(baseBranchName) .withBody(body); return gitHubClientService.createPullRequest(owner, repository, input) .then(new Function<GitHubPullRequest, PullRequest>() { @Override public PullRequest apply(GitHubPullRequest arg) throws FunctionException { return valueOf(arg); } }) .catchErrorPromise(new Function<PromiseError, Promise<PullRequest>>() { @Override public Promise<PullRequest> apply(PromiseError err) throws FunctionException { final String msg = err.getMessage(); if (containsIgnoreCase(msg, NO_COMMITS_IN_PULL_REQUEST_ERROR_MESSAGE)) { return Promises.reject(JsPromiseError.create(new NoCommitsInPullRequestException(brName, baseBranchName))); } else if (containsIgnoreCase(msg, PULL_REQUEST_ALREADY_EXISTS_ERROR_MESSAGE)) { return Promises.reject(JsPromiseError.create(new PullRequestAlreadyExistsException(brName))); } else if (containsIgnoreCase(msg, NO_HISTORYIN_COMMON_ERROR_MESSAGE)) { return Promises.reject(JsPromiseError.create(new NoHistoryInCommonException( "The " + brName + " branch has no history in common with " + owner + ':' + baseBranchName))); } return Promises.reject(err); } }); } @Override public Promise<Repository> getUserFork(final String user, final String owner, final String repository) { return getForks(owner, repository).thenPromise(new Function<List<Repository>, Promise<Repository>>() { @Override public Promise<Repository> apply(List<Repository> repositories) throws FunctionException { final Repository userFork = getUserFork(user, repositories); if (userFork != null) { return Promises.resolve(userFork); } else { return Promises.reject(JsPromiseError.create(new NoUserForkException(user))); } } }); } /** * Returns the forks of the given repository for the given owner. * * @param owner * the repository owner. * @param repository * the repository name. * @param callback * callback called when operation is done. */ private void getForks(@NotNull final String owner, @NotNull final String repository, @NotNull final AsyncCallback<List<Repository>> callback) { gitHubClientService.getForks(owner, repository, new AsyncRequestCallback<GitHubRepositoryList>( dtoUnmarshallerFactory.newUnmarshaller(GitHubRepositoryList.class)) { @Override protected void onSuccess(final GitHubRepositoryList gitHubRepositoryList) { final List<Repository> repositories = new ArrayList<>(); for (final GitHubRepository oneGitHubRepository : gitHubRepositoryList.getRepositories()) { repositories.add(valueOf(oneGitHubRepository)); } callback.onSuccess(repositories); } @Override protected void onFailure(final Throwable exception) { callback.onFailure(exception); } }); } private Promise<List<Repository>> getForks(final String owner, final String repository) { return gitHubClientService.getForks(owner, repository) .then(new Function<GitHubRepositoryList, List<Repository>>() { @Override public List<Repository> apply(GitHubRepositoryList gitHubRepositoryList) throws FunctionException { final List<Repository> repositories = new ArrayList<>(); for (final GitHubRepository oneGitHubRepository : gitHubRepositoryList.getRepositories()) { repositories.add(valueOf(oneGitHubRepository)); } return repositories; } }); } private Repository getUserFork(final String login, final List<Repository> forks) { for (final Repository oneRepository : forks) { final String repositoryUrl = oneRepository.getCloneUrl(); if (repositoryUrl != null && containsIgnoreCase(repositoryUrl, "/" + login + "/")) { return oneRepository; } } return null; } /** * Converts an instance of {@link org.eclipse.che.plugin.github.shared.GitHubRepository} into a {@link * Repository}. * * @param gitHubRepository * the GitHub repository to convert. * @return the corresponding {@link Repository} instance or {@code null} if * given * gitHubRepository is {@code null}. */ private Repository valueOf(final GitHubRepository gitHubRepository) { if (gitHubRepository == null) { return null; } final GitHubRepository gitHubRepositoryParent = gitHubRepository.getParent(); final Repository parent = gitHubRepositoryParent == null ? null : dtoFactory.createDto(Repository.class) .withFork(gitHubRepositoryParent.isFork()) .withName(gitHubRepositoryParent.getName()) .withParent(null) .withPrivateRepo(gitHubRepositoryParent.isPrivateRepo()) .withCloneUrl(gitHubRepositoryParent.getCloneUrl()); return dtoFactory.createDto(Repository.class) .withFork(gitHubRepository.isFork()) .withName(gitHubRepository.getName()) .withParent(parent) .withPrivateRepo(gitHubRepository.isPrivateRepo()) .withCloneUrl(gitHubRepository.getCloneUrl()); } /** * Converts an instance of {@link org.eclipse.che.plugin.github.shared.GitHubPullRequest} into a {@link * PullRequest}. * * @param gitHubPullRequest * the GitHub pull request to convert. * @return the corresponding {@link PullRequest} instance or {@code null} if * given gitHubPullRequest is {@code null}. */ private PullRequest valueOf(final GitHubPullRequest gitHubPullRequest) { if (gitHubPullRequest == null) { return null; } return dtoFactory.createDto(PullRequest.class) .withId(gitHubPullRequest.getId()) .withUrl(gitHubPullRequest.getUrl()) .withHtmlUrl(gitHubPullRequest.getHtmlUrl()) .withNumber(gitHubPullRequest.getNumber()) .withState(gitHubPullRequest.getState()) .withHeadRef(gitHubPullRequest.getHead().getLabel()) .withDescription(gitHubPullRequest.getBody()); } @Override public Promise<HostUser> authenticate(final CurrentUser user) { final Workspace workspace = this.appContext.getWorkspace(); if (workspace == null) { return Promises.reject(JsPromiseError.create("Error accessing current workspace")); } final String authUrl = baseUrl + "/oauth/authenticate?oauth_provider=github&userId=" + user.getProfile().getUserId() + "&scope=user,repo,write:public_key&redirect_after_login=" + Window.Location.getProtocol() + "//" + Window.Location.getHost() + "/ws/" + workspace.getConfig().getName(); return ServiceUtil.performWindowAuth(this, authUrl); } @Override public Promise<PullRequest> updatePullRequest(String owner, String repository, PullRequest pullRequest) { return gitHubClientService.updatePullRequest(owner, repository, pullRequest.getNumber(), valueOf(pullRequest)) .then(new Function<GitHubPullRequest, PullRequest>() { @Override public PullRequest apply(GitHubPullRequest arg) throws FunctionException { return valueOf(arg); } }); } private GitHubPullRequest valueOf(PullRequest pullRequest) { if (pullRequest == null) { return null; } return dtoFactory.createDto(GitHubPullRequest.class) .withId(pullRequest.getId()) .withUrl(pullRequest.getUrl()) .withHtmlUrl(pullRequest.getHtmlUrl()) .withNumber(pullRequest.getNumber()) .withState(pullRequest.getState()) .withBody(pullRequest.getDescription()); } @Override public String toString() { return "GitHubHostingService"; } }