blob: 9ae4295da20dd2efbeeb4266c2208f56d31311da [file] [log] [blame]
/**
* @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});
});
}
}