Merge branch 'stable-3.0'

* stable-3.0:
  ChangeIndexer: Stop using deprecated Futures.immediateCheckedFuture
  OutgoingEmail: Use UrlFormatter to get settings URL
  CommentSender: Use UrlFormatter to get URLs for file and comments
  Elasticsearch: Base the default number of shards on ES version
  Disallow change index task duplication
  AbstractPushForReview: Add tests for pushing with skip-validation option
  Set version to 2.16.10-SNAPSHOT
  Set version to 2.16.9
  Documentation: Fix the Elasticsearch shards/replicas link
  ProjectControl: Allow regexes ref strings for uploads
  ProjectControl: Allow regexes ref strings for tags
  ProjectControl: Reuse constants for ref strings
  Add extension point to gr-user-header

Change-Id: Ib67847e1f8d972543b6b0491e270f6e501cc63b1
This commit is contained in:
Marco Miller
2019-06-19 14:02:28 -04:00
committed by David Pursehouse
14 changed files with 265 additions and 58 deletions

View File

@@ -2888,15 +2888,15 @@ manually.
[[elasticsearch.numberOfShards]]elasticsearch.numberOfShards::
+
Sets the number of shards to use per index. Refer to the
link:https://www.elastic.co/guide/en/elasticsearch/reference/current/_basic_concepts.html#getting-started-shards-and-replicas[
link:https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-concepts.html#getting-started-shards-and-replicas[
Elasticsearch documentation] for details.
+
Defaults to 5.
Defaults to 5 for Elasticsearch versions 5 and 6, and to 1 starting with Elasticsearch 7.
[[elasticsearch.numberOfReplicas]]elasticsearch.numberOfReplicas::
+
Sets the number of replicas to use per index. Refer to the
link:https://www.elastic.co/guide/en/elasticsearch/reference/current/_basic_concepts.html#getting-started-shards-and-replicas[
link:https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-concepts.html#getting-started-shards-and-replicas[
Elasticsearch documentation] for details.
+
Defaults to 1.

View File

