Merge "Test tracing git push for a project via config"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 6b3b79f..0c218c8 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -5976,6 +5976,19 @@
+
By default, unset (all projects are matched).
+[[tracing.metric]]
+==== Subsection tracing.metric
+
+Section to control for which operations latency and counts should be recorded
+in the link:metrics.html#performance[performance metrics].
+
+[[tracing.metric.operation]]tracing.metric.operation::
++
+Name of a Gerrit operation for which latency and counts should be recorded in
+the link:metrics.html#performance[performance metrics].
++
+The operation name must match the operation name that is used with TraceTimer.
+
[[deadline.id]]
==== Subsection deadline.<id>
diff --git a/Documentation/metrics.txt b/Documentation/metrics.txt
index 3b56391..a9b82d6 100644
--- a/Documentation/metrics.txt
+++ b/Documentation/metrics.txt
@@ -73,6 +73,30 @@
** `cancellation_type`:
The cancellation type (graceful or forceful).
+[[performance]]
+=== Performance
+
+* `performance/operations`: Latency of performing operations
+** `operation_name`:
+ The operation that was performed.
+** `request`:
+ The request for which the operation was performed (format = '<request-type>
+ <redacted-request-uri>').
+** `plugin`:
+ The name of the plugin that performed the operation.
+* `performance/operations_count`: Number of performed operations
+** `operation_name`:
+ The operation that was performed.
+** `request`:
+ The request for which the operation was performed (format = '<request-type>
+ <redacted-request-uri>').
+** `plugin`:
+ The name of the plugin that performed the operation.
+
+Performance metrics can be enabled via the
+link:config.gerrit.html#tracing.exportPerformanceMetrics[`tracing.exportPerformanceMetrics`]
+setting.
+
=== Pushes
* `receivecommits/changes`: histogram of number of changes processed in a single
@@ -100,8 +124,10 @@
DELETE).
* `receivecommits/reject_count`: number of rejected pushes
** `kind`:
- The push kind ('magic' if it was a push for code review, 'direct' if it was
- a direct push or 'n/a' if the push kind couldn't be detected).
+ The push kind ('magic push'/'magic push by service user' if it was a push for
+ code review, 'direct push'/'direct push by service user' if it was a direct
+ push, 'magic push by service, 'magic or direct push'/'magic or direct push by
+ service user' if the push kind couldn't be detected).
** `reason`:
The rejection reason.
** `status`:
diff --git a/java/com/google/gerrit/metrics/Timer1.java b/java/com/google/gerrit/metrics/Timer1.java
index 9dc32da..cdf457c 100644
--- a/java/com/google/gerrit/metrics/Timer1.java
+++ b/java/com/google/gerrit/metrics/Timer1.java
@@ -54,6 +54,8 @@
}
}
+ private boolean suppressLogging;
+
protected final String name;
protected final Field<F1> field;
@@ -87,15 +89,24 @@
field.metadataMapper().accept(metadataBuilder, fieldValue);
Metadata metadata = metadataBuilder.build();
- LoggingContext.getInstance()
- .addPerformanceLogRecord(() -> PerformanceLogRecord.create(name, durationNanos, metadata));
- logger.atFinest().log(
- "%s (%s = %s) took %.2f ms", name, field.name(), fieldValue, durationNanos / 1000000.0);
+ if (!suppressLogging) {
+ LoggingContext.getInstance()
+ .addPerformanceLogRecord(
+ () -> PerformanceLogRecord.create(name, durationNanos, metadata));
+ logger.atFinest().log(
+ "%s (%s = %s) took %.2f ms", name, field.name(), fieldValue, durationNanos / 1000000.0);
+ }
doRecord(fieldValue, value, unit);
RequestStateContext.abortIfCancelled();
}
+ /** Suppress logging (debug log and performance log) when values are recorded. */
+ public final Timer1<F1> suppressLogging() {
+ this.suppressLogging = true;
+ return this;
+ }
+
/**
* Record a value in the distribution.
*
diff --git a/java/com/google/gerrit/metrics/Timer2.java b/java/com/google/gerrit/metrics/Timer2.java
index 46dd617..9f08711 100644
--- a/java/com/google/gerrit/metrics/Timer2.java
+++ b/java/com/google/gerrit/metrics/Timer2.java
@@ -57,6 +57,8 @@
}
}
+ private boolean suppressLogging;
+
protected final String name;
protected final Field<F1> field1;
protected final Field<F2> field2;
@@ -95,16 +97,25 @@
field2.metadataMapper().accept(metadataBuilder, fieldValue2);
Metadata metadata = metadataBuilder.build();
- LoggingContext.getInstance()
- .addPerformanceLogRecord(() -> PerformanceLogRecord.create(name, durationNanos, metadata));
- logger.atFinest().log(
- "%s (%s = %s, %s = %s) took %.2f ms",
- name, field1.name(), fieldValue1, field2.name(), fieldValue2, durationNanos / 1000000.0);
+ if (!suppressLogging) {
+ LoggingContext.getInstance()
+ .addPerformanceLogRecord(
+ () -> PerformanceLogRecord.create(name, durationNanos, metadata));
+ logger.atFinest().log(
+ "%s (%s = %s, %s = %s) took %.2f ms",
+ name, field1.name(), fieldValue1, field2.name(), fieldValue2, durationNanos / 1000000.0);
+ }
doRecord(fieldValue1, fieldValue2, value, unit);
RequestStateContext.abortIfCancelled();
}
+ /** Suppress logging (debug log and performance log) when values are recorded. */
+ public final Timer2<F1, F2> suppressLogging() {
+ this.suppressLogging = true;
+ return this;
+ }
+
/**
* Record a value in the distribution.
*
diff --git a/java/com/google/gerrit/metrics/Timer3.java b/java/com/google/gerrit/metrics/Timer3.java
index 922cdd6..2a01d5d 100644
--- a/java/com/google/gerrit/metrics/Timer3.java
+++ b/java/com/google/gerrit/metrics/Timer3.java
@@ -60,6 +60,8 @@
}
}
+ private boolean suppressLogging;
+
protected final String name;
protected final Field<F1> field1;
protected final Field<F2> field2;
@@ -104,23 +106,32 @@
field3.metadataMapper().accept(metadataBuilder, fieldValue3);
Metadata metadata = metadataBuilder.build();
- LoggingContext.getInstance()
- .addPerformanceLogRecord(() -> PerformanceLogRecord.create(name, durationNanos, metadata));
- logger.atFinest().log(
- "%s (%s = %s, %s = %s, %s = %s) took %.2f ms",
- name,
- field1.name(),
- fieldValue1,
- field2.name(),
- fieldValue2,
- field3.name(),
- fieldValue3,
- durationNanos / 1000000.0);
+ if (!suppressLogging) {
+ LoggingContext.getInstance()
+ .addPerformanceLogRecord(
+ () -> PerformanceLogRecord.create(name, durationNanos, metadata));
+ logger.atFinest().log(
+ "%s (%s = %s, %s = %s, %s = %s) took %.2f ms",
+ name,
+ field1.name(),
+ fieldValue1,
+ field2.name(),
+ fieldValue2,
+ field3.name(),
+ fieldValue3,
+ durationNanos / 1000000.0);
+ }
doRecord(fieldValue1, fieldValue2, fieldValue3, value, unit);
RequestStateContext.abortIfCancelled();
}
+ /** Suppress logging (debug log and performance log) when values are recorded. */
+ public final Timer3<F1, F2, F3> suppressLogging() {
+ this.suppressLogging = true;
+ return this;
+ }
+
/**
* Record a value in the distribution.
*
diff --git a/java/com/google/gerrit/server/PerformanceMetrics.java b/java/com/google/gerrit/server/PerformanceMetrics.java
new file mode 100644
index 0000000..3d21fed
--- /dev/null
+++ b/java/com/google/gerrit/server/PerformanceMetrics.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2021 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;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.metrics.Counter3;
+import com.google.gerrit.metrics.Description;
+import com.google.gerrit.metrics.Field;
+import com.google.gerrit.metrics.MetricMaker;
+import com.google.gerrit.metrics.Timer3;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.logging.Metadata;
+import com.google.gerrit.server.logging.PerformanceLogger;
+import com.google.gerrit.server.logging.TraceContext;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+
+/** Performance logger that records the execution times as a metric. */
+@Singleton
+public class PerformanceMetrics implements PerformanceLogger {
+ private static final String OPERATION_LATENCY_METRIC_NAME = "performance/operations";
+ private static final String OPERATION_COUNT_METRIC_NAME = "performance/operations_count";
+
+ private final ImmutableList<String> tracedOperations;
+ public final Timer3<String, String, String> operationsLatency;
+ public final Counter3<String, String, String> operationsCounter;
+
+ @Inject
+ PerformanceMetrics(@GerritServerConfig Config cfg, MetricMaker metricMaker) {
+ this.tracedOperations =
+ ImmutableList.copyOf(cfg.getStringList("tracing", "metric", "operation"));
+
+ Field<String> operationNameField =
+ Field.ofString(
+ "operation_name",
+ (metadataBuilder, fieldValue) -> metadataBuilder.operationName(fieldValue))
+ .description("The operation that was performed.")
+ .build();
+ Field<String> requestField =
+ Field.ofString("request", (metadataBuilder, fieldValue) -> {})
+ .description(
+ "The request for which the operation was performed"
+ + " (format = '<request-type> <redacted-request-uri>').")
+ .build();
+ Field<String> pluginField =
+ Field.ofString(
+ "plugin", (metadataBuilder, fieldValue) -> metadataBuilder.pluginName(fieldValue))
+ .description("The name of the plugin that performed the operation.")
+ .build();
+
+ this.operationsLatency =
+ metricMaker
+ .newTimer(
+ OPERATION_LATENCY_METRIC_NAME,
+ new Description("Latency of performing operations")
+ .setCumulative()
+ .setUnit(Description.Units.MILLISECONDS),
+ operationNameField,
+ requestField,
+ pluginField)
+ .suppressLogging();
+ this.operationsCounter =
+ metricMaker.newCounter(
+ OPERATION_COUNT_METRIC_NAME,
+ new Description("Number of performed operations").setRate(),
+ operationNameField,
+ requestField,
+ pluginField);
+ }
+
+ @Override
+ public void logNanos(String operation, long durationNanos, Instant endTime) {
+ logNanos(operation, durationNanos, endTime, /* metadata= */ null);
+ }
+
+ @Override
+ public void logNanos(
+ String operation, long durationNanos, Instant endTime, @Nullable Metadata metadata) {
+ if (!tracedOperations.contains(operation)) {
+ return;
+ }
+
+ String requestTag = TraceContext.getTag(TraceRequestListener.TAG_REQUEST).orElse("");
+ String pluginTag = TraceContext.getPluginTag().orElse("");
+ operationsLatency.record(operation, requestTag, pluginTag, durationNanos, TimeUnit.NANOSECONDS);
+ operationsCounter.increment(operation, requestTag, pluginTag);
+ }
+}
diff --git a/java/com/google/gerrit/server/config/GerritGlobalModule.java b/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 2b26956..c4ecc80 100644
--- a/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -89,6 +89,7 @@
import com.google.gerrit.server.ExceptionHookImpl;
import com.google.gerrit.server.ExternalUser;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PerformanceMetrics;
import com.google.gerrit.server.RequestListener;
import com.google.gerrit.server.TraceRequestListener;
import com.google.gerrit.server.account.AccountControl;
@@ -455,6 +456,7 @@
DynamicSet.setOf(binder(), SubmitRequirement.class);
DynamicSet.setOf(binder(), QuotaEnforcer.class);
DynamicSet.setOf(binder(), PerformanceLogger.class);
+ DynamicSet.bind(binder(), PerformanceLogger.class).to(PerformanceMetrics.class);
DynamicSet.setOf(binder(), RequestListener.class);
DynamicSet.bind(binder(), RequestListener.class).to(TraceRequestListener.class);
DynamicSet.setOf(binder(), ExceptionHook.class);
diff --git a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
index 9f7b763..536e1fb 100644
--- a/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
+++ b/java/com/google/gerrit/server/git/receive/ReceiveCommits.java
@@ -124,6 +124,7 @@
import com.google.gerrit.server.Sequences;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.AccountResolver.UnresolvableAccountException;
+import com.google.gerrit.server.account.ServiceUserClassifier;
import com.google.gerrit.server.approval.ApprovalsUtil;
import com.google.gerrit.server.cancellation.RequestCancelledException;
import com.google.gerrit.server.cancellation.RequestStateContext;
@@ -234,6 +235,7 @@
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
+import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@@ -426,6 +428,7 @@
private final Sequences seq;
private final SetHashtagsOp.Factory hashtagsFactory;
private final SetTopicOp.Factory setTopicFactory;
+ private final ServiceUserClassifier serviceUserClassifier;
private final ImmutableList<SubmissionListener> superprojectUpdateSubmissionListeners;
private final TagCache tagCache;
private final ProjectConfig.Factory projectConfigFactory;
@@ -516,6 +519,7 @@
Sequences seq,
SetHashtagsOp.Factory hashtagsFactory,
SetTopicOp.Factory setTopicFactory,
+ ServiceUserClassifier serviceUserClassifier,
@SuperprojectUpdateOnSubmission
ImmutableList<SubmissionListener> superprojectUpdateSubmissionListeners,
TagCache tagCache,
@@ -550,6 +554,7 @@
this.exceptionHooks = exceptionHooks;
this.hashtagsFactory = hashtagsFactory;
this.setTopicFactory = setTopicFactory;
+ this.serviceUserClassifier = serviceUserClassifier;
this.indexer = indexer;
this.initializers = initializers;
this.mergeOpProvider = mergeOpProvider;
@@ -762,8 +767,15 @@
logger.atFine().log("Processing commands done.");
} catch (RuntimeException e) {
String formattedCause = getFormattedCause(e).orElse(e.getClass().getSimpleName());
- logger.atFine().withCause(e).log("ReceiveCommits failed due to %s", formattedCause);
- metrics.rejectCount.increment("n/a", formattedCause, SC_INTERNAL_SERVER_ERROR);
+ int statusCode =
+ getStatus(e).map(ExceptionHook.Status::statusCode).orElse(SC_INTERNAL_SERVER_ERROR);
+ logger.at(statusCode < SC_INTERNAL_SERVER_ERROR ? Level.INFO : Level.SEVERE).withCause(e).log(
+ "ReceiveCommits failed due to %s", formattedCause);
+ String pushKind = "magic or direct push";
+ if (serviceUserClassifier.isServiceUser(user.getAccountId())) {
+ pushKind += " by service user";
+ }
+ metrics.rejectCount.increment(pushKind, formattedCause, statusCode);
throw e;
}
progress.end();
@@ -778,6 +790,14 @@
.findFirst();
}
+ private Optional<ExceptionHook.Status> getStatus(Throwable err) {
+ return exceptionHooks.stream()
+ .map(h -> h.getStatus(err))
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .findFirst();
+ }
+
// Process as many commands as possible, but may leave some commands in state NOT_ATTEMPTED.
private void processCommandsUnsafe(
Collection<ReceiveCommand> commands, MultiProgressMonitor progress) {
@@ -3934,10 +3954,13 @@
private void reject(ReceiveCommand cmd, RejectionReason reason) {
logger.atFine().log("Rejecting command '%s': %s", cmd, reason.why());
- metrics.rejectCount.increment(
- MagicBranch.isMagicBranch(cmd.getRefName()) ? "magic" : "direct",
- reason.metricBucket(),
- reason.statusCode());
+
+ String pushKind = (MagicBranch.isMagicBranch(cmd.getRefName()) ? "magic push" : "direct push");
+ if (serviceUserClassifier.isServiceUser(user.getAccountId())) {
+ pushKind += " by service user";
+ }
+ metrics.rejectCount.increment(pushKind, reason.metricBucket(), reason.statusCode());
+
cmd.setResult(REJECTED_OTHER_REASON, reason.why());
}
diff --git a/java/com/google/gerrit/server/logging/PerformanceLogger.java b/java/com/google/gerrit/server/logging/PerformanceLogger.java
index 2f90dfd..fa61eb4 100644
--- a/java/com/google/gerrit/server/logging/PerformanceLogger.java
+++ b/java/com/google/gerrit/server/logging/PerformanceLogger.java
@@ -16,7 +16,6 @@
import com.google.gerrit.extensions.annotations.ExtensionPoint;
import java.time.Instant;
-import java.util.concurrent.TimeUnit;
/**
* Extension point for logging performance records.
@@ -35,27 +34,6 @@
* Record the execution time of an operation in a performance log.
*
* @param operation operation that was performed
- * @param durationMs time that the execution of the operation took (in milliseconds)
- */
- @Deprecated
- default void log(String operation, long durationMs, Instant endTime) {
- log(operation, durationMs, endTime, Metadata.empty());
- }
-
- /**
- * Record the execution time of an operation in a performance log.
- *
- * @param operation operation that was performed
- * @param durationMs time that the execution of the operation took (in milliseconds)
- * @param metadata metadata
- */
- @Deprecated
- void log(String operation, long durationMs, Instant endTime, Metadata metadata);
-
- /**
- * Record the execution time of an operation in a performance log.
- *
- * @param operation operation that was performed
* @param durationNanos time that the execution of the operation took (in nanoseconds)
*/
default void logNanos(String operation, long durationNanos, Instant endTime) {
@@ -69,7 +47,5 @@
* @param durationNanos time that the execution of the operation took (in nanoseconds)
* @param metadata metadata
*/
- default void logNanos(String operation, long durationNanos, Instant endTime, Metadata metadata) {
- log(operation, TimeUnit.NANOSECONDS.toMillis(durationNanos), endTime, metadata);
- }
+ void logNanos(String operation, long durationNanos, Instant endTime, Metadata metadata);
}
diff --git a/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
index c4497dc..8153a5d 100644
--- a/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
+++ b/javatests/com/google/gerrit/acceptance/ssh/SshTraceIT.java
@@ -124,7 +124,7 @@
private ImmutableList.Builder<PerformanceLogEntry> logEntries = ImmutableList.builder();
@Override
- public void log(String operation, long durationMs, Instant endTime, Metadata metadata) {
+ public void logNanos(String operation, long durationNanos, Instant endTime, Metadata metadata) {
logEntries.add(PerformanceLogEntry.create(operation, endTime, metadata));
}
diff --git a/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java b/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
index c1b9f13..8cdf16a 100644
--- a/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
+++ b/javatests/com/google/gerrit/server/logging/LoggingContextAwareExecutorServiceTest.java
@@ -52,7 +52,8 @@
testPerformanceLogger =
new PerformanceLogger() {
@Override
- public void log(String operation, long durationMs, Instant endTime, Metadata metadata) {
+ public void logNanos(
+ String operation, long durationNanos, Instant endTime, Metadata metadata) {
// do nothing
}
};
diff --git a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
index c93061d..4b3c658 100644
--- a/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
+++ b/javatests/com/google/gerrit/server/logging/PerformanceLogContextTest.java
@@ -365,7 +365,7 @@
private ImmutableList.Builder<PerformanceLogEntry> logEntries = ImmutableList.builder();
@Override
- public void log(String operation, long durationMs, Instant endTime, Metadata metadata) {
+ public void logNanos(String operation, long durationNanos, Instant endTime, Metadata metadata) {
logEntries.add(PerformanceLogEntry.create(operation, metadata));
}
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
index e1fc2ed..1ec5262e 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls.ts
@@ -271,6 +271,16 @@
.breadcrumbTooltip {
white-space: nowrap;
}
+ .unrelatedChanges {
+ color: var(--primary-button-text-color);
+ background-color: var(--primary-button-background-color);
+
+ &:hover {
+ // TODO(anuragpathak): Update hover colors as per specification.
+ color: var(--primary-button-text-color);
+ background-color: var(--primary-button-background-color);
+ }
+ }
`,
];
}
@@ -370,8 +380,14 @@
let classes = 'contextControlButton showContext ';
if (type === ContextButtonType.ALL) {
- text = `+${pluralize(linesToExpand, 'common line')}`;
- ariaLabel = `Show ${pluralize(linesToExpand, 'common line')}`;
+ if (this.group.hasNonCommonDeltaGroup()) {
+ text = '+ Unrelated changes';
+ ariaLabel = 'Show unrelated changes';
+ classes += ' unrelatedChanges ';
+ } else {
+ text = `+${pluralize(linesToExpand, 'common line')}`;
+ ariaLabel = `Show ${pluralize(linesToExpand, 'common line')}`;
+ }
classes += this.showBoth()
? 'centeredButton'
: this.showAbove()
@@ -483,7 +499,7 @@
* Creates a container div with partial (+10) expansion buttons (above and/or below).
*/
private createPartialExpansionButtons() {
- if (!this.showPartialLinks()) {
+ if (!this.showPartialLinks() || this.group?.hasNonCommonDeltaGroup()) {
return undefined;
}
let aboveButton;
@@ -515,7 +531,8 @@
if (
!this.showPartialLinks() ||
!this.renderPreferences?.use_block_expansion ||
- this.group?.hasSkipGroup()
+ this.group?.hasSkipGroup() ||
+ this.group?.hasNonCommonDeltaGroup()
) {
return undefined;
}
diff --git a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls_test.ts b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls_test.ts
index 20fc9c4..74726bf 100644
--- a/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-context-controls/gr-context-controls_test.ts
@@ -10,7 +10,10 @@
import {SyntaxBlock} from '../../../api/diff';
import {fixture, html, assert} from '@open-wc/testing';
import {waitEventLoop} from '../../../test/test-utils';
-import {createContextGroup} from '../../../test/test-data-generators';
+import {
+ createContextGroup,
+ createContextGroupWithDelta,
+} from '../../../test/test-data-generators';
suite('gr-context-control tests', () => {
let element: GrContextControls;
@@ -333,4 +336,16 @@
assert.equal(tooltipAbove.getAttribute('position'), 'top');
assert.equal(tooltipBelow.getAttribute('position'), 'bottom');
});
+
+ test('context control with delta group', async () => {
+ element.group = createContextGroupWithDelta();
+ await waitEventLoop();
+
+ const buttons = element.shadowRoot!.querySelectorAll(
+ 'paper-button.showContext'
+ );
+ assert.equal(buttons.length, 1);
+ assert.equal(buttons[0].textContent!.trim(), '+ Unrelated changes');
+ assert.include([...buttons[0].classList.values()], 'unrelatedChanges');
+ });
});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
index c5366e7..c7e9f44 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group.ts
@@ -165,7 +165,8 @@
* Groups where all lines are before or all lines are after the split will be
* retained as is and put into the first or second list respectively. Groups
* with some lines before and some lines after the split will be split into
- * two groups, which will be put into the first and second list.
+ * two groups, which will be put into the first and second list. Groups with
+ * type DELTA which are not common will not be split.
*
* @param split A line number offset relative to the first group's
* start line at which the groups should be split.
@@ -179,31 +180,41 @@
if (groups.length === 0) return [[], []];
const leftSplit = groups[0].lineRange.left.start_line + split;
const rightSplit = groups[0].lineRange.right.start_line + split;
-
+ let isSplitDone = false;
const beforeGroups = [];
const afterGroups = [];
for (const group of groups) {
- const isCompletelyBefore =
- group.lineRange.left.end_line < leftSplit ||
- group.lineRange.right.end_line < rightSplit;
- const isCompletelyAfter =
- leftSplit <= group.lineRange.left.start_line ||
- rightSplit <= group.lineRange.right.start_line;
- if (isCompletelyBefore) {
- beforeGroups.push(group);
- } else if (isCompletelyAfter) {
+ if (isSplitDone) {
afterGroups.push(group);
+ } else if (
+ group.type === GrDiffGroupType.DELTA &&
+ !group.ignoredWhitespaceOnly
+ ) {
+ beforeGroups.push(group);
} else {
- const {beforeSplit, afterSplit} = splitGroupInTwo(
- group,
- leftSplit,
- rightSplit
- );
- if (beforeSplit) {
- beforeGroups.push(beforeSplit);
- }
- if (afterSplit) {
- afterGroups.push(afterSplit);
+ const isCompletelyBefore =
+ group.lineRange.left.end_line < leftSplit ||
+ group.lineRange.right.end_line < rightSplit;
+ const isCompletelyAfter =
+ leftSplit <= group.lineRange.left.start_line ||
+ rightSplit <= group.lineRange.right.start_line;
+ if (isCompletelyBefore) {
+ beforeGroups.push(group);
+ } else if (isCompletelyAfter) {
+ afterGroups.push(group);
+ } else {
+ const {beforeSplit, afterSplit} = splitGroupInTwo(
+ group,
+ leftSplit,
+ rightSplit
+ );
+ if (beforeSplit) {
+ beforeGroups.push(beforeSplit);
+ }
+ if (afterSplit) {
+ afterGroups.push(afterSplit);
+ }
+ isSplitDone = true;
}
}
}
@@ -438,6 +449,15 @@
);
}
+ /** Returns true if it contains a DELTA group excluding whitespace only
+ * changes.
+ */
+ hasNonCommonDeltaGroup() {
+ return this.contextGroups?.some(
+ g => g.type === GrDiffGroupType.DELTA && !g.ignoredWhitespaceOnly
+ );
+ }
+
containsLine(side: Side, line: LineNumber) {
if (typeof line !== 'number') {
// For FILE and LOST, beforeNumber and afterNumber are the same
diff --git a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts
index bbbb4ad..c8446f0 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff/gr-diff-group_test.ts
@@ -97,6 +97,8 @@
suite('hideInContextControl', () => {
let groups: GrDiffGroup[];
+ let groupsWithDelta: GrDiffGroup[];
+ let groupsWithWhiteSpaceOnlyChange: GrDiffGroup[];
setup(() => {
groups = [
new GrDiffGroup({
@@ -108,6 +110,46 @@
],
}),
new GrDiffGroup({
+ type: GrDiffGroupType.BOTH,
+ lines: [
+ new GrDiffLine(GrDiffLineType.BOTH, 8, 10),
+ new GrDiffLine(GrDiffLineType.BOTH, 9, 11),
+ new GrDiffLine(GrDiffLineType.BOTH, 10, 12),
+ new GrDiffLine(GrDiffLineType.BOTH, 11, 13),
+ ],
+ }),
+ new GrDiffGroup({
+ type: GrDiffGroupType.BOTH,
+ lines: [
+ new GrDiffLine(GrDiffLineType.BOTH, 12, 14),
+ new GrDiffLine(GrDiffLineType.BOTH, 13, 15),
+ new GrDiffLine(GrDiffLineType.BOTH, 14, 16),
+ ],
+ }),
+ ];
+
+ groupsWithWhiteSpaceOnlyChange = [
+ groups[0],
+ new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines: [
+ new GrDiffLine(GrDiffLineType.REMOVE, 8),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 10),
+ new GrDiffLine(GrDiffLineType.REMOVE, 9),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 11),
+ new GrDiffLine(GrDiffLineType.REMOVE, 10),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 12),
+ new GrDiffLine(GrDiffLineType.REMOVE, 11),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 13),
+ ],
+ ignoredWhitespaceOnly: true,
+ }),
+ groups[2],
+ ];
+
+ groupsWithDelta = [
+ groups[0],
+ new GrDiffGroup({
type: GrDiffGroupType.DELTA,
lines: [
new GrDiffLine(GrDiffLineType.REMOVE, 8),
@@ -120,14 +162,7 @@
new GrDiffLine(GrDiffLineType.ADD, 0, 13),
],
}),
- new GrDiffGroup({
- type: GrDiffGroupType.BOTH,
- lines: [
- new GrDiffLine(GrDiffLineType.BOTH, 12, 14),
- new GrDiffLine(GrDiffLineType.BOTH, 13, 15),
- new GrDiffLine(GrDiffLineType.BOTH, 14, 16),
- ],
- }),
+ groups[2],
];
});
@@ -144,29 +179,33 @@
assert.equal(collapsedGroups[2], groups[2]);
});
+ test('does not hides when split is at delta group in context control', () => {
+ const collapsedGroups = hideInContextControl(groupsWithDelta, 3, 7);
+ assert.equal(collapsedGroups.length, 3);
+
+ assert.equal(collapsedGroups[0], groupsWithDelta[0]);
+ assert.equal(collapsedGroups[1], groupsWithDelta[1]);
+ assert.equal(collapsedGroups[2], groupsWithDelta[2]);
+ });
+
test('splits partially hidden groups', () => {
const collapsedGroups = hideInContextControl(groups, 4, 8);
assert.equal(collapsedGroups.length, 4);
assert.equal(collapsedGroups[0], groups[0]);
- assert.equal(collapsedGroups[1].type, GrDiffGroupType.DELTA);
- assert.deepEqual(collapsedGroups[1].adds, [groups[1].adds[0]]);
- assert.deepEqual(collapsedGroups[1].removes, [groups[1].removes[0]]);
+ assert.equal(collapsedGroups[1].type, GrDiffGroupType.BOTH);
+ assert.deepEqual(collapsedGroups[1].lines, [groups[1].lines[0]]);
assert.equal(collapsedGroups[2].type, GrDiffGroupType.CONTEXT_CONTROL);
assert.equal(collapsedGroups[2].contextGroups.length, 2);
assert.equal(
collapsedGroups[2].contextGroups[0].type,
- GrDiffGroupType.DELTA
+ GrDiffGroupType.BOTH
);
assert.deepEqual(
- collapsedGroups[2].contextGroups[0].adds,
- groups[1].adds.slice(1)
- );
- assert.deepEqual(
- collapsedGroups[2].contextGroups[0].removes,
- groups[1].removes.slice(1)
+ collapsedGroups[2].contextGroups[0].lines,
+ groups[1].lines.slice(1)
);
assert.equal(
@@ -181,6 +220,54 @@
assert.deepEqual(collapsedGroups[3].lines, groups[2].lines.slice(1));
});
+ test('splits partially hidden common delta groups', () => {
+ const collapsedGroups = hideInContextControl(
+ groupsWithWhiteSpaceOnlyChange,
+ 4,
+ 8
+ );
+ assert.equal(collapsedGroups.length, 4);
+ assert.equal(collapsedGroups[0], groupsWithWhiteSpaceOnlyChange[0]);
+
+ assert.equal(collapsedGroups[1].type, GrDiffGroupType.DELTA);
+ assert.deepEqual(collapsedGroups[1].adds, [
+ groupsWithWhiteSpaceOnlyChange[1].adds[0],
+ ]);
+ assert.deepEqual(collapsedGroups[1].removes, [
+ groupsWithWhiteSpaceOnlyChange[1].removes[0],
+ ]);
+
+ assert.equal(collapsedGroups[2].type, GrDiffGroupType.CONTEXT_CONTROL);
+ assert.equal(collapsedGroups[2].contextGroups.length, 2);
+
+ assert.equal(
+ collapsedGroups[2].contextGroups[0].type,
+ GrDiffGroupType.DELTA
+ );
+ assert.deepEqual(
+ collapsedGroups[2].contextGroups[0].adds,
+ groupsWithWhiteSpaceOnlyChange[1].adds.slice(1)
+ );
+ assert.deepEqual(
+ collapsedGroups[2].contextGroups[0].removes,
+ groupsWithWhiteSpaceOnlyChange[1].removes.slice(1)
+ );
+
+ assert.equal(
+ collapsedGroups[2].contextGroups[1].type,
+ GrDiffGroupType.BOTH
+ );
+ assert.deepEqual(collapsedGroups[2].contextGroups[1].lines, [
+ groupsWithWhiteSpaceOnlyChange[2].lines[0],
+ ]);
+
+ assert.equal(collapsedGroups[3].type, GrDiffGroupType.BOTH);
+ assert.deepEqual(
+ collapsedGroups[3].lines,
+ groupsWithWhiteSpaceOnlyChange[2].lines.slice(1)
+ );
+ });
+
suite('with skip chunks', () => {
setup(() => {
const skipGroup = new GrDiffGroup({
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index 22dbe7c1..2309977 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -688,6 +688,27 @@
});
}
+export function createContextGroupWithDelta() {
+ return new GrDiffGroup({
+ type: GrDiffGroupType.CONTEXT_CONTROL,
+ contextGroups: [
+ new GrDiffGroup({
+ type: GrDiffGroupType.DELTA,
+ lines: [
+ new GrDiffLine(GrDiffLineType.REMOVE, 8),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 10),
+ new GrDiffLine(GrDiffLineType.REMOVE, 9),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 11),
+ new GrDiffLine(GrDiffLineType.REMOVE, 10),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 12),
+ new GrDiffLine(GrDiffLineType.REMOVE, 11),
+ new GrDiffLine(GrDiffLineType.ADD, 0, 13),
+ ],
+ }),
+ ],
+ });
+}
+
export function createBlame(): BlameInfo {
return {
author: 'test-author',