| // Copyright (C) 2009 The Android Open Source Project | |
| // | |
| // 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.google.gerrit.server.config; | |
| import static com.google.common.base.MoreObjects.firstNonNull; | |
| import static com.google.common.base.Strings.emptyToNull; | |
| import static com.google.common.base.Strings.isNullOrEmpty; | |
| import static com.google.common.base.Strings.nullToEmpty; | |
| import com.google.common.flogger.FluentLogger; | |
| import com.google.gerrit.common.Nullable; | |
| import com.google.gerrit.common.data.GitwebType; | |
| import com.google.gerrit.common.data.ParameterizedString; | |
| import com.google.gerrit.extensions.common.WebLinkInfo; | |
| import com.google.gerrit.extensions.registration.DynamicSet; | |
| import com.google.gerrit.extensions.restapi.Url; | |
| import com.google.gerrit.extensions.webui.BranchWebLink; | |
| import com.google.gerrit.extensions.webui.FileHistoryWebLink; | |
| import com.google.gerrit.extensions.webui.FileWebLink; | |
| import com.google.gerrit.extensions.webui.ParentWebLink; | |
| import com.google.gerrit.extensions.webui.PatchSetWebLink; | |
| import com.google.gerrit.extensions.webui.ProjectWebLink; | |
| import com.google.gerrit.extensions.webui.ResolveConflictsWebLink; | |
| import com.google.gerrit.extensions.webui.TagWebLink; | |
| import com.google.inject.AbstractModule; | |
| import com.google.inject.Inject; | |
| import com.google.inject.Singleton; | |
| import java.net.MalformedURLException; | |
| import java.net.URL; | |
| import org.eclipse.jgit.lib.Config; | |
| public class GitwebConfig { | |
| private static final FluentLogger logger = FluentLogger.forEnclosingClass(); | |
| public static boolean isDisabled(Config cfg) { | |
| return isEmptyString(cfg, "gitweb", null, "url") | |
| || isEmptyString(cfg, "gitweb", null, "cgi") | |
| || "disabled".equals(cfg.getString("gitweb", null, "type")); | |
| } | |
| public static class LegacyModule extends AbstractModule { | |
| private final Config cfg; | |
| public LegacyModule(Config cfg) { | |
| this.cfg = cfg; | |
| } | |
| @Override | |
| protected void configure() { | |
| GitwebType type = typeFromConfig(cfg); | |
| if (type != null) { | |
| bind(GitwebType.class).toInstance(type); | |
| if (!isNullOrEmpty(type.getBranch())) { | |
| DynamicSet.bind(binder(), BranchWebLink.class).to(GitwebLinks.class); | |
| } | |
| if (!isNullOrEmpty(type.getTag())) { | |
| DynamicSet.bind(binder(), TagWebLink.class).to(GitwebLinks.class); | |
| } | |
| if (!isNullOrEmpty(type.getFile()) || !isNullOrEmpty(type.getRootTree())) { | |
| DynamicSet.bind(binder(), FileWebLink.class).to(GitwebLinks.class); | |
| } | |
| if (!isNullOrEmpty(type.getFileHistory())) { | |
| DynamicSet.bind(binder(), FileHistoryWebLink.class).to(GitwebLinks.class); | |
| } | |
| if (!isNullOrEmpty(type.getRevision())) { | |
| DynamicSet.bind(binder(), PatchSetWebLink.class).to(GitwebLinks.class); | |
| DynamicSet.bind(binder(), ParentWebLink.class).to(GitwebLinks.class); | |
| DynamicSet.bind(binder(), ResolveConflictsWebLink.class).to(GitwebLinks.class); | |
| } | |
| if (!isNullOrEmpty(type.getProject())) { | |
| DynamicSet.bind(binder(), ProjectWebLink.class).to(GitwebLinks.class); | |
| } | |
| } | |
| } | |
| } | |
| private static boolean isEmptyString(Config cfg, String section, String subsection, String name) { | |
| // This is currently the only way to check for the empty string in a JGit | |
| // config. Fun! | |
| String[] values = cfg.getStringList(section, subsection, name); | |
| return values.length > 0 && isNullOrEmpty(values[0]); | |
| } | |
| @Nullable | |
| private static GitwebType typeFromConfig(Config cfg) { | |
| GitwebType defaultType = defaultType(cfg.getString("gitweb", null, "type")); | |
| if (defaultType == null) { | |
| return null; | |
| } | |
| GitwebType type = new GitwebType(); | |
| type.setLinkName( | |
| firstNonNull(cfg.getString("gitweb", null, "linkname"), defaultType.getLinkName())); | |
| type.setBranch(firstNonNull(cfg.getString("gitweb", null, "branch"), defaultType.getBranch())); | |
| type.setTag(firstNonNull(cfg.getString("gitweb", null, "tag"), defaultType.getTag())); | |
| type.setProject( | |
| firstNonNull(cfg.getString("gitweb", null, "project"), defaultType.getProject())); | |
| type.setRevision( | |
| firstNonNull(cfg.getString("gitweb", null, "revision"), defaultType.getRevision())); | |
| type.setRootTree( | |
| firstNonNull(cfg.getString("gitweb", null, "roottree"), defaultType.getRootTree())); | |
| type.setFile(firstNonNull(cfg.getString("gitweb", null, "file"), defaultType.getFile())); | |
| type.setFileHistory( | |
| firstNonNull(cfg.getString("gitweb", null, "filehistory"), defaultType.getFileHistory())); | |
| type.setUrlEncode(cfg.getBoolean("gitweb", null, "urlencode", defaultType.getUrlEncode())); | |
| String pathSeparator = cfg.getString("gitweb", null, "pathSeparator"); | |
| if (pathSeparator != null) { | |
| if (pathSeparator.length() == 1) { | |
| char c = pathSeparator.charAt(0); | |
| if (isValidPathSeparator(c)) { | |
| type.setPathSeparator(firstNonNull(c, defaultType.getPathSeparator())); | |
| } else { | |
| logger.atWarning().log("Invalid gitweb.pathSeparator: %s", c); | |
| } | |
| } else { | |
| logger.atWarning().log("gitweb.pathSeparator is not a single character: %s", pathSeparator); | |
| } | |
| } | |
| return type; | |
| } | |
| @Nullable | |
| private static GitwebType defaultType(String typeName) { | |
| GitwebType type = new GitwebType(); | |
| switch (nullToEmpty(typeName)) { | |
| case "gitweb": | |
| type.setLinkName("gitweb"); | |
| type.setProject("?p=${project}.git;a=summary"); | |
| type.setRevision("?p=${project}.git;a=commit;h=${commit}"); | |
| type.setBranch("?p=${project}.git;a=shortlog;h=${branch}"); | |
| type.setTag("?p=${project}.git;a=shortlog;h=${tag}"); | |
| type.setRootTree("?p=${project}.git;a=tree;hb=${commit}"); | |
| type.setFile("?p=${project}.git;hb=${commit};f=${file}"); | |
| type.setFileHistory("?p=${project}.git;a=history;hb=${branch};f=${file}"); | |
| break; | |
| case "cgit": | |
| type.setLinkName("cgit"); | |
| type.setProject("${project}.git/summary"); | |
| type.setRevision("${project}.git/commit/?id=${commit}"); | |
| type.setBranch("${project}.git/log/?h=${branch}"); | |
| type.setTag("${project}.git/tag/?h=${tag}"); | |
| type.setRootTree("${project}.git/tree/?h=${commit}"); | |
| type.setFile("${project}.git/tree/${file}?h=${commit}"); | |
| type.setFileHistory("${project}.git/log/${file}?h=${branch}"); | |
| break; | |
| case "custom": | |
| // For a custom type with no explicit link name, just reuse "gitweb". | |
| type.setLinkName("gitweb"); | |
| type.setProject(""); | |
| type.setRevision(""); | |
| type.setBranch(""); | |
| type.setTag(""); | |
| type.setRootTree(""); | |
| type.setFile(""); | |
| type.setFileHistory(""); | |
| break; | |
| case "": | |
| case "disabled": | |
| default: | |
| return null; | |
| } | |
| return type; | |
| } | |
| private final String url; | |
| private final GitwebType type; | |
| @Inject | |
| GitwebConfig( | |
| GitwebCgiConfig cgiConfig, | |
| @GerritServerConfig Config cfg, | |
| @Nullable @CanonicalWebUrl String gerritUrl) | |
| throws MalformedURLException { | |
| if (isDisabled(cfg)) { | |
| type = null; | |
| url = null; | |
| } else { | |
| String cfgUrl = cfg.getString("gitweb", null, "url"); | |
| type = typeFromConfig(cfg); | |
| if (type == null) { | |
| url = null; | |
| } else if (cgiConfig.getGitwebCgi() == null) { | |
| // Use an externally managed gitweb instance, and not an internal one. | |
| url = cfgUrl; | |
| } else { | |
| String baseGerritUrl; | |
| if (gerritUrl != null) { | |
| URL u = new URL(gerritUrl); | |
| baseGerritUrl = u.getPath(); | |
| } else { | |
| baseGerritUrl = "/"; | |
| } | |
| url = firstNonNull(cfgUrl, baseGerritUrl + "gitweb"); | |
| } | |
| } | |
| } | |
| /** Returns GitwebType for gitweb viewer. */ | |
| @Nullable | |
| public GitwebType getGitwebType() { | |
| return type; | |
| } | |
| /** | |
| * Returns URL of the entry point into gitweb. This URL may be relative to our context if gitweb | |
| * is hosted by ourselves; or absolute if its hosted elsewhere; or null if gitweb has not been | |
| * configured. | |
| */ | |
| public String getUrl() { | |
| return url; | |
| } | |
| /** | |
| * Determines if a given character can be used unencoded in an URL as a replacement for the path | |
| * separator '/'. | |
| * | |
| * <p>Reasoning: http://www.ietf.org/rfc/rfc1738.txt § 2.2: | |
| * | |
| * <p>... only alphanumerics, the special characters "$-_.+!*'(),", and reserved characters used | |
| * for their reserved purposes may be used unencoded within a URL. | |
| * | |
| * <p>The following characters might occur in file names, however: | |
| * | |
| * <p>alphanumeric characters, | |
| * | |
| * <p>"$-_.+!'," | |
| */ | |
| static boolean isValidPathSeparator(char c) { | |
| switch (c) { | |
| case '*': | |
| case '(': | |
| case ')': | |
| return true; | |
| default: | |
| return false; | |
| } | |
| } | |
| @Singleton | |
| static class GitwebLinks | |
| implements BranchWebLink, | |
| FileHistoryWebLink, | |
| FileWebLink, | |
| PatchSetWebLink, | |
| ParentWebLink, | |
| ProjectWebLink, | |
| ResolveConflictsWebLink, | |
| TagWebLink { | |
| private final String url; | |
| private final GitwebType type; | |
| private final ParameterizedString branch; | |
| private final ParameterizedString file; | |
| private final ParameterizedString fileHistory; | |
| private final ParameterizedString project; | |
| private final ParameterizedString revision; | |
| private final ParameterizedString tag; | |
| @Inject | |
| GitwebLinks(GitwebConfig config, GitwebType type) { | |
| this.url = config.getUrl(); | |
| this.type = type; | |
| this.branch = parse(type.getBranch()); | |
| this.file = parse(firstNonNull(emptyToNull(type.getFile()), nullToEmpty(type.getRootTree()))); | |
| this.fileHistory = parse(type.getFileHistory()); | |
| this.project = parse(type.getProject()); | |
| this.revision = parse(type.getRevision()); | |
| this.tag = parse(type.getTag()); | |
| } | |
| @Nullable | |
| @Override | |
| public WebLinkInfo getBranchWebLink(String projectName, String branchName) { | |
| if (branch != null) { | |
| return link( | |
| branch | |
| .replace("project", encode(projectName)) | |
| .replace("branch", encode(branchName)) | |
| .toString()); | |
| } | |
| return null; | |
| } | |
| @Nullable | |
| @Override | |
| public WebLinkInfo getTagWebLink(String projectName, String tagName) { | |
| if (tag != null) { | |
| return link( | |
| tag.replace("project", encode(projectName)).replace("tag", encode(tagName)).toString()); | |
| } | |
| return null; | |
| } | |
| @Nullable | |
| @Override | |
| public WebLinkInfo getFileHistoryWebLink(String projectName, String revision, String fileName) { | |
| if (fileHistory != null) { | |
| return link( | |
| fileHistory | |
| .replace("project", encode(projectName)) | |
| .replace("branch", encode(revision)) | |
| .replace("file", encode(fileName)) | |
| .toString()); | |
| } | |
| return null; | |
| } | |
| @Nullable | |
| @Override | |
| public WebLinkInfo getFileWebLink( | |
| String projectName, String revision, String hash, String fileName) { | |
| if (file != null) { | |
| return link( | |
| file.replace("project", encode(projectName)) | |
| .replace("commit", encode(revision)) | |
| .replace("hash", encode(hash)) | |
| .replace("file", encode(fileName)) | |
| .toString()); | |
| } | |
| return null; | |
| } | |
| @Nullable | |
| @Override | |
| public WebLinkInfo getPatchSetWebLink( | |
| String projectName, String commit, String commitMessage, String branchName) { | |
| if (revision != null) { | |
| // commitMessage and branchName are not needed, hence not used. | |
| return link( | |
| revision | |
| .replace("project", encode(projectName)) | |
| .replace("commit", encode(commit)) | |
| .toString()); | |
| } | |
| return null; | |
| } | |
| @Override | |
| public WebLinkInfo getResolveConflictsWebLink( | |
| String projectName, String commit, String commitMessage, String branchName) { | |
| // For Gitweb treat resolve conflicts links the same as patch set links | |
| return getPatchSetWebLink(projectName, commit, commitMessage, branchName); | |
| } | |
| @Override | |
| public WebLinkInfo getParentWebLink( | |
| String projectName, String commit, String commitMessage, String branchName) { | |
| // For Gitweb treat parent revision links the same as patch set links | |
| return getPatchSetWebLink(projectName, commit, commitMessage, branchName); | |
| } | |
| @Nullable | |
| @Override | |
| public WebLinkInfo getProjectWeblink(String projectName) { | |
| if (project != null) { | |
| return link(project.replace("project", encode(projectName)).toString()); | |
| } | |
| return null; | |
| } | |
| private String encode(String val) { | |
| if (type.getUrlEncode()) { | |
| return Url.encode(type.replacePathSeparator(val)); | |
| } | |
| return val; | |
| } | |
| private WebLinkInfo link(String rest) { | |
| WebLinkInfo webLink = new WebLinkInfo(type.getLinkName(), null, url + rest); | |
| webLink.tooltip = "Open in GitWeb"; | |
| return webLink; | |
| } | |
| @Nullable | |
| private static ParameterizedString parse(String pattern) { | |
| if (!isNullOrEmpty(pattern)) { | |
| return new ParameterizedString(pattern); | |
| } | |
| return null; | |
| } | |
| } | |
| } |