package com.cloudbees.jenkins; import com.coravy.hudson.plugins.github.GithubProjectProperty; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Predicates; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; import org.jenkinsci.plugins.github.GitHubPlugin; import org.jenkinsci.plugins.github.config.GitHubServerConfig; import org.jenkinsci.plugins.github.util.misc.NullSafeFunction; import org.kohsuke.github.GHCommitPointer; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import java.io.IOException; import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.google.common.base.Predicates.and; import static com.google.common.base.Predicates.notNull; import static java.lang.String.format; import static org.apache.commons.lang3.StringUtils.trimToEmpty; import static org.apache.commons.lang3.builder.ToStringStyle.SHORT_PREFIX_STYLE; import static org.jenkinsci.plugins.github.config.GitHubServerConfig.withHost; import static org.jenkinsci.plugins.github.util.FluentIterableWrapper.from; /** * Uniquely identifies a repository on GitHub. * * @author Kohsuke Kawaguchi */ public class GitHubRepositoryName { private static final Logger LOGGER = LoggerFactory.getLogger(GitHubRepositoryName.class); private static final Pattern[] URL_PATTERNS = { /** * The first set of patterns extract the host, owner and repository names * from URLs that include a '.git' suffix, removing the suffix from the * repository name. */ Pattern.compile("git@(.+):([^/]+)/([^/]+)\\.git"), Pattern.compile("https?://[^/]+@([^/]+)/([^/]+)/([^/]+)\\.git"), Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)\\.git"), Pattern.compile("git://([^/]+)/([^/]+)/([^/]+)\\.git"), Pattern.compile("ssh://(?:git@)?([^/]+)/([^/]+)/([^/]+)\\.git"), /** * The second set of patterns extract the host, owner and repository names * from all other URLs. Note that these patterns must be processed *after* * the first set, to avoid any '.git' suffix that may be present being included * in the repository name. */ Pattern.compile("git@(.+):([^/]+)/([^/]+)/?"), Pattern.compile("https?://[^/]+@([^/]+)/([^/]+)/([^/]+)/?"), Pattern.compile("https?://([^/]+)/([^/]+)/([^/]+)/?"), Pattern.compile("git://([^/]+)/([^/]+)/([^/]+)/?"), Pattern.compile("ssh://(?:git@)?([^/]+)/([^/]+)/([^/]+)/?") }; /** * Create {@link GitHubRepositoryName} from URL * * @param url repo url. Can be null * * @return parsed {@link GitHubRepositoryName} or null if it cannot be parsed from the specified URL */ @CheckForNull public static GitHubRepositoryName create(String url) { LOGGER.debug("Constructing from URL {}", url); for (Pattern p : URL_PATTERNS) { Matcher m = p.matcher(trimToEmpty(url)); if (m.matches()) { LOGGER.debug("URL matches {}", m); GitHubRepositoryName ret = new GitHubRepositoryName(m.group(1), m.group(2), m.group(3)); LOGGER.debug("Object is {}", ret); return ret; } } LOGGER.warn("Could not match URL {}", url); return null; } /** * @param projectProperty project property to extract url. Can be null * * @return parsed as {@link GitHubRepositoryName} object url to GitHub project * @see #create(String) * @since 1.14.1 */ @CheckForNull public static GitHubRepositoryName create(GithubProjectProperty projectProperty) { if (projectProperty == null) { return null; } return GitHubRepositoryName.create(projectProperty.getProjectUrlStr()); } @SuppressWarnings("visibilitymodifier") public final String host; @SuppressWarnings("visibilitymodifier") public final String userName; @SuppressWarnings("visibilitymodifier") public final String repositoryName; public GitHubRepositoryName(String host, String userName, String repositoryName) { this.host = host; this.userName = userName; this.repositoryName = repositoryName; } public String getHost() { return host; } public String getUserName() { return userName; } public String getRepositoryName() { return repositoryName; } /** * Resolves this name to the actual reference by {@link GHRepository} * * Shortcut for {@link #resolve(Predicate)} with always true predicate * ({@link Predicates#alwaysTrue()}) as argument */ public Iterable<GHRepository> resolve() { return resolve(Predicates.<GitHubServerConfig>alwaysTrue()); } /** * Resolves this name to the actual reference by {@link GHRepository}. * * Since the system can store multiple credentials, * and only some of them might be able to see this name in question, * this method uses {@link org.jenkinsci.plugins.github.config.GitHubPluginConfig#findGithubConfig(Predicate)} * and attempt to find the right credential that can * access this repository. * * Any predicate as argument will be combined with {@link GitHubServerConfig#withHost(String)} to find only * corresponding for this repo name authenticated github repository * * This method walks multiple repositories for each credential that can access the repository. Depending on * what you are trying to do with the repository, you might have to keep trying until a {@link GHRepository} * with suitable permission is returned. * * @param predicate helps to filter only useful for resolve {@link GitHubServerConfig}s * * @return iterable with lazy login process for getting authenticated repos * @since 1.13.0 */ public Iterable<GHRepository> resolve(Predicate<GitHubServerConfig> predicate) { return from(GitHubPlugin.configuration().findGithubConfig(and(withHost(host), predicate))) .transform(toGHRepository(this)) .filter(notNull()); } /** * Variation of {@link #resolve()} method that just returns the first valid repository object. * * This is useful if the caller only relies on the read access to the repository and doesn't need to * walk possible candidates. */ @CheckForNull public GHRepository resolveOne() { return from(resolve()).first().orNull(); } /** * Does this repository match the repository referenced in the given {@link GHCommitPointer}? */ public boolean matches(GHCommitPointer commit) { return userName.equals(commit.getUser().getLogin()) && repositoryName.equals(commit.getRepository().getName()) && host.equals(commit.getRepository().getHtmlUrl().getHost()); } /** * Does this repository match the repository referenced in the given {@link GHCommitPointer}? */ public boolean matches(GHRepository repo) throws IOException { return userName.equals(repo.getOwner().getLogin()) // TODO: use getOwnerName && repositoryName.equals(repo.getName()) && host.equals(repo.getHtmlUrl().getHost()); } @Override public boolean equals(Object obj) { return EqualsBuilder.reflectionEquals(this, obj); } @Override public int hashCode() { return new HashCodeBuilder().append(host).append(userName).append(repositoryName).build(); } @Override public String toString() { return new ToStringBuilder(this, SHORT_PREFIX_STYLE) .append("host", host).append("username", userName).append("repository", repositoryName).build(); } private static Function<GitHub, GHRepository> toGHRepository(final GitHubRepositoryName repoName) { return new NullSafeFunction<GitHub, GHRepository>() { @Override protected GHRepository applyNullSafe(@Nonnull GitHub gitHub) { try { return gitHub.getRepository(format("%s/%s", repoName.getUserName(), repoName.getRepositoryName())); } catch (IOException e) { LOGGER.warn("Failed to obtain repository {}", this, e); return null; } } }; } }