blob: 192520d40eec60ccf0b91957609f56ddbc286f1f [file] [log] [blame]
Ben Rohlfs5ab68c42022-05-30 13:05:58 +02001/**
2 * @license
3 * Copyright 2022 Google LLC
4 * SPDX-License-Identifier: Apache-2.0
5 */
Ben Rohlfsb0f90c22022-06-20 08:55:57 +02006import {
7 BasePatchSetNum,
8 FileInfo,
9 FileNameToFileInfoMap,
10 PARENT,
11 PatchRange,
12 PatchSetNumber,
13 RevisionPatchSetNum,
14} from '../../types/common';
Ben Rohlfs3a03ddb2022-10-13 12:12:52 +020015import {combineLatest, of, from} from 'rxjs';
Ben Rohlfs5ab68c42022-05-30 13:05:58 +020016import {switchMap, map} from 'rxjs/operators';
17import {RestApiService} from '../../services/gr-rest-api/gr-rest-api';
Ben Rohlfs5ab68c42022-05-30 13:05:58 +020018import {select} from '../../utils/observable-util';
Ben Rohlfsb7ef7f72022-06-10 12:17:28 +020019import {FileInfoStatus, SpecialFilePath} from '../../constants/constants';
Ben Rohlfs5ab68c42022-05-30 13:05:58 +020020import {specialFilePathCompare} from '../../utils/path-list-util';
Dhruv Srivastavab26f09b2023-09-28 10:04:19 +020021import {Model} from '../base/model';
Ben Rohlfs5ab68c42022-05-30 13:05:58 +020022import {define} from '../dependency';
23import {ChangeModel} from './change-model';
Ben Rohlfsb7ef7f72022-06-10 12:17:28 +020024import {CommentsModel} from '../comments/comments-model';
Ben Rohlfsfef94442023-04-20 13:02:33 +020025import {Timing} from '../../constants/reporting';
26import {ReportingService} from '../../services/gr-reporting/gr-reporting';
Ben Rohlfs5ab68c42022-05-30 13:05:58 +020027
Ben Rohlfs425a0e92022-12-20 21:36:19 +010028export type FileNameToNormalizedFileInfoMap = {
29 [name: string]: NormalizedFileInfo;
30};
Ben Rohlfs5ab68c42022-05-30 13:05:58 +020031export interface NormalizedFileInfo extends FileInfo {
32 __path: string;
Ben Rohlfsb7ef7f72022-06-10 12:17:28 +020033 // Compared to `FileInfo` these four props are required here.
34 lines_inserted: number;
35 lines_deleted: number;
36 size_delta: number; // in bytes
37 size: number; // in bytes
38}
39
40export function normalize(file: FileInfo, path: string): NormalizedFileInfo {
41 return {
42 __path: path,
43 // These 4 props are required in NormalizedFileInfo, but optional in
44 // FileInfo. So let's set a default value, if not already set.
45 lines_inserted: 0,
46 lines_deleted: 0,
47 size_delta: 0,
48 size: 0,
49 ...file,
50 };
Ben Rohlfs5ab68c42022-05-30 13:05:58 +020051}
52
53function mapToList(map?: FileNameToFileInfoMap): NormalizedFileInfo[] {
54 const list: NormalizedFileInfo[] = [];
55 for (const [key, value] of Object.entries(map ?? {})) {
Ben Rohlfsb7ef7f72022-06-10 12:17:28 +020056 list.push(normalize(value, key));
Ben Rohlfs5ab68c42022-05-30 13:05:58 +020057 }
58 list.sort((f1, f2) => specialFilePathCompare(f1.__path, f2.__path));
59 return list;
60}
61
Ben Rohlfsb7ef7f72022-06-10 12:17:28 +020062export function addUnmodified(
63 files: NormalizedFileInfo[],
64 commentedPaths: string[]
65) {
66 const combined = [...files];
67 for (const commentedPath of commentedPaths) {
68 if (commentedPath === SpecialFilePath.PATCHSET_LEVEL_COMMENTS) continue;
69 if (files.some(f => f.__path === commentedPath)) continue;
70 if (
71 files.some(
72 f => f.status === FileInfoStatus.RENAMED && f.old_path === commentedPath
73 )
74 ) {
75 continue;
76 }
77 combined.push(
78 normalize({status: FileInfoStatus.UNMODIFIED}, commentedPath)
79 );
80 }
81 combined.sort((f1, f2) => specialFilePathCompare(f1.__path, f2.__path));
82 return combined;
83}
84
Ben Rohlfs5ab68c42022-05-30 13:05:58 +020085export interface FilesState {
86 // TODO: Move reviewed files from change model into here. Change model is
87 // already large and complex, so the files model is a better fit.
88
Ben Rohlfsb0f90c22022-06-20 08:55:57 +020089 /**
90 * Basic file and diff information of all files for the currently chosen
91 * patch range.
92 */
Ben Rohlfs5ab68c42022-05-30 13:05:58 +020093 files: NormalizedFileInfo[];
Ben Rohlfsb0f90c22022-06-20 08:55:57 +020094
95 /**
96 * Basic file and diff information of all files for the left chosen patchset
97 * compared against its base (aka parent).
98 *
99 * Empty if the left chosen patchset is PARENT.
100 */
101 filesLeftBase: NormalizedFileInfo[];
102
103 /**
104 * Basic file and diff information of all files for the right chosen patchset
105 * compared against its base (aka parent).
106 *
107 * Empty if the left chosen patchset is PARENT.
108 */
109 filesRightBase: NormalizedFileInfo[];
Ben Rohlfs5ab68c42022-05-30 13:05:58 +0200110}
111
112const initialState: FilesState = {
113 files: [],
Ben Rohlfsb0f90c22022-06-20 08:55:57 +0200114 filesLeftBase: [],
115 filesRightBase: [],
Ben Rohlfs5ab68c42022-05-30 13:05:58 +0200116};
117
118export const filesModelToken = define<FilesModel>('files-model');
119
Chris Poucet8c610782022-10-24 12:45:46 +0200120export class FilesModel extends Model<FilesState> {
Ben Rohlfs5ab68c42022-05-30 13:05:58 +0200121 public readonly files$ = select(this.state$, state => state.files);
122
Ben Rohlfs425a0e92022-12-20 21:36:19 +0100123 /**
124 * `files$` only includes the files that were modified. Here we also include
125 * all unmodified files that have comments with
126 * `status: FileInfoStatus.UNMODIFIED`.
127 */
128 public readonly filesIncludingUnmodified$ = select(
Ben Rohlfsb7ef7f72022-06-10 12:17:28 +0200129 combineLatest([this.files$, this.commentsModel.commentedPaths$]),
130 ([files, commentedPaths]) => addUnmodified(files, commentedPaths)
131 );
132
Ben Rohlfsea6c9fc2022-06-20 14:55:35 +0200133 public readonly filesLeftBase$ = select(
134 this.state$,
135 state => state.filesLeftBase
136 );
137
138 public readonly filesRightBase$ = select(
139 this.state$,
140 state => state.filesRightBase
141 );
142
Ben Rohlfs5ab68c42022-05-30 13:05:58 +0200143 constructor(
144 readonly changeModel: ChangeModel,
Ben Rohlfsb7ef7f72022-06-10 12:17:28 +0200145 readonly commentsModel: CommentsModel,
Ben Rohlfsfef94442023-04-20 13:02:33 +0200146 readonly restApiService: RestApiService,
147 private readonly reporting: ReportingService
Ben Rohlfs5ab68c42022-05-30 13:05:58 +0200148 ) {
149 super(initialState);
150 this.subscriptions = [
Ben Rohlfsfef94442023-04-20 13:02:33 +0200151 this.reportChangeDataStart(),
152 this.reportChangeDataEnd(),
Ben Rohlfsb0f90c22022-06-20 08:55:57 +0200153 this.subscribeToFiles(
154 (psLeft, psRight) => {
155 return {basePatchNum: psLeft, patchNum: psRight};
156 },
157 files => {
158 return {files: [...files]};
159 }
160 ),
161 this.subscribeToFiles(
162 (psLeft, _) => {
Frank Borden1c3479b2023-03-29 15:59:46 +0200163 if (psLeft === PARENT || (psLeft as PatchSetNumber) <= 0)
164 return undefined;
Ben Rohlfsb0f90c22022-06-20 08:55:57 +0200165 return {basePatchNum: PARENT, patchNum: psLeft as PatchSetNumber};
166 },
167 files => {
168 return {filesLeftBase: [...files]};
169 }
170 ),
171 this.subscribeToFiles(
172 (psLeft, psRight) => {
Frank Borden1c3479b2023-03-29 15:59:46 +0200173 if (psLeft === PARENT || (psLeft as PatchSetNumber) <= 0)
174 return undefined;
Ben Rohlfsb0f90c22022-06-20 08:55:57 +0200175 return {basePatchNum: PARENT, patchNum: psRight as PatchSetNumber};
176 },
177 files => {
178 return {filesRightBase: [...files]};
179 }
180 ),
Ben Rohlfs5ab68c42022-05-30 13:05:58 +0200181 ];
182 }
183
Ben Rohlfsfef94442023-04-20 13:02:33 +0200184 private reportChangeDataStart() {
185 return combineLatest([this.changeModel.loading$]).subscribe(
186 ([changeLoading]) => {
187 if (changeLoading) {
188 this.reporting.time(Timing.CHANGE_DATA);
189 }
190 }
191 );
192 }
193
194 private reportChangeDataEnd() {
195 return combineLatest([this.changeModel.loading$, this.files$]).subscribe(
196 ([changeLoading, files]) => {
197 if (!changeLoading && files.length > 0) {
198 this.reporting.timeEnd(Timing.CHANGE_DATA);
199 }
200 }
201 );
202 }
203
Ben Rohlfsb0f90c22022-06-20 08:55:57 +0200204 private subscribeToFiles(
205 rangeChooser: (
206 basePatchNum: BasePatchSetNum,
207 patchNum: RevisionPatchSetNum
208 ) => PatchRange | undefined,
209 filesToState: (files: NormalizedFileInfo[]) => Partial<FilesState>
210 ) {
211 return combineLatest([
Ben Rohlfsb0f90c22022-06-20 08:55:57 +0200212 this.changeModel.changeNum$,
213 this.changeModel.basePatchNum$,
214 this.changeModel.patchNum$,
215 ])
216 .pipe(
Ben Rohlfsd49e8332023-04-20 22:40:04 +0200217 switchMap(([changeNum, basePatchNum, patchNum]) => {
Ben Rohlfsb0f90c22022-06-20 08:55:57 +0200218 if (!changeNum || !patchNum) return of({});
219 const range = rangeChooser(basePatchNum, patchNum);
220 if (!range) return of({});
221 return from(
222 this.restApiService.getChangeOrEditFiles(changeNum, range)
223 );
224 }),
225 map(mapToList),
226 map(filesToState)
227 )
228 .subscribe(state => {
Ben Rohlfs5fa82932022-09-21 09:29:31 +0200229 this.updateState(state);
Ben Rohlfsb0f90c22022-06-20 08:55:57 +0200230 });
231 }
Ben Rohlfs5ab68c42022-05-30 13:05:58 +0200232}