| /** |
| * @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; |
| } |
| } |