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(