Merge "Add the ability to add a new section in gr-repo-access"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index be4316e..38c3e0f 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -147,6 +147,17 @@
+
By default 1.
+[[audit]]
+=== Section audit
+
+[[audit.maskSensitiveData]]audit.maskSensitiveData::
++
+If true, command parameters marked as sensitive are masked in audit logs.
++
+This option only affects audit. Other means of logging will always be masked.
++
+By default `false`.
+
[[auth]]
=== Section auth
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
index d793082..c116d76 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
@@ -88,7 +88,7 @@
/** Create a dialog box to show a single message string. */
public ErrorDialog(String message) {
this();
- body.add(new Label(message));
+ body.add(createErrorMsgLabel(message));
}
/** Create a dialog box to show a single message string. */
@@ -145,12 +145,16 @@
}
if (msg != null) {
- final Label m = new Label(msg);
- m.getElement().getStyle().setProperty("whiteSpace", "pre");
- body.add(m);
+ body.add(createErrorMsgLabel(msg));
}
}
+ private Label createErrorMsgLabel(String message) {
+ Label m = new Label(message);
+ m.getElement().getStyle().setProperty("white-space", "pre");
+ return m;
+ }
+
public ErrorDialog setText(String t) {
text.setText(t);
return this;
diff --git a/gerrit-plugin-gwtui/BUILD b/gerrit-plugin-gwtui/BUILD
index 3f066c7..0880993 100644
--- a/gerrit-plugin-gwtui/BUILD
+++ b/gerrit-plugin-gwtui/BUILD
@@ -24,6 +24,7 @@
"//java/org/eclipse/jgit:libclient-src.jar",
"//java/org/eclipse/jgit:libEdit-src.jar",
"//java/com/google/gerrit/common:libclient-src.jar",
+ "//java/com/google/gerrit/extensions:libapi-src.jar",
"//java/com/google/gwtexpui/clippy:libclippy-src.jar",
"//java/com/google/gwtexpui/globalkey:libglobalkey-src.jar",
"//java/com/google/gwtexpui/progress:libprogress-src.jar",
diff --git a/java/com/google/gerrit/server/events/EventFactory.java b/java/com/google/gerrit/server/events/EventFactory.java
index 8749838..6da2d43 100644
--- a/java/com/google/gerrit/server/events/EventFactory.java
+++ b/java/com/google/gerrit/server/events/EventFactory.java
@@ -246,8 +246,7 @@
la.label = lbl.label;
la.status = lbl.status.name();
if (lbl.appliedBy != null) {
- AccountState accountState = accountCache.get(lbl.appliedBy);
- la.by = asAccountAttribute(accountState);
+ la.by = asAccountAttribute(lbl.appliedBy);
}
sa.labels.add(la);
}
@@ -573,7 +572,7 @@
if (id == null) {
return null;
}
- return asAccountAttribute(accountCache.get(id));
+ return accountCache.maybeGet(id).map(a -> asAccountAttribute(a)).orElse(null);
}
/**
@@ -583,10 +582,6 @@
* @return object suitable for serialization to JSON
*/
public AccountAttribute asAccountAttribute(AccountState accountState) {
- if (accountState == null) {
- return null;
- }
-
AccountAttribute who = new AccountAttribute();
who.name = accountState.getAccount().getFullName();
who.email = accountState.getAccount().getPreferredEmail();
diff --git a/java/com/google/gerrit/server/permissions/ChangeControl.java b/java/com/google/gerrit/server/permissions/ChangeControl.java
index 0aa80e2..47c8543 100644
--- a/java/com/google/gerrit/server/permissions/ChangeControl.java
+++ b/java/com/google/gerrit/server/permissions/ChangeControl.java
@@ -283,6 +283,7 @@
private class ForChangeImpl extends ForChange {
private ChangeData cd;
private Map<String, PermissionRange> labels;
+ private String resourcePath;
ForChangeImpl(@Nullable ChangeData cd, @Nullable Provider<ReviewDb> db) {
this.cd = cd;
@@ -320,9 +321,13 @@
@Override
public String resourcePath() {
- return String.format(
- "/projects/%s/+changes/%s",
- getProjectControl().getProjectState().getName(), changeData().getId().get());
+ if (resourcePath == null) {
+ resourcePath =
+ String.format(
+ "/projects/%s/+changes/%s",
+ getProjectControl().getProjectState().getName(), changeData().getId().get());
+ }
+ return resourcePath;
}
@Override
diff --git a/java/com/google/gerrit/server/permissions/ProjectControl.java b/java/com/google/gerrit/server/permissions/ProjectControl.java
index a3bb424..ddadeaa 100644
--- a/java/com/google/gerrit/server/permissions/ProjectControl.java
+++ b/java/com/google/gerrit/server/permissions/ProjectControl.java
@@ -312,6 +312,8 @@
}
private class ForProjectImpl extends ForProject {
+ private String resourcePath;
+
@Override
public CurrentUser user() {
return getUser();
@@ -324,7 +326,10 @@
@Override
public String resourcePath() {
- return "/projects/" + getProjectState().getName();
+ if (resourcePath == null) {
+ resourcePath = "/projects/" + getProjectState().getName();
+ }
+ return resourcePath;
}
@Override
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
index 94f1acf..9f95171 100644
--- a/java/com/google/gerrit/server/permissions/RefControl.java
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -431,6 +431,8 @@
}
private class ForRefImpl extends ForRef {
+ private String resourcePath;
+
@Override
public CurrentUser user() {
return getUser();
@@ -443,8 +445,12 @@
@Override
public String resourcePath() {
- return String.format(
- "/projects/%s/+refs/%s", getProjectControl().getProjectState().getName(), refName);
+ if (resourcePath == null) {
+ resourcePath =
+ String.format(
+ "/projects/%s/+refs/%s", getProjectControl().getProjectState().getName(), refName);
+ }
+ return resourcePath;
}
@Override
diff --git a/java/com/google/gerrit/sshd/BaseCommand.java b/java/com/google/gerrit/sshd/BaseCommand.java
index cf54a47..1833f9c 100644
--- a/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/java/com/google/gerrit/sshd/BaseCommand.java
@@ -48,6 +48,10 @@
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicReference;
@@ -70,6 +74,8 @@
static final int STATUS_NOT_FOUND = PRIVATE_STATUS | 2;
public static final int STATUS_NOT_ADMIN = PRIVATE_STATUS | 3;
+ private static final String MASK = "***";
+
@Option(name = "--", usage = "end of options", handler = EndOfOptionsHandler.class)
private boolean endOfOptions;
@@ -93,6 +99,8 @@
@Inject private SshScope.Context context;
+ @Inject private SshCommandSensitiveFieldsCache cache;
+
/** Commands declared by a plugin can be scoped by the plugin name. */
@Inject(optional = true)
@PluginName
@@ -111,6 +119,10 @@
/** Unparsed command line options. */
private String[] argv;
+ private List<String> maskedArgv = new ArrayList<>();
+
+ private Set<String> sensitiveParameters = new HashSet<>();
+
public BaseCommand() {
task = Atomics.newReference();
}
@@ -156,6 +168,22 @@
this.argv = argv;
}
+ public List<String> getMaskedArguments() {
+ return maskedArgv;
+ }
+
+ public String getFormattedMaskedArguments(String delimiter) {
+ return String.join(delimiter, maskedArgv);
+ }
+
+ public void setMaskedArguments(List<String> argv) {
+ this.maskedArgv = argv;
+ }
+
+ public boolean isSensitiveParameter(String param) {
+ return sensitiveParameters.contains(param);
+ }
+
@Override
public void destroy() {
Future<?> future = task.getAndSet(null);
@@ -326,7 +354,7 @@
m.append(")");
}
m.append(" during ");
- m.append(context.getCommandLine());
+ m.append(getFormattedMaskedArguments(" "));
log.error(m.toString(), e);
}
@@ -372,7 +400,7 @@
protected String getTaskDescription() {
StringBuilder m = new StringBuilder();
- m.append(context.getCommandLine());
+ m.append(getFormattedMaskedArguments(" "));
return m.toString();
}
@@ -388,12 +416,49 @@
return m.toString();
}
+ private void maskSensitiveParameters() {
+ if (argv == null) {
+ return;
+ }
+ sensitiveParameters = cache.get(this.getClass());
+ maskedArgv = new ArrayList<>();
+ maskedArgv.add(commandName);
+ boolean maskNext = false;
+ for (int i = 0; i < argv.length; i++) {
+ if (maskNext) {
+ maskedArgv.add(MASK);
+ maskNext = false;
+ continue;
+ }
+ String arg = argv[i];
+ String key = extractKey(arg);
+ if (isSensitiveParameter(key)) {
+ maskNext = arg.equals(key);
+ // When arg != key then parameter contains '=' sign and we mask them right away.
+ // Otherwise we mask the next parameter as indicated by maskNext.
+ if (!maskNext) {
+ arg = key + "=" + MASK;
+ }
+ }
+ maskedArgv.add(arg);
+ }
+ }
+
+ private String extractKey(String arg) {
+ int eqPos = arg.indexOf('=');
+ if (eqPos > 0) {
+ return arg.substring(0, eqPos);
+ }
+ return arg;
+ }
+
private final class TaskThunk implements CancelableRunnable, ProjectRunnable {
private final CommandRunnable thunk;
private final String taskName;
private Project.NameKey projectName;
private TaskThunk(CommandRunnable thunk) {
+ maskSensitiveParameters();
this.thunk = thunk;
this.taskName = getTaskName();
}
diff --git a/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index 0287ceb..d061535 100644
--- a/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -29,6 +29,7 @@
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -159,7 +160,7 @@
} catch (Exception e) {
logger.warn(
"Cannot start command \""
- + ctx.getCommandLine()
+ + cmd.getFormattedMaskedArguments(" ")
+ "\" for user "
+ ctx.getSession().getUsername(),
e);
@@ -179,6 +180,10 @@
try {
cmd = dispatcher.get();
cmd.setArguments(argv);
+ cmd.setMaskedArguments(
+ argv.length > 0
+ ? Arrays.asList(argv[0])
+ : Arrays.asList(ctx.getCommandLine().split(" ")[0]));
cmd.setInputStream(in);
cmd.setOutputStream(out);
cmd.setErrorStream(err);
diff --git a/java/com/google/gerrit/sshd/DispatchCommand.java b/java/com/google/gerrit/sshd/DispatchCommand.java
index 3f2e258..62f80c9 100644
--- a/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -107,6 +107,10 @@
atomicCmd.set(cmd);
cmd.start(env);
+ if (cmd instanceof BaseCommand) {
+ setMaskedArguments(((BaseCommand) cmd).getMaskedArguments());
+ }
+
} catch (UnloggedFailure e) {
String msg = e.getMessage();
if (!msg.endsWith("\n")) {
diff --git a/java/com/google/gerrit/sshd/SensitiveData.java b/java/com/google/gerrit/sshd/SensitiveData.java
new file mode 100644
index 0000000..1dd7896
--- /dev/null
+++ b/java/com/google/gerrit/sshd/SensitiveData.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2017 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.sshd;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation tagged on a field of an ssh command to indicate the value must be hidden from logs.
+ */
+@Target({FIELD})
+@Retention(RUNTIME)
+public @interface SensitiveData {}
diff --git a/java/com/google/gerrit/sshd/SshCommandSensitiveFieldsCache.java b/java/com/google/gerrit/sshd/SshCommandSensitiveFieldsCache.java
new file mode 100644
index 0000000..8c79299
--- /dev/null
+++ b/java/com/google/gerrit/sshd/SshCommandSensitiveFieldsCache.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2018 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.sshd;
+
+import java.util.Set;
+
+/** Keeps data about ssh commands' parameters that have extra secure annotation. */
+public interface SshCommandSensitiveFieldsCache {
+ Set<String> get(Class<?> command);
+
+ void evictAll();
+}
diff --git a/java/com/google/gerrit/sshd/SshCommandSensitiveFieldsCacheImpl.java b/java/com/google/gerrit/sshd/SshCommandSensitiveFieldsCacheImpl.java
new file mode 100644
index 0000000..b593388
--- /dev/null
+++ b/java/com/google/gerrit/sshd/SshCommandSensitiveFieldsCacheImpl.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2018 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.sshd;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+import java.lang.reflect.Field;
+import java.util.HashSet;
+import java.util.Set;
+import org.kohsuke.args4j.Option;
+
+public class SshCommandSensitiveFieldsCacheImpl implements SshCommandSensitiveFieldsCache {
+ private static final String CACHE_NAME = "sshd_sensitive_command_params";
+ private final LoadingCache<Class<?>, Set<String>> sshdCommandsCache;
+
+ static Module module() {
+ return new CacheModule() {
+ @Override
+ protected void configure() {
+ cache(CACHE_NAME, new TypeLiteral<Class<?>>() {}, new TypeLiteral<Set<String>>() {})
+ .loader(Loader.class);
+ bind(SshCommandSensitiveFieldsCache.class).to(SshCommandSensitiveFieldsCacheImpl.class);
+ }
+ };
+ }
+
+ @Inject
+ SshCommandSensitiveFieldsCacheImpl(@Named(CACHE_NAME) LoadingCache<Class<?>, Set<String>> cache) {
+ sshdCommandsCache = cache;
+ }
+
+ @Override
+ public Set<String> get(Class<?> cmd) {
+ return sshdCommandsCache.getUnchecked(cmd);
+ }
+
+ @Override
+ public void evictAll() {
+ sshdCommandsCache.invalidateAll();
+ }
+
+ static class Loader extends CacheLoader<Class<?>, Set<String>> {
+
+ @Override
+ public Set<String> load(Class<?> cmd) throws Exception {
+ Set<String> datas = new HashSet<>();
+ for (Field field : cmd.getDeclaredFields()) {
+ if (field.isAnnotationPresent(SensitiveData.class)) {
+ Option option = field.getAnnotation(Option.class);
+ datas.add(option.name());
+ for (String opt : option.aliases()) {
+ datas.add(opt);
+ }
+ }
+ }
+ return datas;
+ }
+ }
+}
diff --git a/java/com/google/gerrit/sshd/SshLog.java b/java/com/google/gerrit/sshd/SshLog.java
index 6465a30..035989a 100644
--- a/java/com/google/gerrit/sshd/SshLog.java
+++ b/java/com/google/gerrit/sshd/SshLog.java
@@ -48,9 +48,12 @@
private static final String P_STATUS = "status";
private static final String P_AGENT = "agent";
+ private static final String MASK = "***";
+
private final Provider<SshSession> session;
private final Provider<Context> context;
private final AsyncAppender async;
+ private final boolean auditMask;
private final AuditService auditService;
@Inject
@@ -64,6 +67,7 @@
this.context = context;
this.auditService = auditService;
+ auditMask = config.getBoolean("audit", "maskSensitiveData", false);
if (!config.getBoolean("sshd", "requestLog", true)) {
async = null;
return;
@@ -121,8 +125,7 @@
final Context ctx = context.get();
ctx.finished = TimeUtil.nowMs();
- String cmd = extractWhat(dcmd);
-
+ String cmd = extractWhat(dcmd, true);
final LoggingEvent event = log(cmd);
event.setProperty(P_WAIT, (ctx.started - ctx.created) + "ms");
event.setProperty(P_EXEC, (ctx.finished - ctx.started) + "ms");
@@ -154,7 +157,11 @@
if (async != null) {
async.append(event);
}
- audit(context.get(), status, dcmd);
+
+ if (!auditMask) {
+ cmd = extractWhat(dcmd, false);
+ }
+ audit(ctx, status, cmd, extractParameters(dcmd));
}
private ListMultimap<String, ?> extractParameters(DispatchCommand dcmd) {
@@ -177,7 +184,10 @@
// --param=value
int eqPos = arg.indexOf('=');
if (arg.startsWith("--") && eqPos > 0) {
- parms.put(arg.substring(0, eqPos), arg.substring(eqPos + 1));
+ String param = arg.substring(0, eqPos);
+ String value =
+ auditMask && dcmd.isSensitiveParameter(param) ? MASK : arg.substring(eqPos + 1);
+ parms.put(param, value);
continue;
}
// -p value or --param value
@@ -192,7 +202,7 @@
if (paramName == null) {
parms.put("$" + argPos++, arg);
} else {
- parms.put(paramName, arg);
+ parms.put(paramName, auditMask && dcmd.isSensitiveParameter(paramName) ? MASK : arg);
paramName = null;
}
}
@@ -256,10 +266,6 @@
audit(ctx, result, cmd, null);
}
- void audit(Context ctx, Object result, DispatchCommand cmd) {
- audit(ctx, result, extractWhat(cmd), extractParameters(cmd));
- }
-
private void audit(Context ctx, Object result, String cmd, ListMultimap<String, ?> params) {
String sessionId;
CurrentUser currentUser;
@@ -277,11 +283,16 @@
auditService.dispatch(new SshAuditEvent(sessionId, currentUser, cmd, created, params, result));
}
- private String extractWhat(DispatchCommand dcmd) {
+ private String extractWhat(DispatchCommand dcmd, boolean hideSensitive) {
if (dcmd == null) {
return "Command was already destroyed";
}
- StringBuilder commandName = new StringBuilder(dcmd.getCommandName());
+ return hideSensitive ? dcmd.getFormattedMaskedArguments(".") : extractWhat(dcmd);
+ }
+
+ private String extractWhat(DispatchCommand dcmd) {
+ String name = dcmd.getCommandName();
+ StringBuilder commandName = new StringBuilder(name == null ? "" : name);
String[] args = dcmd.getArguments();
for (int i = 1; i < args.length; i++) {
commandName.append(".").append(args[i]);
diff --git a/java/com/google/gerrit/sshd/SshModule.java b/java/com/google/gerrit/sshd/SshModule.java
index 748277e..c672b00 100644
--- a/java/com/google/gerrit/sshd/SshModule.java
+++ b/java/com/google/gerrit/sshd/SshModule.java
@@ -65,6 +65,7 @@
configureRequestScope();
install(new AsyncReceiveCommits.Module());
configureAliases();
+ install(SshCommandSensitiveFieldsCacheImpl.module());
bind(SshLog.class);
bind(SshInfo.class).to(SshDaemon.class).in(SINGLETON);
diff --git a/java/com/google/gerrit/sshd/SshPluginStarterCallback.java b/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
index 9ae1814..047690c 100644
--- a/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
+++ b/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
@@ -33,13 +33,16 @@
private final DispatchCommandProvider root;
private final DynamicMap<DynamicOptions.DynamicBean> dynamicBeans;
+ private final SshCommandSensitiveFieldsCache cache;
@Inject
SshPluginStarterCallback(
@CommandName(Commands.ROOT) DispatchCommandProvider root,
- DynamicMap<DynamicOptions.DynamicBean> dynamicBeans) {
+ DynamicMap<DynamicOptions.DynamicBean> dynamicBeans,
+ SshCommandSensitiveFieldsCache cache) {
this.root = root;
this.dynamicBeans = dynamicBeans;
+ this.cache = cache;
}
@Override
@@ -56,6 +59,7 @@
if (cmd != null) {
newPlugin.add(root.replace(Commands.named(newPlugin.getName()), cmd));
}
+ cache.evictAll();
}
private Provider<Command> load(Plugin plugin) {
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index eedb7a4..6442943 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit eedb7a4309bf7b59bded8e022078934f282fbdc0
+Subproject commit 6442943d6a6de21b7d6d25b3fad2753a3c30d2d8
diff --git a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html
index f1ce45c..e16d296 100644
--- a/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html
+++ b/polygerrit-ui/app/behaviors/base-url-behavior/base-url-behavior.html
@@ -21,8 +21,8 @@
window.Gerrit = window.Gerrit || {};
const PROJECT_DASHBOARD_PATTERN = /\/p\/(.+)\/\+\/dashboard\/(.*)/;
- const REPO_URL = '/admin/repos/';
- const PROJECT_URL = '/admin/projects/';
+ const REPO_URL_PATTERN = /^\/admin\/repos/;
+ const PROJECT_URL = '/admin/projects';
/** @polymerBehavior Gerrit.BaseUrlBehavior */
Gerrit.BaseUrlBehavior = {
/** @return {string} */
@@ -38,7 +38,7 @@
clientPath = `/projects/${match[1]},dashboards/${match[2]}`;
}
// Replace any '/admin/project' links to '/admin/repo'
- clientPath = clientPath.replace(REPO_URL, PROJECT_URL);
+ clientPath = clientPath.replace(REPO_URL_PATTERN, PROJECT_URL);
return base + '/?polygerrit=0#' + clientPath;
},
};
diff --git a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
index 3ec7e7b..19eb437 100644
--- a/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
+++ b/polygerrit-ui/app/behaviors/gr-patch-set-behavior/gr-patch-set-behavior.html
@@ -209,7 +209,8 @@
},
/** @return {Boolean} */
- hasEditPatchsetLoaded(patchRange) {
+ hasEditPatchsetLoaded(patchRangeRecord) {
+ const patchRange = patchRangeRecord.base;
if (!patchRange) { return false; }
return patchRange.patchNum === Gerrit.PatchSetBehavior.EDIT_NAME ||
patchRange.basePatchNum === Gerrit.PatchSetBehavior.EDIT_NAME;
diff --git a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
index d79dc69..276febb 100644
--- a/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
+++ b/polygerrit-ui/app/elements/change-list/gr-change-list-view/gr-change-list-view.html
@@ -61,6 +61,8 @@
<div hidden$="[[_loading]]" hidden>
<gr-user-header
user-id="[[_userId]]"
+ show-dashboard-link
+ logged-in="[[loggedIn]]"
class$="[[_computeUserHeaderClass(_userId)]]"></gr-user-header>
<gr-change-list
changes="{{_changes}}"
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
index 9a7ca33..d3d0736 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.html
@@ -15,6 +15,7 @@
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../shared/gr-avatar/gr-avatar.html">
<link rel="import" href="../../shared/gr-date-formatter/gr-date-formatter.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
@@ -26,35 +27,36 @@
:host {
display: block;
height: 9em;
- position: relative;
width: 100%;
}
gr-avatar {
+ display: inline-block;
height: 7em;
left: 1em;
- position: absolute;
+ margin: 1em;
top: 1em;
width: 7em;
}
.info {
- left: 9em;
- position: absolute;
- top: 1em;
+ display: inline-block;
+ padding: 1em;
+ vertical-align: top;
}
.info > div > span {
display: inline-block;
font-weight: bold;
text-align: right;
- width: 6em;
+ width: 4em;
}
.name {
- margin-bottom: .25em;
+ display: inline-block;
}
.name hr {
width: 100%;
}
.status.hide,
- .name.hide {
+ .name.hide,
+ .dashboardLink.hide {
display: none;
}
</style>
@@ -63,10 +65,10 @@
image-size="100"
aria-label="Account avatar"></gr-avatar>
<div class="info">
- <h1 class$="name">
+ <h1 class="name">
[[_computeDetail(_accountDetails, 'name')]]
- <hr/>
</h1>
+ <hr/>
<div class$="status [[_computeStatusClass(_accountDetails)]]">
<span>Status:</span> [[_status]]
</div>
@@ -82,6 +84,11 @@
</gr-date-formatter>
</div>
</div>
+ <div class="info">
+ <div class$="[[_computeDashboardLinkClass(showDashboardLink, loggedIn)]]">
+ <a href$="[[_computeDashboardUrl(_accountDetails)]]">View dashboard</a>
+ </div>
+ </div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
</template>
<script src="gr-user-header.js"></script>
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
index dd3512a..d09e865 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header.js
@@ -23,6 +23,16 @@
observer: '_accountChanged',
},
+ showDashboardLink: {
+ type: Boolean,
+ value: false,
+ },
+
+ loggedIn: {
+ type: Boolean,
+ value: false,
+ },
+
/**
* @type {?{name: ?, email: ?, registered_on: ?}}
*/
@@ -64,5 +74,15 @@
_computeStatusClass(accountDetails) {
return this._computeDetail(accountDetails, 'status') ? '' : 'hide';
},
+
+ _computeDashboardUrl(accountDetails) {
+ if (!accountDetails || !accountDetails.email) { return null; }
+ return Gerrit.Nav.getUrlForUserDashboard(accountDetails.email);
+ },
+
+ _computeDashboardLinkClass(showDashboardLink, loggedIn) {
+ return showDashboardLink && loggedIn ?
+ 'dashboardLink' : 'dashboardLink hide';
+ },
});
})();
diff --git a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
index ab3b249..4ae8db4 100644
--- a/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
+++ b/polygerrit-ui/app/elements/change-list/gr-user-header/gr-user-header_test.html
@@ -68,5 +68,12 @@
});
});
});
+
+ test('_computeDashboardLinkClass', () => {
+ assert.include(element._computeDashboardLinkClass(false, false), 'hide');
+ assert.include(element._computeDashboardLinkClass(true, false), 'hide');
+ assert.include(element._computeDashboardLinkClass(false, true), 'hide');
+ assert.notInclude(element._computeDashboardLinkClass(true, true), 'hide');
+ });
});
</script>
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
index 7ca94e8..9dc1c0f 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.js
@@ -561,21 +561,23 @@
}
}
- // Only show edit button if there is no edit patchset loaded and the
- // file list is not in edit mode.
- if (editPatchsetLoaded || editMode) {
- if (changeActions.edit) {
- delete this.actions.edit;
- this.notifyPath('actions.edit');
- }
- if (!changeActions.doneEdit) {
- this.set('actions.doneEdit', DONE_EDIT);
- }
- } else {
- if (!changeActions.edit) { this.set('actions.edit', EDIT); }
- if (changeActions.doneEdit) {
- delete this.actions.doneEdit;
- this.notifyPath('actions.doneEdit');
+ if (this.changeIsOpen(this.change.status)) {
+ // Only show edit button if there is no edit patchset loaded and the
+ // file list is not in edit mode.
+ if (editPatchsetLoaded || editMode) {
+ if (changeActions.edit) {
+ delete this.actions.edit;
+ this.notifyPath('actions.edit');
+ }
+ if (!changeActions.doneEdit) {
+ this.set('actions.doneEdit', DONE_EDIT);
+ }
+ } else {
+ if (!changeActions.edit) { this.set('actions.edit', EDIT); }
+ if (changeActions.doneEdit) {
+ delete this.actions.doneEdit;
+ this.notifyPath('actions.doneEdit');
+ }
}
}
},
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
index cc065e9..e76e494 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.html
@@ -423,6 +423,7 @@
assert.isNotOk(element.$$('gr-button[data-action-key="publishEdit"]'));
assert.isNotOk(element.$$('gr-button[data-action-key="rebaseEdit"]'));
assert.isOk(element.$$('gr-button[data-action-key="deleteEdit"]'));
+ assert.isNotOk(element.$$('gr-button[data-action-key="edit"]'));
});
test('edit patchset is loaded, needs rebase', () => {
@@ -482,6 +483,7 @@
test('edit action', done => {
element.addEventListener('edit-tap', () => { done(); });
element.set('editMode', true);
+ element.change = {status: 'NEW'};
flushAsynchronousOperations();
assert.isNotOk(element.$$('gr-button[data-action-key="edit"]'));
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
index bf7f94e..5310a2b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.html
@@ -364,7 +364,7 @@
reply-disabled="[[_replyDisabled]]"
reply-button-label="[[_replyButtonLabel]]"
commit-message="[[_latestCommitMessage]]"
- edit-patchset-loaded="[[hasEditPatchsetLoaded(_patchRange)]]"
+ edit-patchset-loaded="[[hasEditPatchsetLoaded(_patchRange.*)]]"
edit-mode="[[_editMode]]"
edit-based-on-current-patch-set="[[hasEditBasedOnCurrentPatchSet(_allPatchSets)]]"
on-reload-change="_handleReloadChange"
diff --git a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
index ddd072e..8cd0329 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list-header/gr-file-list-header.html
@@ -110,13 +110,18 @@
align-items: center;
display: flex;
}
+ .rightControls gr-button,
+ gr-patch-range-select {
+ margin: 0 -4px;
+ }
.fileViewActions gr-button {
+ margin: 0;
--gr-button: {
padding: 2px 4px;
}
}
- .fileViewActions > *:not(:last-child) {
- margin-right: 5px;
+ .fileViewActions gr-button:first-of-type {
+ margin-left: 4px;
}
.editMode .hideOnEdit {
display: none;
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
index 083eb67..c088ffb 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.html
@@ -59,12 +59,19 @@
:host(.editMode) .showOnEdit {
display: initial;
}
+ .invisible {
+ visibility: hidden;
+ }
.controlRow {
align-items: center;
display: flex;
height: 2.25em;
justify-content: center;
}
+ .controlRow.invisible,
+ .show-hide.invisible {
+ display: none;
+ }
.reviewed,
.status {
align-items: center;
@@ -119,12 +126,6 @@
.stats {
min-width: 7em;
}
- .invisible {
- display: none;
- }
- .hideContent {
- visibility: hidden;
- }
.row:not(.header) .stats,
.total-stats {
font-family: var(--monospace-font-family);
@@ -256,7 +257,7 @@
<div class="stickyArea">
<div class$="file-row row [[_computePathClass(file.__path, _expandedFilePaths.*)]]"
data-path$="[[file.__path]]" tabindex="-1">
- <div class$="[[_computeClass('status', file.__path, 'true')]]"
+ <div class$="[[_computeClass('status', file.__path)]]"
tabindex="0"
aria-label$="[[_computeFileStatusLabel(file.status)]]">
[[_computeFileStatus(file.status)]]
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
index 9873fea..c9989cc 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list.js
@@ -696,12 +696,11 @@
/**
* @param {string} baseClass
* @param {string} path
- * @param {boolean=} opt_keepSpace
*/
- _computeClass(baseClass, path, opt_keepSpace) {
+ _computeClass(baseClass, path) {
const classes = [baseClass];
if (path === this.COMMIT_MESSAGE_PATH || path === this.MERGE_LIST_PATH) {
- classes.push(!opt_keepSpace ? 'invisible' : 'hideContent');
+ classes.push('invisible');
}
return classes.join(' ');
},
diff --git a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
index a8588ee..90ec46f 100644
--- a/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
+++ b/polygerrit-ui/app/elements/change/gr-file-list/gr-file-list_test.html
@@ -610,8 +610,6 @@
assert.equal(element._computeClass('clazz', '/foo/bar/baz'), 'clazz');
assert.equal(element._computeClass('clazz', '/COMMIT_MSG'),
'clazz invisible');
- assert.equal(element._computeClass('clazz', '/COMMIT_MSG', true),
- 'clazz hideContent');
});
test('file review status', () => {
diff --git a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
index ed69a73..fcb4ecc 100644
--- a/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
+++ b/polygerrit-ui/app/elements/core/gr-navigation/gr-navigation.html
@@ -375,8 +375,19 @@
*/
getUrlForOwner(owner) {
return this._getUrlFor({
+ view: Gerrit.Nav.View.SEARCH,
+ owner,
+ });
+ },
+
+ /**
+ * @param {string} user The name of the user.
+ * @return {string}
+ */
+ getUrlForUserDashboard(user) {
+ return this._getUrlFor({
view: Gerrit.Nav.View.DASHBOARD,
- user: owner,
+ user,
});
},
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
index 00735cf..c13d69f 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.html
@@ -82,6 +82,16 @@
</span>
</section>
<section>
+ <span class="title">Line numbers</span>
+ <span class="value">
+ <input
+ id="showLineNumbers"
+ type="checkbox"
+ checked$="[[editPrefs.hide_line_numbers]]"
+ on-change="_handleLineNumbersChanged">
+ </span>
+ </section>
+ <section>
<span class="title">Match brackets</span>
<span class="value">
<input
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
index 30752aa..0a791fd 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences.js
@@ -49,6 +49,11 @@
this._handleEditPrefsChanged();
},
+ _handleLineNumbersChanged() {
+ this.set('editPrefs.hide_line_numbers', !this.$.showLineNumbers.checked);
+ this._handleEditPrefsChanged();
+ },
+
_handleMatchBracketsChanged() {
this.set('editPrefs.match_brackets', this.$.showMatchBrackets.checked);
this._handleEditPrefsChanged();
diff --git a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
index e91290f6..118ee4f 100644
--- a/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
+++ b/polygerrit-ui/app/elements/settings/gr-edit-preferences/gr-edit-preferences_test.html
@@ -93,6 +93,8 @@
.firstElementChild.checked, editPreferences.syntax_highlighting);
assert.equal(valueOf('Show tabs', 'editPreferences')
.firstElementChild.checked, editPreferences.show_tabs);
+ assert.equal(valueOf('Line numbers', 'editPreferences')
+ .firstElementChild.checked, editPreferences.hide_line_numbers);
assert.equal(valueOf('Match brackets', 'editPreferences')
.firstElementChild.checked, editPreferences.match_brackets);
assert.equal(valueOf('Line wrapping', 'editPreferences')
diff --git a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
index 1744d28..fa6c1e2 100644
--- a/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
+++ b/polygerrit-ui/app/elements/shared/gr-tooltip/gr-tooltip.html
@@ -27,7 +27,7 @@
background-color: var(--tooltip-background-color, #333);
box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
color: #fff;
- font-size: .75rem;
+ font-size: var(--font-size-small);
position: absolute;
z-index: 1000;
max-width: var(--tooltip-max-width);
diff --git a/polygerrit-ui/app/styles/shared-styles.html b/polygerrit-ui/app/styles/shared-styles.html
index 0c80cdc..7f7b441 100644
--- a/polygerrit-ui/app/styles/shared-styles.html
+++ b/polygerrit-ui/app/styles/shared-styles.html
@@ -75,7 +75,7 @@
font-family: var(--font-family-bold);
}
h3 {
- font-size: var(--font-size-small);
+ font-size: 1.17em;
font-family: var(--font-family-bold);
}
iron-icon {
@@ -88,13 +88,13 @@
display: none !important;
}
.separator {
- background-color: rgba(0, 0, 0, .3);
+ border-left: 1px solid rgba(0, 0, 0, .3);
height: 20px;
margin: 0 8px;
- width: 1px;
+
}
.separator.transparent {
- background-color: transparent;
+ border-color: transparent;
}
paper-toggle-button {
--paper-toggle-button-checked-bar-color: var(--color-link);