| /** |
| * @license |
| * Copyright 2023 Google LLC |
| * SPDX-License-Identifier: Apache-2.0 |
| */ |
| import { |
| ChangeInfo, |
| RelatedChangeAndCommitInfo, |
| SubmittedTogetherInfo, |
| } from '../../types/common'; |
| import {RestApiService} from '../../services/gr-rest-api/gr-rest-api'; |
| import {select} from '../../utils/observable-util'; |
| import {Model} from '../model'; |
| import {define} from '../dependency'; |
| import {ChangeModel} from './change-model'; |
| import {combineLatest, forkJoin, from, of} from 'rxjs'; |
| import {map, switchMap} from 'rxjs/operators'; |
| import {ConfigModel} from '../config/config-model'; |
| import {ChangeStatus} from '../../api/rest-api'; |
| import {isDefined} from '../../types/types'; |
| |
| export interface RelatedChangesState { |
| /** `undefined` means "not yet loaded". */ |
| relatedChanges?: RelatedChangeAndCommitInfo[]; |
| submittedTogether?: SubmittedTogetherInfo; |
| cherryPicks?: ChangeInfo[]; |
| conflictingChanges?: ChangeInfo[]; |
| sameTopicChanges?: ChangeInfo[]; |
| revertingChanges: ChangeInfo[]; |
| } |
| |
| const initialState: RelatedChangesState = { |
| relatedChanges: undefined, |
| submittedTogether: undefined, |
| cherryPicks: undefined, |
| conflictingChanges: undefined, |
| sameTopicChanges: undefined, |
| revertingChanges: [], |
| }; |
| |
| export const relatedChangesModelToken = define<RelatedChangesModel>( |
| 'related-changes-model' |
| ); |
| |
| export class RelatedChangesModel extends Model<RelatedChangesState> { |
| public readonly relatedChanges$ = select( |
| this.state$, |
| state => state.relatedChanges |
| ); |
| |
| public readonly submittedTogether$ = select( |
| this.state$, |
| state => state.submittedTogether |
| ); |
| |
| public readonly cherryPicks$ = select( |
| this.state$, |
| state => state.cherryPicks |
| ); |
| |
| public readonly conflictingChanges$ = select( |
| this.state$, |
| state => state.conflictingChanges |
| ); |
| |
| public readonly sameTopicChanges$ = select( |
| this.state$, |
| state => state.sameTopicChanges |
| ); |
| |
| /** |
| * Emits all changes that have reverted the current change, based on |
| * information from parsed change messages. Abandoned changes are not |
| * included. |
| */ |
| public readonly revertingChanges$ = select( |
| this.state$, |
| state => state.revertingChanges |
| ); |
| |
| /** |
| * Emits one reverting change (if there is any) from revertingChanges$. |
| * It prefers MERGED changes. Otherwise the choice is random. |
| */ |
| public readonly revertingChange$ = select( |
| this.revertingChanges$, |
| revertingChanges => { |
| if (revertingChanges.length === 0) return undefined; |
| const submittedRevert = revertingChanges.find( |
| c => c.status === ChangeStatus.MERGED |
| ); |
| if (submittedRevert) return submittedRevert; |
| return revertingChanges[0]; |
| } |
| ); |
| |
| /** |
| * Determines whether the change has a parent change. If there |
| * is a relation chain, and the change id is not the last item of the |
| * relation chain, then there is a parent. |
| */ |
| public readonly hasParent$ = select( |
| combineLatest([this.changeModel.change$, this.relatedChanges$]), |
| ([change, relatedChanges]) => { |
| if (!change) return undefined; |
| if (relatedChanges === undefined) return undefined; |
| if (relatedChanges.length === 0) return false; |
| const lastChangeId = relatedChanges[relatedChanges.length - 1].change_id; |
| return lastChangeId !== change.change_id; |
| } |
| ); |
| |
| constructor( |
| readonly changeModel: ChangeModel, |
| readonly configModel: ConfigModel, |
| readonly restApiService: RestApiService |
| ) { |
| super(initialState); |
| this.subscriptions = [ |
| this.loadRelatedChanges(), |
| this.loadSubmittedTogether(), |
| this.loadCherryPicks(), |
| this.loadConflictingChanges(), |
| this.loadSameTopicChanges(), |
| this.loadRevertingChanges(), |
| ]; |
| } |
| |
| private loadRelatedChanges() { |
| return combineLatest([ |
| this.changeModel.changeNum$, |
| this.changeModel.latestPatchNum$, |
| ]) |
| .pipe( |
| switchMap(([changeNum, latestPatchNum]) => { |
| if (!changeNum || !latestPatchNum) return of(undefined); |
| return from( |
| this.restApiService |
| .getRelatedChanges(changeNum, latestPatchNum) |
| .then(info => info?.changes ?? []) |
| ); |
| }) |
| ) |
| .subscribe(relatedChanges => { |
| this.updateState({relatedChanges}); |
| }); |
| } |
| |
| private loadSubmittedTogether() { |
| return this.changeModel.changeNum$ |
| .pipe( |
| switchMap(changeNum => { |
| if (!changeNum) return of(undefined); |
| return from( |
| this.restApiService.getChangesSubmittedTogether(changeNum) |
| ); |
| }) |
| ) |
| .subscribe(submittedTogether => { |
| this.updateState({submittedTogether}); |
| }); |
| } |
| |
| private loadCherryPicks() { |
| return combineLatest([ |
| this.changeModel.changeNum$, |
| this.changeModel.changeId$, |
| this.changeModel.repo$, |
| ]) |
| .pipe( |
| switchMap(([changeNum, changeId, repo]) => { |
| if (!changeNum || !changeId || !repo) return of(undefined); |
| return from( |
| this.restApiService.getChangeCherryPicks(repo, changeId, changeNum) |
| ); |
| }) |
| ) |
| .subscribe(cherryPicks => { |
| this.updateState({cherryPicks}); |
| }); |
| } |
| |
| private loadConflictingChanges() { |
| return combineLatest([ |
| this.changeModel.changeNum$, |
| this.changeModel.status$, |
| this.changeModel.mergeable$, |
| ]) |
| .pipe( |
| switchMap(([changeNum, status, mergeable]) => { |
| if (!changeNum || !status || !mergeable) return of(undefined); |
| if (status !== ChangeStatus.NEW) return of(undefined); |
| return from(this.restApiService.getChangeConflicts(changeNum)); |
| }) |
| ) |
| .subscribe(conflictingChanges => { |
| this.updateState({conflictingChanges}); |
| }); |
| } |
| |
| private loadSameTopicChanges() { |
| return combineLatest([ |
| this.changeModel.changeNum$, |
| this.changeModel.topic$, |
| this.configModel.serverConfig$, |
| ]) |
| .pipe( |
| switchMap(([changeNum, topic, config]) => { |
| if (!changeNum || !topic || !config) return of(undefined); |
| if (config.change.submit_whole_topic) return of(undefined); |
| return from( |
| this.restApiService.getChangesWithSameTopic(topic, { |
| openChangesOnly: true, |
| changeToExclude: changeNum, |
| }) |
| ); |
| }) |
| ) |
| .subscribe(sameTopicChanges => { |
| this.updateState({sameTopicChanges}); |
| }); |
| } |
| |
| private loadRevertingChanges() { |
| return this.changeModel.revertingChangeIds$ |
| .pipe( |
| switchMap(changeIds => { |
| if (!changeIds?.length) return of([]); |
| return forkJoin( |
| changeIds.map(changeId => |
| from(this.restApiService.getChange(changeId)) |
| ) |
| ); |
| }), |
| map(changes => changes.filter(isDefined)), |
| map(changes => changes.filter(c => c.status !== ChangeStatus.ABANDONED)) |
| ) |
| .subscribe(revertingChanges => { |
| this.updateState({revertingChanges}); |
| }); |
| } |
| } |