Merge changes I1d7420de,I819f24cd,I6e8ecbe9,I2e01c81b
* changes:
Get rid of a special test-only request context and related methods.
Remove SshSession from test Context.
Remove unused code from the AcceptanceTestRequestScope
Remove unused classes and no-op methods from tests
diff --git a/Documentation/cmd-ls-projects.txt b/Documentation/cmd-ls-projects.txt
index ebd365a..72e7630 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -16,6 +16,10 @@
[--limit <N>]
[--prefix | -p <prefix>]
[--has-acl-for GROUP]
+ [--match | -m]
+ [-r REGEX]
+ [--start | -S]
+ [--state | -s ]
--
== DESCRIPTION
@@ -58,6 +62,22 @@
Displays project inheritance in a tree-like format.
This option does not work together with the show-branch option.
+--match::
+-m
+ Match project substring
+
+-r::
+ Match project regex
+
+--start::
+-S::
+ Number of projects to skip
+
+--state::
+-s::
+ Filter by project state. [ACTIVE | READON_ONLY | HIDDEN]
+
+
[NOTE]
If the calling user does not meet any of the following criteria:
diff --git a/java/com/google/gerrit/server/submit/MergeOp.java b/java/com/google/gerrit/server/submit/MergeOp.java
index c1e8093..1e76737 100644
--- a/java/com/google/gerrit/server/submit/MergeOp.java
+++ b/java/com/google/gerrit/server/submit/MergeOp.java
@@ -1012,6 +1012,14 @@
implicitMergesRoots.remove(entry.getKey());
continue;
}
+ if (entry.getValue() == null) {
+ logger.atSevere().log("The entry value is null for the key %s", entry.getKey());
+ }
+ rw.parseBody(entry.getValue());
+ if (entry.getValue().getParents() == null) {
+ logger.atSevere().log(
+ "The entry value has null parents. The value is: %s", entry.getValue());
+ }
for (RevCommit parent : entry.getValue().getParents()) {
reachableCommits.push(Map.entry(entry.getKey(), parent));
}
diff --git a/java/com/google/gerrit/sshd/SshDaemon.java b/java/com/google/gerrit/sshd/SshDaemon.java
index cc35a32..af7d22b 100644
--- a/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/java/com/google/gerrit/sshd/SshDaemon.java
@@ -77,6 +77,7 @@
import org.apache.sshd.common.forward.DefaultForwarderFactory;
import org.apache.sshd.common.future.CloseFuture;
import org.apache.sshd.common.future.SshFutureListener;
+import org.apache.sshd.common.global.KeepAliveHandler;
import org.apache.sshd.common.io.AbstractIoServiceFactory;
import org.apache.sshd.common.io.IoAcceptor;
import org.apache.sshd.common.io.IoServiceFactory;
@@ -109,7 +110,6 @@
import org.apache.sshd.server.command.CommandFactory;
import org.apache.sshd.server.forward.ForwardingFilter;
import org.apache.sshd.server.global.CancelTcpipForwardHandler;
-import org.apache.sshd.server.global.KeepAliveHandler;
import org.apache.sshd.server.global.NoMoreSessionsHandler;
import org.apache.sshd.server.global.TcpipForwardHandler;
import org.apache.sshd.server.session.ServerSessionImpl;
diff --git a/plugins/delete-project b/plugins/delete-project
index f046ac6..75b79c1 160000
--- a/plugins/delete-project
+++ b/plugins/delete-project
@@ -1 +1 @@
-Subproject commit f046ac6773ea1c6e10b5e95b763ae685c24ec7f1
+Subproject commit 75b79c115ba3dd03aa56a3165e6c319fa30c6c4a
diff --git a/polygerrit-ui/app/api/diff.ts b/polygerrit-ui/app/api/diff.ts
index 24110ed..092375a 100644
--- a/polygerrit-ui/app/api/diff.ts
+++ b/polygerrit-ui/app/api/diff.ts
@@ -255,6 +255,11 @@
show_newline_warning_left?: boolean;
show_newline_warning_right?: boolean;
use_new_image_diff_ui?: boolean;
+ /**
+ * Temporary flag for switching over to a simplified version of the diff
+ * processor.
+ */
+ use_simplified_processor?: boolean;
}
/**
diff --git a/polygerrit-ui/app/api/rest-api.ts b/polygerrit-ui/app/api/rest-api.ts
index ebc1f88..dd79542 100644
--- a/polygerrit-ui/app/api/rest-api.ts
+++ b/polygerrit-ui/app/api/rest-api.ts
@@ -517,31 +517,6 @@
}
/**
- * The CommentInfo entity contains information about an inline comment.
- * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-info
- */
-export interface CommentInfo {
- id: UrlEncodedCommentId;
- updated: Timestamp;
- // TODO(TS): Make this required. Every comment must have patch_set set.
- patch_set?: RevisionPatchSetNum;
- path?: string;
- side?: CommentSide;
- parent?: number;
- line?: number;
- range?: CommentRange;
- in_reply_to?: UrlEncodedCommentId;
- message?: string;
- author?: AccountInfo;
- tag?: string;
- unresolved?: boolean;
- change_message_id?: string;
- commit_id?: string;
- context_lines?: ContextLine[];
- source_content_type?: string;
-}
-
-/**
* The CommentRange entity describes the range of an inline comment.
* https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-range
*
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
index 00f7639..6770d1c 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
+++ b/polygerrit-ui/app/elements/diff/gr-diff-host/gr-diff-host.ts
@@ -326,6 +326,12 @@
resolve(this, highlightServiceToken),
() => getAppContext().reportingService
);
+ this.renderPrefs = {
+ ...this.renderPrefs,
+ use_simplified_processor: this.flags.isEnabled(
+ KnownExperimentId.SIMPLIFIED_DIFF_PROCESSOR
+ ),
+ };
this.addEventListener(
// These are named inconsistently for a reason:
// The create-comment event is fired to indicate that we should
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index d553130..05ac69b 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -660,6 +660,7 @@
${this.renderHumanActions()} ${this.renderRobotActions()}
</div>
${this.renderGeneratedSuggestionPreview()}
+ ${this.renderFixSuggestionPreview()}
</div>
</gr-endpoint-decorator>
${this.renderConfirmDialog()}
@@ -995,6 +996,13 @@
`;
}
+ private renderFixSuggestionPreview() {
+ if (!this.comment?.fix_suggestions) return nothing;
+ return html`<gr-suggestion-diff-preview
+ .fixReplacementInfos=${this.comment?.fix_suggestions?.[0].replacements}
+ ></gr-suggestion-diff-preview>`;
+ }
+
private showGeneratedSuggestion() {
return (
(this.flagsService.isEnabled(KnownExperimentId.ML_SUGGESTED_EDIT) ||
@@ -1416,7 +1424,11 @@
],
};
}
- if (isRobot(this.comment) && this.comment.fix_suggestions.length > 0) {
+ if (
+ isRobot(this.comment) &&
+ this.comment.fix_suggestions &&
+ this.comment.fix_suggestions.length > 0
+ ) {
const id = this.comment.robot_id;
return {
fixSuggestions: this.comment.fix_suggestions.map(s => {
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
index 03d8e32..d3df6c2 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment_test.ts
@@ -239,6 +239,7 @@
</gr-button>
</div>
</div>
+ <gr-suggestion-diff-preview></gr-suggestion-diff-preview>
</div>
</gr-endpoint-decorator>
<dialog id="confirmDeleteModal" tabindex="-1">
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts b/polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts
index fc37a0e..42fa984 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-model/gr-diff-model.ts
@@ -3,7 +3,7 @@
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {Observable, combineLatest, from} from 'rxjs';
+import {Observable, combineLatest, from, of} from 'rxjs';
import {debounceTime, filter, switchMap, withLatestFrom} from 'rxjs/operators';
import {
CreateCommentEventDetail,
@@ -36,6 +36,10 @@
GrDiffProcessor,
ProcessingOptions,
} from '../gr-diff-processor/gr-diff-processor';
+import {
+ GrDiffProcessorSimplified,
+ ProcessingOptions as ProcessingOptionsSimplified,
+} from '../gr-diff-processor/gr-diff-processor-simplified';
import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
import {assert} from '../../../utils/common-util';
import {countLines, isImageDiff} from '../../../utils/diff-util';
@@ -237,7 +241,7 @@
withLatestFrom(this.keyLocations$),
debounceTime(1),
switchMap(([[diff, context, renderPrefs], keyLocations]) => {
- const options: ProcessingOptions = {
+ const options: ProcessingOptions | ProcessingOptionsSimplified = {
context,
keyLocations,
isBinary: !!(isImageDiff(diff) || diff.binary),
@@ -245,8 +249,16 @@
if (renderPrefs?.num_lines_rendered_at_once) {
options.asyncThreshold = renderPrefs.num_lines_rendered_at_once;
}
- const processor = new GrDiffProcessor(options);
- return from(processor.process(diff.content));
+
+ // TODO: When switching to the simplified processor unconditionally,
+ // then we can use map() instead of switchMap().
+ if (renderPrefs?.use_simplified_processor) {
+ const processor = new GrDiffProcessorSimplified(options);
+ return of(processor.process(diff.content));
+ } else {
+ const processor = new GrDiffProcessor(options);
+ return from(processor.process(diff.content));
+ }
})
)
.subscribe(groups => {
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor-simplified.ts b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor-simplified.ts
new file mode 100644
index 0000000..a306de8
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor-simplified.ts
@@ -0,0 +1,550 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import {GrDiffLine, Highlights} from '../gr-diff/gr-diff-line';
+import {
+ GrDiffGroup,
+ GrDiffGroupType,
+ hideInContextControl,
+} from '../gr-diff/gr-diff-group';
+import {DiffContent} from '../../../types/diff';
+import {Side} from '../../../constants/constants';
+import {getStringLength} from '../gr-diff-highlight/gr-annotation';
+import {GrDiffLineType, LineNumber} from '../../../api/diff';
+import {FULL_CONTEXT, KeyLocations} from '../gr-diff/gr-diff-utils';
+
+// visible for testing
+export interface State {
+ lineNums: {
+ left: number;
+ right: number;
+ };
+ chunkIndex: number;
+}
+
+interface ChunkEnd {
+ offset: number;
+ keyLocation: boolean;
+}
+
+/** Interface for listening to the output of the processor. */
+export interface GroupConsumer {
+ addGroup(group: GrDiffGroup): void;
+ clearGroups(): void;
+}
+
+/** Interface for listening to the output of the processor. */
+export interface ProcessingOptions {
+ context: number;
+ keyLocations?: KeyLocations;
+ asyncThreshold?: number;
+ isBinary?: boolean;
+}
+
+/**
+ * Converts the API's `DiffContent`s to `GrDiffGroup`s for rendering.
+ *
+ * Glossary:
+ * - "chunk": A single `DiffContent` as returned by the API.
+ * - "group": A single `GrDiffGroup` as used for rendering.
+ * - "common" chunk/group: A chunk/group that should be considered unchanged
+ * for diffing purposes. This can mean its either actually unchanged, or it
+ * has only whitespace changes.
+ * - "key location": A line number and side of the diff that should not be
+ * collapsed e.g. because a comment is attached to it, or because it was
+ * provided in the URL and thus should be visible
+ * - "uncollapsible" chunk/group: A chunk/group that is either not "common",
+ * or cannot be collapsed because it contains a key location
+ *
+ * Here a a number of tasks this processor performs:
+ * - splitting large chunks to allow more granular async rendering
+ * - adding a group for the "File" pseudo line that file-level comments can
+ * be attached to
+ * - replacing common parts of the diff that are outside the user's
+ * context setting and do not have comments with a group representing the
+ * "expand context" widget. This may require splitting a chunk/group so
+ * that the part that is within the context or has comments is shown, while
+ * the rest is not.
+ */
+export class GrDiffProcessorSimplified {
+ // visible for testing
+ context: number;
+
+ // visible for testing
+ keyLocations: KeyLocations;
+
+ private isBinary = false;
+
+ private groups: GrDiffGroup[] = [];
+
+ constructor(options: ProcessingOptions) {
+ this.context = options.context;
+ this.keyLocations = options.keyLocations ?? {left: {}, right: {}};
+ this.isBinary = options.isBinary ?? false;
+ }
+
+ /**
+ * Process the diff chunks into GrDiffGroups.
+ *
+ * @return an array of GrDiffGroups
+ */
+ process(chunks: DiffContent[]): GrDiffGroup[] {
+ this.groups = [];
+ this.groups.push(this.makeGroup('LOST'));
+ this.groups.push(this.makeGroup('FILE'));
+
+ this.processChunks(chunks);
+ return this.groups;
+ }
+
+ processChunks(chunks: DiffContent[]) {
+ if (this.isBinary) return;
+
+ const state = {
+ lineNums: {left: 0, right: 0},
+ chunkIndex: 0,
+ };
+ chunks = this.splitCommonChunksWithKeyLocations(chunks);
+
+ while (state.chunkIndex < chunks.length) {
+ const stateUpdate = this.processNext(state, chunks);
+ for (const group of stateUpdate.groups) {
+ this.groups.push(group);
+ }
+ state.lineNums.left += stateUpdate.lineDelta.left;
+ state.lineNums.right += stateUpdate.lineDelta.right;
+ state.chunkIndex = stateUpdate.newChunkIndex;
+ }
+ }
+
+ /**
+ * Process the next uncollapsible chunk, or the next collapsible chunks.
+ */
+ // visible for testing
+ processNext(state: State, chunks: DiffContent[]) {
+ const firstUncollapsibleChunkIndex = this.firstUncollapsibleChunkIndex(
+ chunks,
+ state.chunkIndex
+ );
+ if (firstUncollapsibleChunkIndex === state.chunkIndex) {
+ const chunk = chunks[state.chunkIndex];
+ return {
+ lineDelta: {
+ left: this.linesLeft(chunk).length,
+ right: this.linesRight(chunk).length,
+ },
+ groups: [
+ this.chunkToGroup(
+ chunk,
+ state.lineNums.left + 1,
+ state.lineNums.right + 1
+ ),
+ ],
+ newChunkIndex: state.chunkIndex + 1,
+ };
+ }
+
+ return this.processCollapsibleChunks(
+ state,
+ chunks,
+ firstUncollapsibleChunkIndex
+ );
+ }
+
+ private linesLeft(chunk: DiffContent) {
+ return chunk.ab || chunk.a || [];
+ }
+
+ private linesRight(chunk: DiffContent) {
+ return chunk.ab || chunk.b || [];
+ }
+
+ private firstUncollapsibleChunkIndex(chunks: DiffContent[], offset: number) {
+ let chunkIndex = offset;
+ while (
+ chunkIndex < chunks.length &&
+ this.isCollapsibleChunk(chunks[chunkIndex])
+ ) {
+ chunkIndex++;
+ }
+ return chunkIndex;
+ }
+
+ private isCollapsibleChunk(chunk: DiffContent) {
+ return (chunk.ab || chunk.common || chunk.skip) && !chunk.keyLocation;
+ }
+
+ /**
+ * Process a stretch of collapsible chunks.
+ *
+ * Outputs up to three groups:
+ * 1) Visible context before the hidden common code, unless it's the
+ * very beginning of the file.
+ * 2) Context hidden behind a context bar, unless empty.
+ * 3) Visible context after the hidden common code, unless it's the very
+ * end of the file.
+ */
+ private processCollapsibleChunks(
+ state: State,
+ chunks: DiffContent[],
+ firstUncollapsibleChunkIndex: number
+ ) {
+ const collapsibleChunks = chunks.slice(
+ state.chunkIndex,
+ firstUncollapsibleChunkIndex
+ );
+ const lineCount = collapsibleChunks.reduce(
+ (sum, chunk) => sum + this.commonChunkLength(chunk),
+ 0
+ );
+
+ let groups = this.chunksToGroups(
+ collapsibleChunks,
+ state.lineNums.left + 1,
+ state.lineNums.right + 1
+ );
+
+ const hasSkippedGroup = !!groups.find(g => g.skip);
+ if (this.context !== FULL_CONTEXT || hasSkippedGroup) {
+ const contextNumLines = this.context > 0 ? this.context : 0;
+ const hiddenStart = state.chunkIndex === 0 ? 0 : contextNumLines;
+ const hiddenEnd =
+ lineCount -
+ (firstUncollapsibleChunkIndex === chunks.length ? 0 : this.context);
+ groups = hideInContextControl(groups, hiddenStart, hiddenEnd);
+ }
+
+ return {
+ lineDelta: {
+ left: lineCount,
+ right: lineCount,
+ },
+ groups,
+ newChunkIndex: firstUncollapsibleChunkIndex,
+ };
+ }
+
+ private commonChunkLength(chunk: DiffContent) {
+ if (chunk.skip) {
+ return chunk.skip;
+ }
+ console.assert(!!chunk.ab || !!chunk.common);
+
+ console.assert(
+ !chunk.a || (!!chunk.b && chunk.a.length === chunk.b.length),
+ 'common chunk needs same number of a and b lines: ',
+ chunk
+ );
+ return this.linesLeft(chunk).length;
+ }
+
+ private chunksToGroups(
+ chunks: DiffContent[],
+ offsetLeft: number,
+ offsetRight: number
+ ): GrDiffGroup[] {
+ return chunks.map(chunk => {
+ const group = this.chunkToGroup(chunk, offsetLeft, offsetRight);
+ const chunkLength = this.commonChunkLength(chunk);
+ offsetLeft += chunkLength;
+ offsetRight += chunkLength;
+ return group;
+ });
+ }
+
+ private chunkToGroup(
+ chunk: DiffContent,
+ offsetLeft: number,
+ offsetRight: number
+ ): GrDiffGroup {
+ const type =
+ chunk.ab || chunk.skip ? GrDiffGroupType.BOTH : GrDiffGroupType.DELTA;
+ const lines = this.linesFromChunk(chunk, offsetLeft, offsetRight);
+ const options = {
+ moveDetails: chunk.move_details,
+ dueToRebase: !!chunk.due_to_rebase,
+ ignoredWhitespaceOnly: !!chunk.common,
+ keyLocation: !!chunk.keyLocation,
+ };
+ if (chunk.skip !== undefined) {
+ return new GrDiffGroup({
+ type,
+ skip: chunk.skip,
+ offsetLeft,
+ offsetRight,
+ ...options,
+ });
+ } else {
+ return new GrDiffGroup({
+ type,
+ lines,
+ ...options,
+ });
+ }
+ }
+
+ private linesFromChunk(
+ chunk: DiffContent,
+ offsetLeft: number,
+ offsetRight: number
+ ) {
+ if (chunk.ab) {
+ return chunk.ab.map((row, i) =>
+ this.lineFromRow(GrDiffLineType.BOTH, offsetLeft, offsetRight, row, i)
+ );
+ }
+ let lines: GrDiffLine[] = [];
+ if (chunk.a) {
+ // Avoiding a.push(...b) because that causes callstack overflows for
+ // large b, which can occur when large files are added removed.
+ lines = lines.concat(
+ this.linesFromRows(
+ GrDiffLineType.REMOVE,
+ chunk.a,
+ offsetLeft,
+ chunk.edit_a
+ )
+ );
+ }
+ if (chunk.b) {
+ // Avoiding a.push(...b) because that causes callstack overflows for
+ // large b, which can occur when large files are added removed.
+ lines = lines.concat(
+ this.linesFromRows(
+ GrDiffLineType.ADD,
+ chunk.b,
+ offsetRight,
+ chunk.edit_b
+ )
+ );
+ }
+ return lines;
+ }
+
+ // visible for testing
+ linesFromRows(
+ lineType: GrDiffLineType,
+ rows: string[],
+ offset: number,
+ intralineInfos?: number[][]
+ ): GrDiffLine[] {
+ const grDiffHighlights = intralineInfos
+ ? this.convertIntralineInfos(rows, intralineInfos)
+ : undefined;
+ return rows.map((row, i) =>
+ this.lineFromRow(lineType, offset, offset, row, i, grDiffHighlights)
+ );
+ }
+
+ private lineFromRow(
+ type: GrDiffLineType,
+ offsetLeft: number,
+ offsetRight: number,
+ row: string,
+ i: number,
+ highlights?: Highlights[]
+ ): GrDiffLine {
+ const line = new GrDiffLine(type);
+ line.text = row;
+ if (type !== GrDiffLineType.ADD) line.beforeNumber = offsetLeft + i;
+ if (type !== GrDiffLineType.REMOVE) line.afterNumber = offsetRight + i;
+ if (highlights) {
+ line.hasIntralineInfo = true;
+ line.highlights = highlights.filter(hl => hl.contentIndex === i);
+ } else {
+ line.hasIntralineInfo = false;
+ }
+ return line;
+ }
+
+ private makeGroup(number: LineNumber) {
+ const line = new GrDiffLine(GrDiffLineType.BOTH);
+ line.beforeNumber = number;
+ line.afterNumber = number;
+ return new GrDiffGroup({type: GrDiffGroupType.BOTH, lines: [line]});
+ }
+
+ /**
+ * In order to show key locations, such as comments, out of the bounds of
+ * the selected context, treat them as separate chunks within the model so
+ * that the content (and context surrounding it) renders correctly.
+ *
+ * @param chunks DiffContents as returned from server.
+ * @return Finer grained DiffContents.
+ */
+ // visible for testing
+ splitCommonChunksWithKeyLocations(chunks: DiffContent[]): DiffContent[] {
+ const result = [];
+ let leftLineNum = 1;
+ let rightLineNum = 1;
+
+ for (const chunk of chunks) {
+ // If it isn't a common chunk, append it as-is and update line numbers.
+ if (!chunk.ab && !chunk.skip && !chunk.common) {
+ if (chunk.a) {
+ leftLineNum += chunk.a.length;
+ }
+ if (chunk.b) {
+ rightLineNum += chunk.b.length;
+ }
+ result.push(chunk);
+ continue;
+ }
+
+ if (chunk.common && chunk.a!.length !== chunk.b!.length) {
+ throw new Error(
+ 'DiffContent with common=true must always have equal length'
+ );
+ }
+ const numLines = this.commonChunkLength(chunk);
+ const chunkEnds = this.findChunkEndsAtKeyLocations(
+ numLines,
+ leftLineNum,
+ rightLineNum
+ );
+ leftLineNum += numLines;
+ rightLineNum += numLines;
+
+ if (chunk.skip) {
+ result.push({
+ ...chunk,
+ skip: chunk.skip,
+ keyLocation: false,
+ });
+ } else if (chunk.ab) {
+ result.push(
+ ...this.splitAtChunkEnds(chunk.ab, chunkEnds).map(
+ ({lines, keyLocation}) => {
+ return {
+ ...chunk,
+ ab: lines,
+ keyLocation,
+ };
+ }
+ )
+ );
+ } else if (chunk.common) {
+ const aChunks = this.splitAtChunkEnds(chunk.a!, chunkEnds);
+ const bChunks = this.splitAtChunkEnds(chunk.b!, chunkEnds);
+ result.push(
+ ...aChunks.map(({lines, keyLocation}, i) => {
+ return {
+ ...chunk,
+ a: lines,
+ b: bChunks[i].lines,
+ keyLocation,
+ };
+ })
+ );
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * @return Offsets of the new chunk ends, including whether it's a key
+ * location.
+ */
+ private findChunkEndsAtKeyLocations(
+ numLines: number,
+ leftOffset: number,
+ rightOffset: number
+ ): ChunkEnd[] {
+ const result = [];
+ let lastChunkEnd = 0;
+ for (let i = 0; i < numLines; i++) {
+ // If this line should not be collapsed.
+ if (
+ this.keyLocations[Side.LEFT][leftOffset + i] ||
+ this.keyLocations[Side.RIGHT][rightOffset + i]
+ ) {
+ // If any lines have been accumulated into the chunk leading up to
+ // this non-collapse line, then add them as a chunk and start a new
+ // one.
+ if (i > lastChunkEnd) {
+ result.push({offset: i, keyLocation: false});
+ lastChunkEnd = i;
+ }
+
+ // Add the non-collapse line as its own chunk.
+ result.push({offset: i + 1, keyLocation: true});
+ }
+ }
+
+ if (numLines > lastChunkEnd) {
+ result.push({offset: numLines, keyLocation: false});
+ }
+
+ return result;
+ }
+
+ private splitAtChunkEnds(lines: string[], chunkEnds: ChunkEnd[]) {
+ const result = [];
+ let lastChunkEndOffset = 0;
+ for (const {offset, keyLocation} of chunkEnds) {
+ if (lastChunkEndOffset === offset) continue;
+ result.push({
+ lines: lines.slice(lastChunkEndOffset, offset),
+ keyLocation,
+ });
+ lastChunkEndOffset = offset;
+ }
+ return result;
+ }
+
+ /**
+ * Converts `IntralineInfo`s return by the API to `GrLineHighlights` used
+ * for rendering.
+ */
+ // visible for testing
+ convertIntralineInfos(
+ rows: string[],
+ intralineInfos: number[][]
+ ): Highlights[] {
+ // +1 to account for the \n that is not part of the rows passed here
+ const lineLengths = rows.map(r => getStringLength(r) + 1);
+
+ let rowIndex = 0;
+ let idx = 0;
+ const normalized = [];
+ for (const [skipLength, markLength] of intralineInfos) {
+ let lineLength = lineLengths[rowIndex];
+ let j = 0;
+ while (j < skipLength) {
+ if (idx === lineLength) {
+ idx = 0;
+ lineLength = lineLengths[++rowIndex];
+ continue;
+ }
+ idx++;
+ j++;
+ }
+ let lineHighlight: Highlights = {
+ contentIndex: rowIndex,
+ startIndex: idx,
+ };
+
+ j = 0;
+ while (lineLength && j < markLength) {
+ if (idx === lineLength) {
+ idx = 0;
+ lineLength = lineLengths[++rowIndex];
+ normalized.push(lineHighlight);
+ lineHighlight = {
+ contentIndex: rowIndex,
+ startIndex: idx,
+ };
+ continue;
+ }
+ idx++;
+ j++;
+ }
+ lineHighlight.endIndex = idx;
+ normalized.push(lineHighlight);
+ }
+ return normalized;
+ }
+}
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor-simplified_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor-simplified_test.ts
new file mode 100644
index 0000000..abd224f
--- /dev/null
+++ b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor-simplified_test.ts
@@ -0,0 +1,987 @@
+/**
+ * @license
+ * Copyright 2016 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import '../../../test/common-test-setup';
+import './gr-diff-processor';
+import {GrDiffLine} from '../gr-diff/gr-diff-line';
+import {GrDiffGroup, GrDiffGroupType} from '../gr-diff/gr-diff-group';
+import {
+ GrDiffProcessorSimplified,
+ ProcessingOptions,
+ State,
+} from './gr-diff-processor-simplified';
+import {DiffContent} from '../../../types/diff';
+import {assert} from '@open-wc/testing';
+import {FILE, GrDiffLineType} from '../../../api/diff';
+import {FULL_CONTEXT} from '../gr-diff/gr-diff-utils';
+
+suite('gr-diff-processor tests', () => {
+ const loremIpsum =
+ 'Lorem ipsum dolor sit amet, ei nonumes vituperata ius. ' +
+ 'Duo animal omnesque fabellas et. Id has phaedrum dignissim ' +
+ 'deterruisset, pro ei petentium comprehensam, ut vis solum dicta. ' +
+ 'Eos cu aliquam labores qualisque, usu postea inermis te, et solum ' +
+ 'fugit assum per.';
+
+ let processor: GrDiffProcessorSimplified;
+ let options: ProcessingOptions = {
+ context: 4,
+ };
+
+ setup(() => {});
+
+ suite('not logged in', () => {
+ setup(() => {
+ options = {context: 4};
+ processor = new GrDiffProcessorSimplified(options);
+ });
+
+ test('process loaded content', async () => {
+ const content: DiffContent[] = [
+ {
+ ab: ['<!DOCTYPE html>', '<meta charset="utf-8">'],
+ },
+ {
+ a: [' Welcome ', ' to the wooorld of tomorrow!'],
+ b: [' Hello, world!'],
+ },
+ {
+ ab: [
+ 'Leela: This is the only place the ship can’t hear us, so ',
+ 'everyone pretend to shower.',
+ 'Fry: Same as every day. Got it.',
+ ],
+ },
+ ];
+
+ const groups = await processor.process(content);
+ groups.shift(); // remove portedThreadsWithoutRangeGroup
+ assert.equal(groups.length, 4);
+
+ let group = groups[0];
+ assert.equal(group.type, GrDiffGroupType.BOTH);
+ assert.equal(group.lines.length, 1);
+ assert.equal(group.lines[0].text, '');
+ assert.equal(group.lines[0].beforeNumber, FILE);
+ assert.equal(group.lines[0].afterNumber, FILE);
+
+ group = groups[1];
+ assert.equal(group.type, GrDiffGroupType.BOTH);
+ assert.equal(group.lines.length, 2);
+
+ function beforeNumberFn(l: GrDiffLine) {
+ return l.beforeNumber;
+ }
+ function afterNumberFn(l: GrDiffLine) {
+ return l.afterNumber;
+ }
+ function textFn(l: GrDiffLine) {
+ return l.text;
+ }
+
+ assert.deepEqual(group.lines.map(beforeNumberFn), [1, 2]);
+ assert.deepEqual(group.lines.map(afterNumberFn), [1, 2]);
+ assert.deepEqual(group.lines.map(textFn), [
+ '<!DOCTYPE html>',
+ '<meta charset="utf-8">',
+ ]);
+
+ group = groups[2];
+ assert.equal(group.type, GrDiffGroupType.DELTA);
+ assert.equal(group.lines.length, 3);
+ assert.equal(group.adds.length, 1);
+ assert.equal(group.removes.length, 2);
+ assert.deepEqual(group.removes.map(beforeNumberFn), [3, 4]);
+ assert.deepEqual(group.adds.map(afterNumberFn), [3]);
+ assert.deepEqual(group.removes.map(textFn), [
+ ' Welcome ',
+ ' to the wooorld of tomorrow!',
+ ]);
+ assert.deepEqual(group.adds.map(textFn), [' Hello, world!']);
+
+ group = groups[3];
+ assert.equal(group.type, GrDiffGroupType.BOTH);
+ assert.equal(group.lines.length, 3);
+ assert.deepEqual(group.lines.map(beforeNumberFn), [5, 6, 7]);
+ assert.deepEqual(group.lines.map(afterNumberFn), [4, 5, 6]);
+ assert.deepEqual(group.lines.map(textFn), [
+ 'Leela: This is the only place the ship can’t hear us, so ',
+ 'everyone pretend to shower.',
+ 'Fry: Same as every day. Got it.',
+ ]);
+ });
+
+ test('first group is for file', async () => {
+ const content = [{b: ['foo']}];
+
+ const groups = await processor.process(content);
+ groups.shift(); // remove portedThreadsWithoutRangeGroup
+
+ assert.equal(groups[0].type, GrDiffGroupType.BOTH);
+ assert.equal(groups[0].lines.length, 1);
+ assert.equal(groups[0].lines[0].text, '');
+ assert.equal(groups[0].lines[0].beforeNumber, FILE);
+ assert.equal(groups[0].lines[0].afterNumber, FILE);
+ });
+
+ suite('context groups', async () => {
+ test('at the beginning, larger than context', async () => {
+ options.context = 10;
+ processor = new GrDiffProcessorSimplified(options);
+ const content = [
+ {
+ ab: Array.from<string>({length: 100}).fill(
+ 'all work and no play make jack a dull boy'
+ ),
+ },
+ {a: ['all work and no play make andybons a dull boy']},
+ ];
+
+ const groups = await processor.process(content);
+ // group[0] is the LOST group
+ // group[1] is the FILE group
+
+ assert.equal(groups[2].type, GrDiffGroupType.CONTEXT_CONTROL);
+ assert.instanceOf(groups[2].contextGroups[0], GrDiffGroup);
+ assert.equal(groups[2].contextGroups[0].lines.length, 90);
+ for (const l of groups[2].contextGroups[0].lines) {
+ assert.equal(l.text, 'all work and no play make jack a dull boy');
+ }
+
+ assert.equal(groups[3].type, GrDiffGroupType.BOTH);
+ assert.equal(groups[3].lines.length, 10);
+ for (const l of groups[3].lines) {
+ assert.equal(l.text, 'all work and no play make jack a dull boy');
+ }
+ });
+
+ test('at the beginning with skip chunks', async () => {
+ options.context = 10;
+ processor = new GrDiffProcessorSimplified(options);
+ const content = [
+ {
+ ab: Array.from<string>({length: 20}).fill(
+ 'all work and no play make jack a dull boy'
+ ),
+ },
+ {skip: 43900},
+ {ab: Array.from<string>({length: 30}).fill('some other content')},
+ {a: ['some other content']},
+ ];
+
+ const groups = await processor.process(content);
+ groups.shift(); // remove portedThreadsWithoutRangeGroup
+
+ // group[0] is the file group
+
+ const commonGroup = groups[1];
+
+ // Hidden context before
+ assert.equal(commonGroup.type, GrDiffGroupType.CONTEXT_CONTROL);
+ assert.instanceOf(commonGroup.contextGroups[0], GrDiffGroup);
+ assert.equal(commonGroup.contextGroups[0].lines.length, 20);
+ for (const l of commonGroup.contextGroups[0].lines) {
+ assert.equal(l.text, 'all work and no play make jack a dull boy');
+ }
+
+ // Skipped group
+ const skipGroup = commonGroup.contextGroups[1];
+ assert.equal(skipGroup.skip, 43900);
+ const expectedRange = {
+ left: {start_line: 21, end_line: 43920},
+ right: {start_line: 21, end_line: 43920},
+ };
+ assert.deepEqual(skipGroup.lineRange, expectedRange);
+
+ // Hidden context after
+ assert.equal(commonGroup.contextGroups[2].lines.length, 20);
+ for (const l of commonGroup.contextGroups[2].lines) {
+ assert.equal(l.text, 'some other content');
+ }
+
+ // Displayed lines
+ assert.equal(groups[2].type, GrDiffGroupType.BOTH);
+ assert.equal(groups[2].lines.length, 10);
+ for (const l of groups[2].lines) {
+ assert.equal(l.text, 'some other content');
+ }
+ });
+
+ test('at the beginning, smaller than context', async () => {
+ options.context = 10;
+ processor = new GrDiffProcessorSimplified(options);
+ const content = [
+ {
+ ab: Array.from<string>({length: 5}).fill(
+ 'all work and no play make jack a dull boy'
+ ),
+ },
+ {a: ['all work and no play make andybons a dull boy']},
+ ];
+
+ const groups = await processor.process(content);
+ groups.shift(); // remove portedThreadsWithoutRangeGroup
+
+ // group[0] is the file group
+
+ assert.equal(groups[1].type, GrDiffGroupType.BOTH);
+ assert.equal(groups[1].lines.length, 5);
+ for (const l of groups[1].lines) {
+ assert.equal(l.text, 'all work and no play make jack a dull boy');
+ }
+ });
+
+ test('at the end, larger than context', async () => {
+ options.context = 10;
+ processor = new GrDiffProcessorSimplified(options);
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {
+ ab: Array.from<string>({length: 100}).fill(
+ 'all work and no play make jill a dull girl'
+ ),
+ },
+ ];
+
+ const groups = await processor.process(content);
+ groups.shift(); // remove portedThreadsWithoutRangeGroup
+
+ // group[0] is the file group
+ // group[1] is the "a" group
+
+ assert.equal(groups[2].type, GrDiffGroupType.BOTH);
+ assert.equal(groups[2].lines.length, 10);
+ for (const l of groups[2].lines) {
+ assert.equal(l.text, 'all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[3].type, GrDiffGroupType.CONTEXT_CONTROL);
+ assert.instanceOf(groups[3].contextGroups[0], GrDiffGroup);
+ assert.equal(groups[3].contextGroups[0].lines.length, 90);
+ for (const l of groups[3].contextGroups[0].lines) {
+ assert.equal(l.text, 'all work and no play make jill a dull girl');
+ }
+ });
+
+ test('at the end, smaller than context', async () => {
+ options.context = 10;
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {
+ ab: Array.from<string>({length: 5}).fill(
+ 'all work and no play make jill a dull girl'
+ ),
+ },
+ ];
+
+ const groups = await processor.process(content);
+ groups.shift(); // remove portedThreadsWithoutRangeGroup
+
+ // group[0] is the file group
+ // group[1] is the "a" group
+
+ assert.equal(groups[2].type, GrDiffGroupType.BOTH);
+ assert.equal(groups[2].lines.length, 5);
+ for (const l of groups[2].lines) {
+ assert.equal(l.text, 'all work and no play make jill a dull girl');
+ }
+ });
+
+ test('for interleaved ab and common: true chunks', async () => {
+ options.context = 10;
+ processor = new GrDiffProcessorSimplified(options);
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {
+ ab: Array.from<string>({length: 3}).fill(
+ 'all work and no play make jill a dull girl'
+ ),
+ },
+ {
+ a: Array.from<string>({length: 3}).fill(
+ 'all work and no play make jill a dull girl'
+ ),
+ b: Array.from<string>({length: 3}).fill(
+ ' all work and no play make jill a dull girl'
+ ),
+ common: true,
+ },
+ {
+ ab: Array.from<string>({length: 3}).fill(
+ 'all work and no play make jill a dull girl'
+ ),
+ },
+ {
+ a: Array.from<string>({length: 3}).fill(
+ 'all work and no play make jill a dull girl'
+ ),
+ b: Array.from<string>({length: 3}).fill(
+ ' all work and no play make jill a dull girl'
+ ),
+ common: true,
+ },
+ {
+ ab: Array.from<string>({length: 3}).fill(
+ 'all work and no play make jill a dull girl'
+ ),
+ },
+ ];
+
+ const groups = await processor.process(content);
+ groups.shift(); // remove portedThreadsWithoutRangeGroup
+
+ // group[0] is the file group
+ // group[1] is the "a" group
+
+ // The first three interleaved chunks are completely shown because
+ // they are part of the context (3 * 3 <= 10)
+
+ assert.equal(groups[2].type, GrDiffGroupType.BOTH);
+ assert.equal(groups[2].lines.length, 3);
+ for (const l of groups[2].lines) {
+ assert.equal(l.text, 'all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[3].type, GrDiffGroupType.DELTA);
+ assert.equal(groups[3].lines.length, 6);
+ assert.equal(groups[3].adds.length, 3);
+ assert.equal(groups[3].removes.length, 3);
+ for (const l of groups[3].removes) {
+ assert.equal(l.text, 'all work and no play make jill a dull girl');
+ }
+ for (const l of groups[3].adds) {
+ assert.equal(l.text, ' all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[4].type, GrDiffGroupType.BOTH);
+ assert.equal(groups[4].lines.length, 3);
+ for (const l of groups[4].lines) {
+ assert.equal(l.text, 'all work and no play make jill a dull girl');
+ }
+
+ // The next chunk is partially shown, so it results in two groups
+
+ assert.equal(groups[5].type, GrDiffGroupType.DELTA);
+ assert.equal(groups[5].lines.length, 2);
+ assert.equal(groups[5].adds.length, 1);
+ assert.equal(groups[5].removes.length, 1);
+ for (const l of groups[5].removes) {
+ assert.equal(l.text, 'all work and no play make jill a dull girl');
+ }
+ for (const l of groups[5].adds) {
+ assert.equal(l.text, ' all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[6].type, GrDiffGroupType.CONTEXT_CONTROL);
+ assert.equal(groups[6].contextGroups.length, 2);
+
+ assert.equal(groups[6].contextGroups[0].lines.length, 4);
+ assert.equal(groups[6].contextGroups[0].removes.length, 2);
+ assert.equal(groups[6].contextGroups[0].adds.length, 2);
+ for (const l of groups[6].contextGroups[0].removes) {
+ assert.equal(l.text, 'all work and no play make jill a dull girl');
+ }
+ for (const l of groups[6].contextGroups[0].adds) {
+ assert.equal(l.text, ' all work and no play make jill a dull girl');
+ }
+
+ // The final chunk is completely hidden
+ assert.equal(groups[6].contextGroups[1].type, GrDiffGroupType.BOTH);
+ assert.equal(groups[6].contextGroups[1].lines.length, 3);
+ for (const l of groups[6].contextGroups[1].lines) {
+ assert.equal(l.text, 'all work and no play make jill a dull girl');
+ }
+ });
+
+ test('in the middle, larger than context', async () => {
+ options.context = 10;
+ processor = new GrDiffProcessorSimplified(options);
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {
+ ab: Array.from<string>({length: 100}).fill(
+ 'all work and no play make jill a dull girl'
+ ),
+ },
+ {a: ['all work and no play make andybons a dull boy']},
+ ];
+
+ const groups = await processor.process(content);
+ groups.shift(); // remove portedThreadsWithoutRangeGroup
+
+ // group[0] is the file group
+ // group[1] is the "a" group
+
+ assert.equal(groups[2].type, GrDiffGroupType.BOTH);
+ assert.equal(groups[2].lines.length, 10);
+ for (const l of groups[2].lines) {
+ assert.equal(l.text, 'all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[3].type, GrDiffGroupType.CONTEXT_CONTROL);
+ assert.instanceOf(groups[3].contextGroups[0], GrDiffGroup);
+ assert.equal(groups[3].contextGroups[0].lines.length, 80);
+ for (const l of groups[3].contextGroups[0].lines) {
+ assert.equal(l.text, 'all work and no play make jill a dull girl');
+ }
+
+ assert.equal(groups[4].type, GrDiffGroupType.BOTH);
+ assert.equal(groups[4].lines.length, 10);
+ for (const l of groups[4].lines) {
+ assert.equal(l.text, 'all work and no play make jill a dull girl');
+ }
+ });
+
+ test('in the middle, smaller than context', async () => {
+ options.context = 10;
+ processor = new GrDiffProcessorSimplified(options);
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {
+ ab: Array.from<string>({length: 5}).fill(
+ 'all work and no play make jill a dull girl'
+ ),
+ },
+ {a: ['all work and no play make andybons a dull boy']},
+ ];
+
+ const groups = await processor.process(content);
+ groups.shift(); // remove portedThreadsWithoutRangeGroup
+
+ // group[0] is the file group
+ // group[1] is the "a" group
+
+ assert.equal(groups[2].type, GrDiffGroupType.BOTH);
+ assert.equal(groups[2].lines.length, 5);
+ for (const l of groups[2].lines) {
+ assert.equal(l.text, 'all work and no play make jill a dull girl');
+ }
+ });
+ });
+
+ test('in the middle with skip chunks', async () => {
+ options.context = 10;
+ processor = new GrDiffProcessorSimplified(options);
+ const content = [
+ {a: ['all work and no play make andybons a dull boy']},
+ {
+ ab: Array.from<string>({length: 20}).fill(
+ 'all work and no play make jill a dull girl'
+ ),
+ },
+ {skip: 60},
+ {
+ ab: Array.from<string>({length: 20}).fill(
+ 'all work and no play make jill a dull girl'
+ ),
+ },
+ {a: ['all work and no play make andybons a dull boy']},
+ ];
+
+ const groups = await processor.process(content);
+ groups.shift(); // remove portedThreadsWithoutRangeGroup
+
+ // group[0] is the file group
+ // group[1] is the chunk with a
+ // group[2] is the displayed part of ab before
+
+ const commonGroup = groups[3];
+
+ // Hidden context before
+ assert.equal(commonGroup.type, GrDiffGroupType.CONTEXT_CONTROL);
+ assert.instanceOf(commonGroup.contextGroups[0], GrDiffGroup);
+ assert.equal(commonGroup.contextGroups[0].lines.length, 10);
+ for (const l of commonGroup.contextGroups[0].lines) {
+ assert.equal(l.text, 'all work and no play make jill a dull girl');
+ }
+
+ // Skipped group
+ const skipGroup = commonGroup.contextGroups[1];
+ assert.equal(skipGroup.skip, 60);
+ const expectedRange = {
+ left: {start_line: 22, end_line: 81},
+ right: {start_line: 21, end_line: 80},
+ };
+ assert.deepEqual(skipGroup.lineRange, expectedRange);
+
+ // Hidden context after
+ assert.equal(commonGroup.contextGroups[2].lines.length, 10);
+ for (const l of commonGroup.contextGroups[2].lines) {
+ assert.equal(l.text, 'all work and no play make jill a dull girl');
+ }
+ // group[4] is the displayed part of the second ab
+ });
+
+ test('works with skip === 0', async () => {
+ options.context = 3;
+ processor = new GrDiffProcessorSimplified(options);
+ const content = [
+ {
+ skip: 0,
+ },
+ {
+ b: [
+ '/**',
+ ' * @license',
+ ' * Copyright 2015 Google LLC',
+ ' * SPDX-License-Identifier: Apache-2.0',
+ ' */',
+ "import '../../../test/common-test-setup';",
+ ],
+ },
+ ];
+ await processor.process(content);
+ });
+
+ test('break up common diff chunks', () => {
+ options.keyLocations = {
+ left: {1: true},
+ right: {10: true},
+ };
+ processor = new GrDiffProcessorSimplified(options);
+
+ const content = [
+ {
+ ab: [
+ 'copy',
+ '',
+ 'asdf',
+ 'qwer',
+ 'zxcv',
+ '',
+ 'http',
+ '',
+ 'vbnm',
+ 'dfgh',
+ 'yuio',
+ 'sdfg',
+ '1234',
+ ],
+ },
+ ];
+ const result = processor.splitCommonChunksWithKeyLocations(content);
+ assert.deepEqual(result, [
+ {
+ ab: ['copy'],
+ keyLocation: true,
+ },
+ {
+ ab: ['', 'asdf', 'qwer', 'zxcv', '', 'http', '', 'vbnm'],
+ keyLocation: false,
+ },
+ {
+ ab: ['dfgh'],
+ keyLocation: true,
+ },
+ {
+ ab: ['yuio', 'sdfg', '1234'],
+ keyLocation: false,
+ },
+ ]);
+ });
+
+ test('does not break-down common chunks w/ context', () => {
+ const ab = Array(75)
+ .fill(0)
+ .map(() => `${Math.random()}`);
+ const content = [{ab}];
+ processor.context = 4;
+ const result = processor.splitCommonChunksWithKeyLocations(content);
+ assert.equal(result.length, 1);
+ assert.deepEqual(result[0].ab, content[0].ab);
+ assert.isFalse(result[0].keyLocation);
+ });
+
+ test('intraline normalization', () => {
+ // The content and highlights are in the format returned by the Gerrit
+ // REST API.
+ let content = [
+ ' <section class="summary">',
+ ' <gr-formatted-text content="' +
+ '[[_computeCurrentRevisionMessage(change)]]"></gr-formatted-text>',
+ ' </section>',
+ ];
+ let highlights = [
+ [31, 34],
+ [42, 26],
+ ];
+
+ let results = processor.convertIntralineInfos(content, highlights);
+ assert.deepEqual(results, [
+ {
+ contentIndex: 0,
+ startIndex: 31,
+ },
+ {
+ contentIndex: 1,
+ startIndex: 0,
+ endIndex: 33,
+ },
+ {
+ contentIndex: 1,
+ endIndex: 101,
+ startIndex: 75,
+ },
+ ]);
+ const lines = processor.linesFromRows(
+ GrDiffLineType.BOTH,
+ content,
+ 0,
+ highlights
+ );
+ assert.equal(lines.length, 3);
+ assert.isTrue(lines[0].hasIntralineInfo);
+ assert.equal(lines[0].highlights.length, 1);
+ assert.isTrue(lines[1].hasIntralineInfo);
+ assert.equal(lines[1].highlights.length, 2);
+ assert.isTrue(lines[2].hasIntralineInfo);
+ assert.equal(lines[2].highlights.length, 0);
+
+ content = [
+ ' this._path = value.path;',
+ '',
+ ' // When navigating away from the page, there is a ' +
+ 'possibility that the',
+ ' // patch number is no longer a part of the URL ' +
+ '(say when navigating to',
+ ' // the top-level change info view) and therefore ' +
+ 'undefined in `params`.',
+ ' if (!this._patchRange.patchNum) {',
+ ];
+ highlights = [
+ [14, 17],
+ [11, 70],
+ [12, 67],
+ [12, 67],
+ [14, 29],
+ ];
+ results = processor.convertIntralineInfos(content, highlights);
+ assert.deepEqual(results, [
+ {
+ contentIndex: 0,
+ startIndex: 14,
+ endIndex: 31,
+ },
+ {
+ contentIndex: 2,
+ startIndex: 8,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 3,
+ startIndex: 11,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 4,
+ startIndex: 11,
+ endIndex: 78,
+ },
+ {
+ contentIndex: 5,
+ startIndex: 12,
+ endIndex: 41,
+ },
+ ]);
+
+ content = ['🙈 a', '🙉 b', '🙊 c'];
+ highlights = [[2, 7]];
+ results = processor.convertIntralineInfos(content, highlights);
+ assert.deepEqual(results, [
+ {
+ contentIndex: 0,
+ startIndex: 2,
+ },
+ {
+ contentIndex: 1,
+ startIndex: 0,
+ },
+ {
+ contentIndex: 2,
+ startIndex: 0,
+ endIndex: 1,
+ },
+ ]);
+ });
+
+ test('image diffs', async () => {
+ const content = Array(200).fill({ab: ['', '']});
+ options.isBinary = true;
+ processor = new GrDiffProcessorSimplified(options);
+ const groups = await processor.process(content);
+ assert.equal(groups.length, 2);
+
+ // Image diffs don't process content, just the 'FILE' line.
+ assert.equal(groups[0].lines.length, 1);
+ });
+
+ suite('processNext', () => {
+ let rows: string[];
+
+ setup(() => {
+ rows = loremIpsum.split(' ');
+ });
+
+ test('FULL_CONTEXT', () => {
+ processor.context = FULL_CONTEXT;
+ const state: State = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 1,
+ };
+ const chunks = [{a: ['foo']}, {ab: rows}, {a: ['bar']}];
+ const result = processor.processNext(state, chunks);
+
+ // Results in one, uncollapsed group with all rows.
+ assert.equal(result.groups.length, 1);
+ assert.equal(result.groups[0].type, GrDiffGroupType.BOTH);
+ assert.equal(result.groups[0].lines.length, rows.length);
+
+ // Line numbers are set correctly.
+ assert.equal(
+ result.groups[0].lines[0].beforeNumber,
+ state.lineNums.left + 1
+ );
+ assert.equal(
+ result.groups[0].lines[0].afterNumber,
+ state.lineNums.right + 1
+ );
+
+ assert.equal(
+ result.groups[0].lines[rows.length - 1].beforeNumber,
+ state.lineNums.left + rows.length
+ );
+ assert.equal(
+ result.groups[0].lines[rows.length - 1].afterNumber,
+ state.lineNums.right + rows.length
+ );
+ });
+
+ test('FULL_CONTEXT with skip chunks still get collapsed', () => {
+ processor.context = FULL_CONTEXT;
+ const lineNums = {left: 10, right: 100};
+ const state = {
+ lineNums,
+ chunkIndex: 1,
+ };
+ const skip = 10000;
+ const chunks = [{a: ['foo']}, {skip}, {ab: rows}, {a: ['bar']}];
+ const result = processor.processNext(state, chunks);
+ // Results in one, uncollapsed group with all rows.
+ assert.equal(result.groups.length, 1);
+ assert.equal(result.groups[0].type, GrDiffGroupType.CONTEXT_CONTROL);
+
+ // Skip and ab group are hidden in the same context control
+ assert.equal(result.groups[0].contextGroups.length, 2);
+ const [skippedGroup, abGroup] = result.groups[0].contextGroups;
+
+ // Line numbers are set correctly.
+ assert.deepEqual(skippedGroup.lineRange, {
+ left: {
+ start_line: lineNums.left + 1,
+ end_line: lineNums.left + skip,
+ },
+ right: {
+ start_line: lineNums.right + 1,
+ end_line: lineNums.right + skip,
+ },
+ });
+
+ assert.deepEqual(abGroup.lineRange, {
+ left: {
+ start_line: lineNums.left + skip + 1,
+ end_line: lineNums.left + skip + rows.length,
+ },
+ right: {
+ start_line: lineNums.right + skip + 1,
+ end_line: lineNums.right + skip + rows.length,
+ },
+ });
+ });
+
+ test('with context', () => {
+ processor.context = 10;
+ const state = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 1,
+ };
+ const chunks = [{a: ['foo']}, {ab: rows}, {a: ['bar']}];
+ const result = processor.processNext(state, chunks);
+ const expectedCollapseSize = rows.length - 2 * processor.context;
+
+ assert.equal(result.groups.length, 3, 'Results in three groups');
+
+ // The first and last are uncollapsed context, whereas the middle has
+ // a single context-control line.
+ assert.equal(result.groups[0].lines.length, processor.context);
+ assert.equal(result.groups[2].lines.length, processor.context);
+
+ // The collapsed group has the hidden lines as its context group.
+ assert.equal(
+ result.groups[1].contextGroups[0].lines.length,
+ expectedCollapseSize
+ );
+ });
+
+ test('first', () => {
+ processor.context = 10;
+ const state = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 0,
+ };
+ const chunks = [{ab: rows}, {a: ['foo']}, {a: ['bar']}];
+ const result = processor.processNext(state, chunks);
+ const expectedCollapseSize = rows.length - processor.context;
+
+ assert.equal(result.groups.length, 2, 'Results in two groups');
+
+ // Only the first group is collapsed.
+ assert.equal(result.groups[1].lines.length, processor.context);
+
+ // The collapsed group has the hidden lines as its context group.
+ assert.equal(
+ result.groups[0].contextGroups[0].lines.length,
+ expectedCollapseSize
+ );
+ });
+
+ test('few-rows', () => {
+ // Only ten rows.
+ rows = rows.slice(0, 10);
+ processor.context = 10;
+ const state = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 0,
+ };
+ const chunks = [{ab: rows}, {a: ['foo']}, {a: ['bar']}];
+ const result = processor.processNext(state, chunks);
+
+ // Results in one uncollapsed group with all rows.
+ assert.equal(result.groups.length, 1, 'Results in one group');
+ assert.equal(result.groups[0].lines.length, rows.length);
+ });
+
+ test('no single line collapse', () => {
+ rows = rows.slice(0, 7);
+ processor.context = 3;
+ const state = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 1,
+ };
+ const chunks = [{a: ['foo']}, {ab: rows}, {a: ['bar']}];
+ const result = processor.processNext(state, chunks);
+
+ // Results in one uncollapsed group with all rows.
+ assert.equal(result.groups.length, 1, 'Results in one group');
+ assert.equal(result.groups[0].lines.length, rows.length);
+ });
+
+ suite('with key location', () => {
+ let state: State;
+ let chunks: DiffContent[];
+
+ setup(() => {
+ state = {
+ lineNums: {left: 10, right: 100},
+ chunkIndex: 0,
+ };
+ processor.context = 10;
+ chunks = [{ab: rows}, {ab: ['foo'], keyLocation: true}, {ab: rows}];
+ });
+
+ test('context before', () => {
+ state.chunkIndex = 0;
+ const result = processor.processNext(state, chunks);
+
+ // The first chunk is split into two groups:
+ // 1) A context-control, hiding everything but the context before
+ // the key location.
+ // 2) The context before the key location.
+ // The key location is not processed in this call to processNext
+ assert.equal(result.groups.length, 2);
+ // The collapsed group has the hidden lines as its context group.
+ assert.equal(
+ result.groups[0].contextGroups[0].lines.length,
+ rows.length - processor.context
+ );
+ assert.equal(result.groups[1].lines.length, processor.context);
+ });
+
+ test('key location itself', () => {
+ state.chunkIndex = 1;
+ const result = processor.processNext(state, chunks);
+
+ // The second chunk results in a single group, that is just the
+ // line with the key location
+ assert.equal(result.groups.length, 1);
+ assert.equal(result.groups[0].lines.length, 1);
+ assert.equal(result.lineDelta.left, 1);
+ assert.equal(result.lineDelta.right, 1);
+ });
+
+ test('context after', () => {
+ state.chunkIndex = 2;
+ const result = processor.processNext(state, chunks);
+
+ // The last chunk is split into two groups:
+ // 1) The context after the key location.
+ // 1) A context-control, hiding everything but the context after the
+ // key location.
+ assert.equal(result.groups.length, 2);
+ assert.equal(result.groups[0].lines.length, processor.context);
+ // The collapsed group has the hidden lines as its context group.
+ assert.equal(
+ result.groups[1].contextGroups[0].lines.length,
+ rows.length - processor.context
+ );
+ });
+ });
+ });
+
+ suite('gr-diff-processor helpers', () => {
+ let rows: string[];
+
+ setup(() => {
+ rows = loremIpsum.split(' ');
+ });
+
+ test('linesFromRows', () => {
+ const startLineNum = 10;
+ let result = processor.linesFromRows(
+ GrDiffLineType.ADD,
+ rows,
+ startLineNum + 1
+ );
+
+ assert.equal(result.length, rows.length);
+ assert.equal(result[0].type, GrDiffLineType.ADD);
+ assert.notOk(result[0].hasIntralineInfo);
+ assert.equal(result[0].afterNumber, startLineNum + 1);
+ assert.notOk(result[0].beforeNumber);
+ assert.equal(
+ result[result.length - 1].afterNumber,
+ startLineNum + rows.length
+ );
+ assert.notOk(result[result.length - 1].beforeNumber);
+
+ result = processor.linesFromRows(
+ GrDiffLineType.REMOVE,
+ rows,
+ startLineNum + 1
+ );
+
+ assert.equal(result.length, rows.length);
+ assert.equal(result[0].type, GrDiffLineType.REMOVE);
+ assert.notOk(result[0].hasIntralineInfo);
+ assert.equal(result[0].beforeNumber, startLineNum + 1);
+ assert.notOk(result[0].afterNumber);
+ assert.equal(
+ result[result.length - 1].beforeNumber,
+ startLineNum + rows.length
+ );
+ assert.notOk(result[result.length - 1].afterNumber);
+ });
+ });
+ });
+});
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index b59d7a8b3..4a1fc29 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -22,4 +22,5 @@
ML_SUGGESTED_EDIT = 'UiFeature__ml_suggested_edit',
ML_SUGGESTED_EDIT_V2 = 'UiFeature__ml_suggested_edit_v2',
REVISION_PARENTS_DATA = 'UiFeature__revision_parents_data',
+ SIMPLIFIED_DIFF_PROCESSOR = 'UiFeature__simplified_diff_processor',
}
diff --git a/polygerrit-ui/app/types/common.ts b/polygerrit-ui/app/types/common.ts
index 3f02b2a..e12ee6c 100644
--- a/polygerrit-ui/app/types/common.ts
+++ b/polygerrit-ui/app/types/common.ts
@@ -41,7 +41,6 @@
ChangeMessageId,
ChangeMessageInfo,
ChangeSubmissionId,
- CommentInfo,
CommentLinkInfo,
CommentLinks,
CommentSide,
@@ -146,7 +145,6 @@
ChangeMessageId,
ChangeMessageInfo,
ChangeSubmissionId,
- CommentInfo,
CommentLinkInfo,
CommentLinks,
CommentRange,
@@ -809,6 +807,32 @@
ERROR = 'ERROR',
}
+/**
+ * The CommentInfo entity contains information about an inline comment.
+ * https://gerrit-review.googlesource.com/Documentation/rest-api-changes.html#comment-info
+ */
+export interface CommentInfo {
+ id: UrlEncodedCommentId;
+ updated: Timestamp;
+ // TODO(TS): Make this required. Every comment must have patch_set set.
+ patch_set?: RevisionPatchSetNum;
+ path?: string;
+ side?: CommentSide;
+ parent?: number;
+ line?: number;
+ range?: CommentRange;
+ in_reply_to?: UrlEncodedCommentId;
+ message?: string;
+ author?: AccountInfo;
+ tag?: string;
+ unresolved?: boolean;
+ change_message_id?: string;
+ commit_id?: string;
+ context_lines?: ContextLine[];
+ source_content_type?: string;
+ fix_suggestions?: FixSuggestionInfo[];
+}
+
export type DraftInfo = Omit<CommentInfo, 'id' | 'updated'> & {
// Must be set for all drafts.
// Drafts received from the backend will be modified immediately with
@@ -1203,6 +1227,7 @@
message?: string;
tag?: string;
unresolved?: boolean;
+ fix_suggestions?: FixSuggestionInfo[];
}
/**
@@ -1431,7 +1456,6 @@
robot_run_id: RobotRunId;
url?: string;
properties: {[propertyName: string]: string};
- fix_suggestions: FixSuggestionInfo[];
}
export type PathToRobotCommentsInfoMap = {[path: string]: RobotCommentInfo[]};
diff --git a/polygerrit-ui/app/utils/comment-util.ts b/polygerrit-ui/app/utils/comment-util.ts
index 7fad329..364f372 100644
--- a/polygerrit-ui/app/utils/comment-util.ts
+++ b/polygerrit-ui/app/utils/comment-util.ts
@@ -589,5 +589,8 @@
if (comment.tag) {
output.tag = comment.tag;
}
+ if (comment.fix_suggestions) {
+ output.fix_suggestions = comment.fix_suggestions;
+ }
return output;
}
diff --git a/tools/nongoogle.bzl b/tools/nongoogle.bzl
index 9f9b1b5..562b072 100644
--- a/tools/nongoogle.bzl
+++ b/tools/nongoogle.bzl
@@ -115,18 +115,18 @@
sha1 = "cb2f351bf4463751201f43bb99865235d5ba07ca",
)
- SSHD_VERS = "2.10.0"
+ SSHD_VERS = "2.12.0"
maven_jar(
name = "sshd-osgi",
artifact = "org.apache.sshd:sshd-osgi:" + SSHD_VERS,
- sha1 = "03677ac1da780b7bdb682da50b762d79ea0d940d",
+ sha1 = "32b8de1cbb722ba75bdf9898e0c41d42af00ce57",
)
maven_jar(
name = "sshd-sftp",
artifact = "org.apache.sshd:sshd-sftp:" + SSHD_VERS,
- sha1 = "88707339ac0693d48df0ec1bafb84c78d792ed08",
+ sha1 = "0f96f00a07b186ea62838a6a4122e8f4cad44df6",
)
maven_jar(
@@ -144,7 +144,7 @@
maven_jar(
name = "sshd-mina",
artifact = "org.apache.sshd:sshd-mina:" + SSHD_VERS,
- sha1 = "b1f77377fbc517400e7665d0b2c83b58b41aa45d",
+ sha1 = "8b202f7d4c0d7b714fd0c93a1352af52aa031149",
)
maven_jar(