1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
|
---
stage: Create
group: Code Review
info: Any user with at least the Maintainer role can merge updates to this content. For details, see https://docs.gitlab.com/ee/development/development_processes.html#development-guidelines-review.
description: "Developer documentation explaining how the different parts of the Vue-based frontend diffs are generated."
---
# Merge request diffs frontend overview
This document provides an overview on how the frontend diffs Vue application works, and
the various different parts that exist. It should help contributors:
- Understand how the diffs Vue app is set up.
- Identify any areas that need improvement.
This document is a living document. Update it whenever anything significant changes in
the diffs application.
## Diffs Vue app
### Components
The Vue app for rendering diffs uses many different Vue components, some of which get shared
with other areas of the GitLab app. The below chart shows the direction for which components
get rendered.
This chart contains several types of items:
| Legend item | Interpretation |
| ----------- | -------------- |
| `xxx~~`, `ee-xxx~~` | A shortened directory path name. Can be found in `[ee]/app/assets/javascripts`, and omits `0..n` nested folders. |
| Rectangular nodes | Files. |
| Oval nodes | Plain language describing a deeper concept. |
| Double-rectangular nodes | Simplified code branch. |
| Diamond and circle nodes | Branches that have 2 (diamond) or 3+ (circle) options. |
| Pendant / banner nodes (left notch, right square) | A parent directory to shorten nested paths. |
| `./` | A path relative to the closest parent directory pendant node. Non-relative paths nested under parent pendant nodes are not in that directory. |
```mermaid
%%{init: { "fontFamily": "GitLab Sans" }}%%
flowchart TB
accTitle: Component rendering
accDescr: Flowchart of how components are rendered in the GitLab front end
classDef code font-family: monospace;
A["diffs~~app.vue"]
descVirtualScroller(["Virtual Scroller"])
codeForFiles[["v-for(diffFiles)"]]
B["diffs~~diff_file.vue"]
C["diffs~~diff_file_header.vue"]
D["diffs~~diff_stats.vue"]
E["diffs~~diff_content.vue"]
boolFileIsText{isTextFile}
boolOnlyWhitespace{isWhitespaceOnly}
boolNotDiffable{notDiffable}
boolNoPreview{noPreview}
descShowChanges(["Show button to "Show changes""])
%% Non-text changes
dirDiffViewer>"vue_shared~~diff_viewer"]
F["./viewers/not_diffable.vue"]
G["./viewers/no_preview.vue"]
H["./diff_viewer.vue"]
I["diffs~~diff_view.vue"]
boolIsRenamed{isRenamed}
boolIsModeChanged{isModeChanged}
boolFileHasNoPath{hasNewPath}
boolIsImage{isImage}
J["./viewers/renamed.vue"]
K["./viewers/mode_changed.vue"]
descNoViewer(["No viewer is rendered"])
L["./viewers/image_diff_viewer.vue"]
M["./viewers/download.vue"]
N["vue_shared~~download_diff_viewer.vue"]
boolImageIsReplaced{isReplaced}
O["vue_shared~~image_viewer.vue"]
switchImageMode((image_diff_viewer.mode))
P["./viewers/image_diff/onion_skin_viewer.vue"]
Q["./viewers/image_diff/swipe_viewer.vue"]
R["./viewers/image_diff/two_up_viewer.vue"]
S["diffs~~image_diff_overlay.vue"]
codeForImageDiscussions[["v-for(discussions)"]]
T["vue_shared~~design_note_pin.vue"]
U["vue_shared~~user_avatar_link.vue"]
V["diffs~~diff_discussions.vue"]
W["batch_comments~~diff_file_drafts.vue"]
codeForTwoUpDiscussions[["v-for(discussions)"]]
codeForTwoUpDrafts[["v-for(drafts)"]]
X["notes~~notable_discussion.vue"]
%% Text-file changes
codeForDiffLines[["v-for(diffLines)"]]
Y["diffs~~diff_expansion_cell.vue"]
Z["diffs~~diff_row.vue"]
AA["diffs~~diff_line.vue"]
AB["batch_comments~~draft_note.vue"]
AC["diffs~~diff_comment_cell.vue"]
AD["diffs~~diff_gutter_avatars.vue"]
AE["ee-diffs~~inline_findings_gutter_icon_dropdown.vue"]
AF["notes~~noteable_note.vue"]
AG["notes~~note_actions.vue"]
AH["notes~~note_body.vue"]
AI["notes~~note_header.vue"]
AJ["notes~~reply_button.vue"]
AK["notes~~note_awards_list.vue"]
AL["notes~~note_edited_text.vue"]
AM["notes~~note_form.vue"]
AN["vue_shared~~awards_list.vue"]
AO["emoji~~picker.vue"]
AP["emoji~~emoji_list.vue"]
descEmojiVirtualScroll(["Virtual Scroller"])
AQ["emoji~~category.vue"]
AR["emoji~emoji_category.vue"]
AS["vue_shared~~markdown_editor.vue"]
class codeForFiles,codeForImageDiscussions code;
class codeForTwoUpDiscussions,codeForTwoUpDrafts code;
class codeForDiffLines code;
%% Also apply code styling to this switch node
class switchImageMode code;
%% Also apply code styling to these boolean nodes
class boolFileIsText,boolOnlyWhitespace,boolNotDiffable,boolNoPreview code;
class boolIsRenamed,boolIsModeChanged,boolFileHasNoPath,boolIsImage code;
class boolImageIsReplaced code;
A --> descVirtualScroller
A -->|"Virtual Scroller is
disabled when
Find in page search
(Cmd/Ctrl+f) is used."|codeForFiles
descVirtualScroller --> codeForFiles
codeForFiles --> B --> C --> D
B --> E
%% File view flags cascade
E --> boolFileIsText
boolFileIsText --> |yes| I
boolFileIsText --> |no| boolOnlyWhitespace
boolOnlyWhitespace --> |yes| descShowChanges
boolOnlyWhitespace --> |no| dirDiffViewer
dirDiffViewer --> H
H --> boolNotDiffable
boolNotDiffable --> |yes| F
boolNotDiffable --> |no| boolNoPreview
boolNoPreview --> |yes| G
boolNoPreview --> |no| boolIsRenamed
boolIsRenamed --> |yes| J
boolIsRenamed --> |no| boolIsModeChanged
boolIsModeChanged --> |yes| K
boolIsModeChanged --> |no| boolFileHasNoPath
boolFileHasNoPath --> |yes| boolIsImage
boolFileHasNoPath --> |no| descNoViewer
boolIsImage --> |yes| L
boolIsImage --> |no| M
M --> N
%% Image diff viewer
L --> boolImageIsReplaced
boolImageIsReplaced --> |yes| switchImageMode
boolImageIsReplaced --> |no| O
switchImageMode -->|"'twoup' (default)"| R
switchImageMode -->|'onion'| P
switchImageMode -->|'swipe'| Q
P & Q --> S
S --> codeForImageDiscussions
S --> AM
R-->|"Rendered in
note container div"|U & W & V
%% Do not combine this with the "P & Q --> S" statement above
%% The order of these node relationships defines the
%% layout of the graph, and we need it in this order.
R --> S
V --> codeForTwoUpDiscussions
W --> codeForTwoUpDrafts
%% This invisible link forces `noteable_discussion`
%% to render above `design_note_pin`
X ~~~ T
codeForTwoUpDrafts --> AB
codeForImageDiscussions & codeForTwoUpDiscussions & codeForTwoUpDrafts --> T
codeForTwoUpDiscussions --> X
%% Text file diff viewer
I --> codeForDiffLines
codeForDiffLines --> Z
codeForDiffLines -->|"isMatchLine?"| Y
codeForDiffLines -->|"hasCodeQuality?"| AA
codeForDiffLines -->|"hasDraftNote(s)?"| AB
Z -->|"hasCodeQuality?"| AE
Z -->|"hasDiscussions?"| AD
AA --> AC
%% Draft notes
AB --> AF
AF --> AG & AH & AI
AG --> AJ
AH --> AK & AL & AM
AK --> AN --> AO --> AP --> descEmojiVirtualScroll --> AQ --> AR
AM --> AS
```
Some of the components are rendered more than others, but the main component is `diff_row.vue`.
This component renders every diff line in a diff file. For performance reasons, this
component is a functional component. However, when we upgrade to Vue 3, this is no longer
required.
The main diff app component is the main entry point to the diffs app. One of the most important parts
of this component is to dispatch the action that assigns discussions to diff lines. This action
gets dispatched after the metadata request is completed, and after the batch diffs requests are
finished. There is also a watcher set up to watches for changes in both the diff files array and the notes
array. Whenever a change happens here, the set discussion action gets dispatched.
The DiffRow component is set up in a way that allows for us to store the diff line data in one format.
Previously, we had to request two different formats for inline and side-by-side. The DiffRow component
then uses this standard format to render the diff line data. With this standard format, the user
can then switch between inline and side-by-side without the need to re-fetch any data.
NOTE:
For this component, a lot of the data used and rendered gets memoized and cached, based on
various conditions. It is possible that data sometimes gets cached between each different
component render.
### Vuex store
The Vuex store for the diffs app consists of 3 different modules:
- Notes
- Diffs
- Batch comments
The notes module is responsible for the discussions, including diff discussions. In this module,
the discussions get fetched, and the polling for new discussions is setup. This module gets shared
with the issue app as well, so changes here need to be tested in both issues and merge requests.
The diffs module is responsible for the everything related to diffs. This includes, but is not limited
to, fetching diffs, assigning diff discussions to lines, and creating diff discussions.
Finally, the batch comments module is not complex, and is responsible only for the draft comments feature.
However, this module does dispatch actions in the notes and diff modules whenever draft comments
are published.
### API Requests
#### Metadata
The diffs metadata endpoint exists to fetch the base data the diffs app requires quickly, without
the need to fetch all the diff files. This includes, but is not limited to:
- Diff filenames, including some extra meta data for diff files
- Added and removed line numbers
- Branch names
- Diff versions
The most important part of the metadata response is the diff filenames. This data allows the diffs
app to render the file browser inside of the diffs app, without waiting for all batch diffs
requests to complete.
When the metadata response is received, the diff file data is processed into the correct structure
that the frontend requires to render the file browser in either tree view or list view.
The structure for this file object is:
```javascript
{
"key": "",
"path": "",
"name": "",
"type": "",
"tree": [],
"changed": true,
"diffLoaded": false,
"filePaths": {
"old": file.old_path,
"new": file.new_path
},
"tempFile": false,
"deleted": false,
"fileHash": "",
"addedLines": 1,
"removedLines": 1,
"parentPath": "/",
"submodule": false
}
```
#### Batch diffs
To reduce the response size for the diffs endpoint, we are splitting this response up into different
requests, to:
- Reduces the response size of each request.
- Allows the diffs app to start rendering diffs as quickly as the first request finishes.
To make the first request quicker, the request gets sent asking for a small amount of
diffs. The number of diffs requested then increases, until the maximum number of diffs per request is 30.
When the request finishes, the diffs app formats the data received into a format that makes
it easier for the diffs app to render the diffs lines.
```mermaid
%%{init: { "fontFamily": "GitLab Sans" }}%%
graph TD
accTitle: Formatting diffs
accDescr: A flowchart of steps taken when rendering a diff, including retrieval and display preparations
A[fetchDiffFilesBatch] -->
B[commit SET_DIFF_DATA_BATCH] -->
C[prepareDiffData] -->
D[prepareRawDiffFile] -->
E[ensureBasicDiffFileLines] -->
F[prepareDiffFileLines] -->
G[finalizeDiffFile] -->
H[deduplicateFilesList]
```
After this has been completed, the diffs app can now begin to render the diff lines. However, before
anything can be rendered the diffs app does one more format. It takes the diff line data, and maps
the data into a format for easier switching between inline and side-by-side modes. This
formatting happens in a computed property inside the `diff_content.vue` component.
### Render queue
NOTE:
This _might_ not be required any more. Some investigation work is required to decide
the future of the render queue. The virtual scroll bar we created has probably removed
any performance benefit we got from this approach.
To render diffs quickly, we have a render queue that allows the diffs to render only if the
browser is idle. This saves the browser getting frozen when rendering a lot of large diffs at once,
and allows us to reduce the total blocking time.
This pipeline of rendering files happens only if all the below conditions are `true` for every
diff file. If any of these are `false`, then this render queue does not happen and the diffs get
rendered as expected.
- Are the diffs in this file already rendered?
- Does this diff have a viewer? (Meaning, is it not a download?)
- Is the diff expanded?
This chart gives a brief overview of the pipeline that happens:
```mermaid
%%{init: { "fontFamily": "GitLab Sans" }}%%
graph TD
accTitle: Render queue pipeline
accDescr: Flowchart of the steps in the render queue pipeline
A[startRenderDiffsQueue] -->B
B[commit RENDER_FILE current file index] -->C
C[canRenderNextFile?]
C -->|Yes| D[Render file] -->B
C -->|No| E[Re-run requestIdleCallback] -->C
```
The checks that happen:
- Is the idle time remaining less than 5 ms?
- Have we already tried to render this file 4 times?
After these checks happen, the file is marked in Vuex as `renderable`, which allows the diffs
app to start rendering the diff lines and discussions.
|