blob: 9cf54237e2af9d1cb1b8f0a7adf5183052d4a4a1 [file] [log] [blame]
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {customElement, state} from 'lit/decorators.js';
import {css, html, HTMLTemplateResult, LitElement} from 'lit';
import {resolve} from '../../../models/dependency';
import {subscribe} from '../../lit/subscription-controller';
import {changeModelToken} from '../../../models/change/change-model';
import {
CommitId,
EDIT,
NumericChangeId,
ParentInfo,
PatchSetNumber,
RepoName,
RevisionInfo,
} from '../../../api/rest-api';
import {fontStyles} from '../../../styles/gr-font-styles';
import {branchName} from '../../../utils/patch-set-util';
import {when} from 'lit/directives/when.js';
import {createChangeUrl} from '../../../models/views/change';
import {sharedStyles} from '../../../styles/shared-styles';
import {configModelToken} from '../../../models/config/config-model';
import {getDocUrl} from '../../../utils/url-util';
@customElement('gr-revision-parents')
export class GrRevisionParents extends LitElement {
@state() repo?: RepoName;
@state() revision?: RevisionInfo;
@state() baseRevision?: RevisionInfo;
@state() showDetails = false;
@state() docsBaseUrl = '';
private readonly getChangeModel = resolve(this, changeModelToken);
private readonly getConfigModel = resolve(this, configModelToken);
constructor() {
super();
subscribe(
this,
() => this.getChangeModel().revision$,
x => {
if (x?._number === EDIT) x = undefined;
this.revision = x as RevisionInfo | undefined;
}
);
subscribe(
this,
() => this.getChangeModel().repo$,
x => (this.repo = x)
);
subscribe(
this,
() => this.getChangeModel().baseRevision$,
x => (this.baseRevision = x)
);
subscribe(
this,
() => this.getConfigModel().docsBaseUrl$,
x => (this.docsBaseUrl = x)
);
}
static override get styles() {
return [
fontStyles,
sharedStyles,
css`
:host {
display: block;
}
div.container {
padding: var(--spacing-m) var(--spacing-l);
border-top: 1px solid var(--border-color);
}
.sections {
display: flex;
}
.section {
margin-top: 0;
padding-right: var(--spacing-xxl);
}
.section h4 {
margin: 0;
}
.title {
font-weight: var(--font-weight-bold);
}
.messageContainer {
display: flex;
padding: var(--spacing-m) var(--spacing-l);
border-top: 1px solid var(--border-color);
}
.messageContainer.info {
background-color: var(--info-background);
}
.messageContainer.warning {
background-color: var(--warning-background);
}
.messageContainer gr-icon {
margin-right: var(--spacing-m);
}
.messageContainer.info gr-icon {
color: var(--info-foreground);
}
.messageContainer.warning gr-icon {
color: var(--warning-foreground);
}
.messageContainer .text {
max-width: 600px;
}
.messageContainer .text p {
margin: 0;
}
.messageContainer .text gr-button {
margin-left: -4px;
}
gr-commit-info {
display: inline-block;
}
`,
];
}
override render() {
return html`${this.renderMessage()}${this.renderDetails()}`;
}
private renderMessage() {
if (!this.baseRevision || !this.revision) return;
// For merges we are only interested in the target branch parent, which is [0].
// And for non-merges there is no more than 1 parent, so [0] is the only choice.
const parentLeft = this.baseRevision?.parents_data?.[0];
const parentRight = this.revision?.parents_data?.[0];
// Note that is you diff a patchset against its base, then baseRevision will be
// `undefined`. Thus after this line we know that we are dealing with diffs
// of the type "patchset x vs patchset y".
if (!parentLeft || !parentRight) return;
const psLeft = this.baseRevision?._number;
const psRight = this.revision?._number;
const parentCommitLeft = parentLeft.commit_id;
const parentCommitRight = parentRight.commit_id;
const branchLeft = branchName(parentLeft.branch_name);
const branchRight = branchName(parentRight.branch_name);
const isMergedLeft = parentLeft.is_merged_in_target_branch;
const isMergedRight = parentRight.is_merged_in_target_branch;
const changeNumLeft = parentLeft.change_number;
const changeNumRight = parentRight.change_number;
const changePsLeft = parentLeft.patch_set_number;
const changePsRight = parentRight.patch_set_number;
if (parentCommitLeft === parentCommitRight) return;
// Subsequently: different commit
if (branchLeft !== branchRight) {
return html`
${this.renderWarning(
'warning',
html`
Patchset ${psLeft} and ${psRight} are targeting different branches.
`
)}
`;
}
// Subsequently: different commit, same target branch
// Such a situation is really rare and weird. You have to do something like committing to one
// branch and then uploading to another. This warning should actually also be shown, if
// you are not comparing PS X and PS Y, because it is generally a weird patchset state.
const isWeirdLeft = !isMergedLeft && !changeNumLeft;
const isWeirdRight = !isMergedRight && !changeNumRight;
if (isWeirdLeft || isWeirdRight) {
const weirdPs =
isWeirdLeft && isWeirdRight
? `${psLeft} and ${psRight} are`
: isWeirdLeft
? `${psLeft} is`
: `${psRight} is`;
return html`
${this.renderWarning(
'warning',
html`
Patchset ${weirdPs} based on a commit that neither exists in its
target branch, nor is it a commit of another active change.
`
)}
`;
}
if (
changeNumLeft &&
changeNumRight &&
changeNumLeft === changeNumRight &&
// This check is probably redundant, because "same change and ps" should mean "same commit".
psLeft !== psRight
) {
return html`
${this.renderWarning(
'info',
html`
The change was rebased from patchset
${this.renderPatchsetLink(changeNumLeft, changePsLeft)} onto
patchset ${this.renderPatchsetLink(changeNumLeft, changePsRight)} of
change ${this.renderChangeLink(changeNumLeft)}
${when(isMergedRight, () => html` (MERGED)`)}.
`
)}
`;
}
// No additional info? Then "different commit" and "same branch" means "standard rebase".
if (isMergedLeft && isMergedRight) {
return html`
${this.renderWarning(
'info',
html`
The change was rebased from
${this.renderCommitLink(parentCommitLeft, false)} onto
${this.renderCommitLink(parentCommitRight, false)}.
`
)}
`;
}
// By now we know that we have different commit, same target branch, no weird parent,
// and not a standard rebase. So let's spell out what the left and right side are based on.
return this.renderWarning(
'warning',
html`${this.renderInfo(this.baseRevision)}<br />${this.renderInfo(
this.revision
)}`
);
}
private renderInfo(rev: RevisionInfo) {
const parent = rev.parents_data?.[0];
if (!parent) return;
const ps = rev._number;
const isMerged = parent.is_merged_in_target_branch;
const changeNum = parent.change_number;
if (changeNum && !isMerged) {
return html`
Patchset ${ps} is based on patchset
${this.renderPatchsetLink(changeNum, parent.patch_set_number)} of change
${this.renderChangeLink(changeNum)}.
`;
} else {
return html`
Patchset ${ps} is based on commit
${this.renderCommitLink(parent.commit_id, false)} in the target branch
(${branchName(parent.branch_name)}).
`;
}
}
private renderWarning(icon: string, message: HTMLTemplateResult) {
const isWarning = icon === 'warning';
return html`
<div class="messageContainer ${icon}">
<div class="icon">
<gr-icon icon=${icon}></gr-icon>
</div>
<div class="text">
<p>
${message}${when(
isWarning,
() => html`
<br />
The diff below may not be meaningful and may <br />
even be hiding relevant changes.
<a
href=${getDocUrl(
this.docsBaseUrl,
'user-review-ui.html#hazardous-rebases'
)}
>Learn more</a
>
`
)}
</p>
${when(
isWarning,
() => html`
<p>
<gr-button
link
@click=${() => (this.showDetails = !this.showDetails)}
>${this.showDetails ? 'Hide' : 'Show'} details</gr-button
>
</p>
`
)}
</div>
</div>
`;
}
private renderDetails() {
if (!this.showDetails) return;
if (!this.baseRevision || !this.revision) return;
const parentLeft = this.baseRevision.parents_data?.[0];
const parentRight = this.revision.parents_data?.[0];
if (!parentRight || !parentLeft) return;
return html`
<div class="container">
<div class="sections">
${this.renderSection(
this.baseRevision,
parentLeft,
parentLeft.change_number === parentRight.change_number
)}
${this.renderSection(
this.revision,
parentRight,
parentLeft.change_number === parentRight.change_number
)}
</div>
</div>
`;
}
private renderCommitLink(commit?: CommitId, showCopyButton = true) {
if (!commit) return;
return html`<gr-commit-info
.commitInfo=${{commit}}
.showCopyButton=${showCopyButton}
></gr-commit-info>`;
}
private renderChangeLink(changeNum: NumericChangeId) {
return html`
<a href=${createChangeUrl({changeNum, repo: this.repo!})}>${changeNum}</a>
`;
}
private renderPatchsetLink(
changeNum: NumericChangeId,
patchNum?: PatchSetNumber
) {
if (!patchNum) return;
return html`
<a
href=${createChangeUrl({
changeNum,
repo: this.repo!,
patchNum,
})}
>${patchNum}</a
>
`;
}
private renderSection(
revision: RevisionInfo,
parent: ParentInfo,
sameChange: boolean
) {
const ps = revision._number;
const commit = parent.commit_id;
const branch = branchName(parent.branch_name);
const isMerged = parent.is_merged_in_target_branch;
const changeNum = parent.change_number as NumericChangeId;
const changePs = parent.patch_set_number;
createChangeUrl({changeNum, repo: this.repo!});
return html`
<div class="section">
<h4 class="heading-4">Patchset ${ps}</h4>
<div>Target branch: ${branch}</div>
<div>Base commit: ${this.renderCommitLink(commit)}</div>
${when(
!changeNum && !isMerged,
() => html`
<div>
<gr-icon icon="warning"></gr-icon>
<span
>Warning: The base commit is not known (aka reachable) in the
target branch.</span
>
</div>
`
)}
${when(
changeNum && (sameChange || !isMerged),
() => html`
<div>
Base change: ${this.renderChangeLink(changeNum)}, patchset
${this.renderPatchsetLink(changeNum, changePs)}
${when(isMerged, () => html`(MERGED)`)}
</div>
`
)}
</div>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'gr-revision-parents': GrRevisionParents;
}
}