blob: f8c0592a5ab32331d3eed9d7a2942882129318f7 [file] [log] [blame]
// 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;
}
}
}