@@ -198,7 +198,7 @@ abstract class AbstractElasticIndex<K, V> implements Index<K, V> {
}
// Recreate the index.
String indexCreationFields = concatJsonString(getSettings(), getMappings());
String indexCreationFields = concatJsonString(getSettings(client.adapter()), getMappings());
response =
performRequest(
"PUT", indexName + client.adapter().includeTypeNameParam(), indexCreationFields);
@@ -213,8 +213,8 @@ abstract class AbstractElasticIndex<K, V> implements Index<K, V> {
protected abstract String getMappings();
private String getSettings() {
return gson.toJson(ImmutableMap.of(SETTINGS, ElasticSetting.createSetting(config)));
private String getSettings(ElasticQueryAdapter adapter) {
return gson.toJson(ImmutableMap.of(SETTINGS, ElasticSetting.createSetting(config, adapter)));
}
protected abstract String getId(V v);

View File

@@ -42,7 +42,7 @@ class ElasticConfiguration {
static final String KEY_NUMBER_OF_REPLICAS = "numberOfReplicas";
static final String DEFAULT_PORT = "9200";
static final String DEFAULT_USERNAME = "elastic";
static final int DEFAULT_NUMBER_OF_SHARDS = 5;
static final int DEFAULT_NUMBER_OF_SHARDS = 0;
static final int DEFAULT_NUMBER_OF_REPLICAS = 1;
private final Config cfg;
@@ -100,4 +100,11 @@ class ElasticConfiguration {
String getIndexName(String name, int schemaVersion) {
return String.format("%s%s_%04d", prefix, name, schemaVersion);
}
int getNumberOfShards(ElasticQueryAdapter adapter) {
if (numberOfShards == DEFAULT_NUMBER_OF_SHARDS) {
return adapter.getDefaultNumberOfShards();
}
return numberOfShards;
}
}

View File

@@ -27,6 +27,7 @@ public class ElasticQueryAdapter {
private final boolean useV5Type;
private final boolean useV6Type;
private final boolean omitType;
private final int defaultNumberOfShards;
private final String searchFilteringName;
private final String indicesExistParams;
@@ -41,6 +42,7 @@ public class ElasticQueryAdapter {
this.useV5Type = !version.isV6OrLater();
this.useV6Type = version.isV6();
this.omitType = version.isV7OrLater();
this.defaultNumberOfShards = version.isV7OrLater() ? 1 : 5;
this.versionDiscoveryUrl = version.isV6OrLater() ? "/%s*" : "/%s*/_aliases";
this.searchFilteringName = "_source";
this.indicesExistParams =
@@ -98,6 +100,10 @@ public class ElasticQueryAdapter {
return omitType;
}
int getDefaultNumberOfShards() {
return defaultNumberOfShards;
}
String getType() {
return getType("");
}

View File

@@ -22,18 +22,18 @@ class ElasticSetting {
private static final ImmutableMap<String, String> CUSTOM_CHAR_MAPPING =
ImmutableMap.of("\\u002E", "\\u0020", "\\u005F", "\\u0020");
static SettingProperties createSetting(ElasticConfiguration config) {
return new ElasticSetting.Builder().addCharFilter().addAnalyzer().build(config);
static SettingProperties createSetting(ElasticConfiguration config, ElasticQueryAdapter adapter) {
return new ElasticSetting.Builder().addCharFilter().addAnalyzer().build(config, adapter);
}
static class Builder {
private final ImmutableMap.Builder<String, FieldProperties> fields =
new ImmutableMap.Builder<>();
SettingProperties build(ElasticConfiguration config) {
SettingProperties build(ElasticConfiguration config, ElasticQueryAdapter adapter) {
SettingProperties properties = new SettingProperties();
properties.analysis = fields.build();
properties.numberOfShards = config.numberOfShards;
properties.numberOfShards = config.getNumberOfShards(adapter);
properties.numberOfReplicas = config.numberOfReplicas;
return properties;
}

View File

@@ -47,10 +47,32 @@ public interface UrlFormatter {
return getWebUrl().map(url -> url + "c/" + project.get() + "/+/" + id.get());
}
/** Returns the URL for viewing a file in a given patch set of a change. */
default Optional<String> getPatchFileView(Change change, int patchsetId, String filename) {
return getChangeViewUrl(change.getProject(), change.getId())
.map(url -> url + "/" + patchsetId + "/" + filename);
}
/** Returns the URL for viewing a comment in a file in a given patch set of a change. */
default Optional<String> getInlineCommentView(
Change change, int patchsetId, String filename, short side, int startLine) {
return getPatchFileView(change, patchsetId, filename)
.map(url -> url + String.format("@%s%d", side == 0 ? "a" : "", startLine));
}
/** Returns a URL pointing to a section of the settings page. */
default Optional<String> getSettingsUrl() {
return getWebUrl().map(url -> url + "settings");
}
/**
* Returns a URL pointing to a section of the settings page, or the settings page if {@code
* section} is null.
*/
default Optional<String> getSettingsUrl(@Nullable String section) {
return getWebUrl()
.map(url -> url + "settings" + (Strings.isNullOrEmpty(section) ? "" : "#" + section));
return Strings.isNullOrEmpty(section)
? getSettingsUrl()
: getSettingsUrl().map(url -> url + "#" + section);
}
/** Returns a URL pointing to a documentation page, at a given named anchor. */

View File

@@ -16,6 +16,7 @@ package com.google.gerrit.server.index.change;
import static com.google.gerrit.server.git.QueueProvider.QueueType.BATCH;
import com.google.common.base.Objects;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -40,7 +41,9 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Config;
@@ -70,6 +73,11 @@ public class ChangeIndexer {
private final StalenessChecker stalenessChecker;
private final boolean autoReindexIfStale;
private final Set<IndexTask> queuedIndexTasks =
Collections.newSetFromMap(new ConcurrentHashMap<>());
private final Set<ReindexIfStaleTask> queuedReindexIfStaleTasks =
Collections.newSetFromMap(new ConcurrentHashMap<>());
@AssistedInject
ChangeIndexer(
@GerritServerConfig Config cfg,
@@ -123,7 +131,11 @@ public class ChangeIndexer {
* @return future for the indexing task.
*/
public ListenableFuture<?> indexAsync(Project.NameKey project, Change.Id id) {
return submit(new IndexTask(project, id));
IndexTask task = new IndexTask(project, id);
if (queuedIndexTasks.add(task)) {
return submit(task);
}
return Futures.immediateFuture(null);
}
/**
@@ -242,7 +254,11 @@ public class ChangeIndexer {
* @return future for reindexing the change; returns true if the change was stale.
*/
public ListenableFuture<Boolean> reindexIfStale(Project.NameKey project, Change.Id id) {
return submit(new ReindexIfStaleTask(project, id), batchExecutor);
ReindexIfStaleTask task = new ReindexIfStaleTask(project, id);
if (queuedReindexIfStaleTasks.add(task)) {
return submit(task, batchExecutor);
}
return Futures.immediateFuture(false);
}
private void autoReindexIfStale(ChangeData cd) {
@@ -281,6 +297,8 @@ public class ChangeIndexer {
protected abstract T callImpl() throws Exception;
protected abstract void remove();
@Override
public abstract String toString();
@@ -311,15 +329,35 @@ public class ChangeIndexer {
@Override
public Void callImpl() throws Exception {
remove();
ChangeData cd = changeDataFactory.create(project, id);
index(cd);
return null;
}
@Override
public int hashCode() {
return Objects.hashCode(IndexTask.class, id.get());
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof IndexTask)) {
return false;
}
IndexTask other = (IndexTask) obj;
return id.get() == other.id.get();
}
@Override
public String toString() {
return "index-change-" + id;
}
@Override
protected void remove() {
queuedIndexTasks.remove(this);
}
}
// Not AbstractIndexTask as it doesn't need a request context.
@@ -359,6 +397,7 @@ public class ChangeIndexer {
@Override
public Boolean callImpl() throws Exception {
remove();
try {
if (stalenessChecker.isStale(id)) {
indexImpl(changeDataFactory.create(project, id));
@@ -375,10 +414,29 @@ public class ChangeIndexer {
return false;
}
@Override
public int hashCode() {
return Objects.hashCode(ReindexIfStaleTask.class, id.get());
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof ReindexIfStaleTask)) {
return false;
}
ReindexIfStaleTask other = (ReindexIfStaleTask) obj;
return id.get() == other.id.get();
}
@Override
public String toString() {
return "reindex-if-stale-change-" + id;
}
@Override
protected void remove() {
queuedReindexIfStaleTasks.remove(this);
}
}
private boolean isCausedByRepositoryNotFoundException(Throwable throwable) {

View File

@@ -17,6 +17,7 @@ package com.google.gerrit.server.index.change;
import static com.google.common.util.concurrent.MoreExecutors.directExecutor;
import static com.google.gerrit.server.query.change.ChangeData.asChanges;
import com.google.common.base.Objects;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
@@ -42,8 +43,11 @@ import com.google.gerrit.server.util.RequestContext;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import org.eclipse.jgit.lib.Config;
@@ -61,6 +65,8 @@ public class ReindexAfterRefUpdate implements GitReferenceUpdatedListener {
private final ListeningExecutorService executor;
private final boolean enabled;
private final Set<Index> queuedIndexTasks = Collections.newSetFromMap(new ConcurrentHashMap<>());
@Inject
ReindexAfterRefUpdate(
@GerritServerConfig Config cfg,
@@ -107,9 +113,12 @@ public class ReindexAfterRefUpdate implements GitReferenceUpdatedListener {
@Override
public void onSuccess(List<Change> changes) {
for (Change c : changes) {
// Don't retry indefinitely; if this fails changes may be stale.
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError = executor.submit(new Index(event, c.getId()));
Index task = new Index(event, c.getId());
if (queuedIndexTasks.add(task)) {
// Don't retry indefinitely; if this fails changes may be stale.
@SuppressWarnings("unused")
Future<?> possiblyIgnoredError = executor.submit(task);
}
}
}
@@ -139,6 +148,8 @@ public class ReindexAfterRefUpdate implements GitReferenceUpdatedListener {
}
protected abstract V impl(RequestContext ctx) throws Exception;
protected abstract void remove();
}
private class GetChanges extends Task<List<Change>> {
@@ -163,6 +174,9 @@ public class ReindexAfterRefUpdate implements GitReferenceUpdatedListener {
+ " update of project "
+ event.getProjectName();
}
@Override
protected void remove() {}
}
private class Index extends Task<Void> {
@@ -176,6 +190,7 @@ public class ReindexAfterRefUpdate implements GitReferenceUpdatedListener {
@Override
protected Void impl(RequestContext ctx) throws IOException {
// Reload change, as some time may have passed since GetChanges.
remove();
try {
Change c =
notesFactory.createChecked(Project.nameKey(event.getProjectName()), id).getChange();
@@ -186,9 +201,28 @@ public class ReindexAfterRefUpdate implements GitReferenceUpdatedListener {
return null;
}
@Override
public int hashCode() {
return Objects.hashCode(Index.class, id.get());
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Index)) {
return false;
}
Index other = (Index) obj;
return id.get() == other.id.get();
}
@Override
public String toString() {
return "Index change " + id.get() + " of project " + event.getProjectName();
}
@Override
protected void remove() {
queuedIndexTasks.remove(this);
}
}
}

View File

@@ -75,21 +75,19 @@ public class CommentSender extends ReplyToChangeSender {
public List<Comment> comments = new ArrayList<>();
/** @return a web link to the given patch set and file. */
public String getLink() {
String url = getGerritUrl();
if (url == null) {
return null;
}
public String getFileLink() {
return args.urlFormatter
.get()
.getPatchFileView(change, patchSetId, KeyUtil.encode(filename))
.orElse(null);
}
return new StringBuilder()
.append(url)
.append("#/c/")
.append(change.getId())
.append('/')
.append(patchSetId)
.append('/')
.append(KeyUtil.encode(filename))
.toString();
/** @return a web link to a comment within a given patch set and file. */
public String getCommentLink(short side, int startLine) {
return args.urlFormatter
.get()
.getInlineCommentView(change, patchSetId, KeyUtil.encode(filename), side, startLine)
.orElse(null);
}
/**
@@ -391,7 +389,7 @@ public class CommentSender extends ReplyToChangeSender {
for (CommentSender.FileCommentGroup group : getGroupedInlineComments(repo)) {
Map<String, Object> groupData = new HashMap<>();
groupData.put("link", group.getLink());
groupData.put("link", group.getFileLink());
groupData.put("title", group.getTitle());
groupData.put("patchSetId", group.patchSetId);
@@ -420,11 +418,9 @@ public class CommentSender extends ReplyToChangeSender {
// Set the comment link.
if (comment.lineNbr == 0) {
commentData.put("link", group.getLink());
} else if (comment.side == 0) {
commentData.put("link", group.getLink() + "@a" + startLine);
commentData.put("link", group.getFileLink());
} else {
commentData.put("link", group.getLink() + '@' + startLine);
commentData.put("link", group.getCommentLink(comment.side, startLine));
}
// Set robot comment data.

View File

@@ -282,16 +282,10 @@ public abstract class OutgoingEmail {
}
public String getSettingsUrl() {
if (getGerritUrl() != null) {
final StringBuilder r = new StringBuilder();
r.append(getGerritUrl());
r.append("settings");
return r.toString();
}
return null;
return args.urlFormatter.get().getSettingsUrl().orElse(null);
}
public String getGerritUrl() {
private String getGerritUrl() {
return args.urlFormatter.get().getWebUrl().orElse(null);
}

View File

@@ -15,7 +15,10 @@
package com.google.gerrit.server.permissions;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.gerrit.common.data.AccessSection.ALL;
import static com.google.gerrit.common.data.AccessSection.REGEX_PREFIX;
import static com.google.gerrit.reviewdb.client.RefNames.REFS_TAGS;
import static com.google.gerrit.server.util.MagicBranch.NEW_CHANGE;
import com.google.common.collect.Sets;
import com.google.gerrit.common.data.AccessSection;
@@ -139,7 +142,7 @@ class ProjectControl {
/** Is this user a project owner? */
boolean isOwner() {
return (isDeclaredOwner() && controlForRef("refs/*").canPerform(Permission.OWNER)) || isAdmin();
return (isDeclaredOwner() && controlForRef(ALL).canPerform(Permission.OWNER)) || isAdmin();
}
/**
@@ -200,7 +203,8 @@ class ProjectControl {
private boolean canCreateChanges() {
for (SectionMatcher matcher : access()) {
AccessSection section = matcher.getSection();
if (section.getName().startsWith("refs/for/")) {
if (section.getName().startsWith(NEW_CHANGE)
|| section.getName().startsWith(REGEX_PREFIX + NEW_CHANGE)) {
Permission permission = section.getPermission(Permission.PUSH);
if (permission != null && controlForRef(section.getName()).canPerform(Permission.PUSH)) {
return true;
@@ -222,7 +226,8 @@ class ProjectControl {
for (SectionMatcher matcher : access()) {
AccessSection section = matcher.getSection();
if (section.getName().startsWith(REFS_TAGS)) {
if (section.getName().startsWith(REFS_TAGS)
|| section.getName().startsWith(REGEX_PREFIX + REFS_TAGS)) {
Permission permission = section.getPermission(permissionName);
if (permission == null) {
continue;
@@ -276,7 +281,7 @@ class ProjectControl {
private boolean canPerformOnAllRefs(String permission, Set<String> ignore) {
boolean canPerform = false;
Set<String> patterns = allRefPatterns(permission);
if (patterns.contains(AccessSection.ALL)) {
if (patterns.contains(ALL)) {
// Only possible if granted on the pattern that
// matches every possible reference. Check all
// patterns also have the permission.

View File

@@ -84,6 +84,8 @@ import com.google.gerrit.extensions.common.EditInfo;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.testing.EditInfoSubject;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.git.ObjectIds;
import com.google.gerrit.mail.Address;
import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -94,8 +96,12 @@ import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.events.CommitReceivedEvent;
import com.google.gerrit.server.git.receive.NoteDbPushOption;
import com.google.gerrit.server.git.receive.ReceiveConstants;
import com.google.gerrit.server.git.validators.CommitValidationException;
import com.google.gerrit.server.git.validators.CommitValidationListener;
import com.google.gerrit.server.git.validators.CommitValidationMessage;
import com.google.gerrit.server.git.validators.CommitValidators.ChangeIdValidator;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.gerrit.server.project.testing.TestLabels;
@@ -105,6 +111,7 @@ import com.google.gerrit.testing.TestTimeUtil;
import com.google.inject.Inject;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashMap;
@@ -112,6 +119,7 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import org.eclipse.jgit.api.errors.GitAPIException;
@@ -152,6 +160,8 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
private static String NEW_CHANGE_INDICATOR = " [NEW]";
private LabelType patchSetLock;
@Inject private DynamicSet<CommitValidationListener> commitValidators;
@BeforeClass
public static void setTimeForTesting() {
TestTimeUtil.resetWithClockStep(1, SECONDS);
@@ -2330,6 +2340,57 @@ public abstract class AbstractPushForReview extends AbstractDaemonTest {
.isEqualTo(Iterables.getLast(commits).name());
}
private static class TestValidator implements CommitValidationListener {
private final AtomicInteger count = new AtomicInteger();
@Override
public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
throws CommitValidationException {
count.incrementAndGet();
return Collections.emptyList();
}
public int count() {
return count.get();
}
}
@Test
public void skipValidation() throws Exception {
String master = "refs/heads/master";
TestValidator validator = new TestValidator();
RegistrationHandle handle = commitValidators.add("test-validator", validator);
try {
// Validation listener is called on normal push
PushOneCommit push =
pushFactory.create(admin.newIdent(), testRepo, "change1", "a.txt", "content");
PushOneCommit.Result r = push.to(master);
r.assertOkStatus();
assertThat(validator.count()).isEqualTo(1);
// Push is rejected and validation listener is not called when not allowed
// to use skip option
PushOneCommit push2 =
pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
push2.setPushOptions(ImmutableList.of(PUSH_OPTION_SKIP_VALIDATION));
r = push2.to(master);
r.assertErrorStatus("not permitted: skip validation");
assertThat(validator.count()).isEqualTo(1);
// Validation listener is not called when skip option is used
grantSkipValidation(project, master, SystemGroupBackend.REGISTERED_USERS);
PushOneCommit push3 =
pushFactory.create(admin.newIdent(), testRepo, "change2", "b.txt", "content");
push3.setPushOptions(ImmutableList.of(PUSH_OPTION_SKIP_VALIDATION));
r = push3.to(master);
r.assertOkStatus();
assertThat(validator.count()).isEqualTo(1);
} finally {
handle.remove();
}
}
@Test
public void pushNoteDbRef() throws Exception {
String ref = "refs/changes/34/1234/meta";

View File

@@ -653,13 +653,17 @@ public class CommentsIT extends AbstractDaemonTest {
+ "comments\n"
+ "\n"
+ url
+ "#/c/"
+ "c/"
+ project.get()
+ "/+/"
+ c
+ "/1/a.txt \n"
+ "File a.txt:\n"
+ "\n"
+ url
+ "#/c/"
+ "c/"
+ project.get()
+ "/+/"
+ c
+ "/1/a.txt@a2 \n"
+ "PS1, Line 2: \n"
@@ -667,7 +671,9 @@ public class CommentsIT extends AbstractDaemonTest {
+ "\n"
+ "\n"
+ url
+ "#/c/"
+ "c/"
+ project.get()
+ "/+/"
+ c
+ "/1/a.txt@1 \n"
+ "PS1, Line 1: boring\n"
@@ -675,13 +681,17 @@ public class CommentsIT extends AbstractDaemonTest {
+ "\n"
+ "\n"
+ url
+ "#/c/"
+ "c/"
+ project.get()
+ "/+/"
+ c
+ "/2/a.txt \n"
+ "File a.txt:\n"
+ "\n"
+ url
+ "#/c/"
+ "c/"
+ project.get()
+ "/+/"
+ c
+ "/2/a.txt@a1 \n"
+ "PS2, Line 1: \n"
@@ -689,7 +699,9 @@ public class CommentsIT extends AbstractDaemonTest {
+ "\n"
+ "\n"
+ url
+ "#/c/"
+ "c/"
+ project.get()
+ "/+/"
+ c
+ "/2/a.txt@a2 \n"
+ "PS2, Line 2: \n"
@@ -697,7 +709,9 @@ public class CommentsIT extends AbstractDaemonTest {
+ "\n"
+ "\n"
+ url
+ "#/c/"
+ "c/"
+ project.get()
+ "/+/"
+ c
+ "/2/a.txt@1 \n"
+ "PS2, Line 1: interesting\n"
@@ -705,7 +719,9 @@ public class CommentsIT extends AbstractDaemonTest {
+ "\n"
+ "\n"
+ url
+ "#/c/"
+ "c/"
+ project.get()
+ "/+/"
+ c
+ "/2/a.txt@2 \n"
+ "PS2, Line 2: nten\n"

View File

@@ -16,12 +16,14 @@ limitations under the License.
-->
<link rel="import" href="/bower_components/polymer/polymer.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../core/gr-navigation/gr-navigation.html">
<link rel="import" href="../../plugins/gr-endpoint-decorator/gr-endpoint-decorator.html">
<link rel="import" href="../../plugins/gr-endpoint-param/gr-endpoint-param.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">
<link rel="import" href="../../../styles/dashboard-header-styles.html">
<link rel="import" href="../../../styles/shared-styles.html">
<dom-module id="gr-user-header">
<template>
@@ -62,6 +64,12 @@ limitations under the License.
date-str="[[_computeDetail(_accountDetails, 'registered_on')]]">
</gr-date-formatter>
</div>
<gr-endpoint-decorator name="user-header">
<gr-endpoint-param name="accountDetails" value="[[_accountDetails]]">
</gr-endpoint-param>
<gr-endpoint-param name="loggedIn" value="[[loggedIn]]">
</gr-endpoint-param>
</gr-endpoint-decorator>
</div>
<div class="info">
<div class$="[[_computeDashboardLinkClass(showDashboardLink, loggedIn)]]">