| /** |
| * @module modules/commit-range-sk |
| * @description <h2><code>commit-range-sk</code></h2> |
| * |
| * Displays a link that describes a range of commits in a repo. This element |
| * uses the global `window.perf.commit_range_url`, which can be set on Perf via |
| * the command line. |
| */ |
| import { html, LitElement, PropertyValues } from 'lit'; |
| import { customElement, property, state } from 'lit/decorators.js'; |
| import { lookupCids } from '../cid/cid'; |
| import { MISSING_DATA_SENTINEL } from '../const/const'; |
| import { ColumnHeader, CommitNumber } from '../json'; |
| import '../window/window'; |
| import { TrimHash } from '../common/commit'; |
| |
| // Converts CommitNumbers to Git hashes. |
| type commitNumberToHashes = (commitNumbers: CommitNumber[]) => Promise<string[]>; |
| |
| /** The default implementation for commitNumberToHashes run the commit numbers |
| * through cid lookup to get the hashes by making a request to the server. |
| */ |
| const defaultcommitNumberToHashes = async (cids: CommitNumber[]): Promise<string[]> => { |
| const json = await lookupCids(cids); |
| return [json.commitSlice![0].hash, json.commitSlice![1].hash]; |
| }; |
| |
| @customElement('commit-range-sk') |
| export class CommitRangeSk extends LitElement { |
| @property({ attribute: false }) |
| trace: number[] = []; |
| |
| @property({ type: Number, attribute: 'commit-index' }) |
| commitIndex: number = -1; |
| |
| @property({ attribute: false }) |
| header: (ColumnHeader | null)[] | null = null; |
| |
| @property({ attribute: false }) |
| hashes: string[] | null = null; |
| |
| @state() |
| private _text: string = ''; |
| |
| @state() |
| private _url: string = ''; |
| |
| private _autoload: boolean = true; |
| |
| private _commitIds: [CommitNumber, CommitNumber] | null = null; |
| |
| private currentRequestId: number = 0; |
| |
| private hashCache: Map<string, string[]> = new Map(); |
| |
| // commitNumberToHashes can be replaced to make testing easier. |
| private commitNumberToHashes: commitNumberToHashes = async ( |
| cids: CommitNumber[] |
| ): Promise<string[]> => { |
| const cacheKey = cids.join(','); |
| if (this.hashCache.has(cacheKey)) { |
| return this.hashCache.get(cacheKey)!; |
| } |
| |
| let hashes: string[] = []; |
| if (this._autoload) { |
| hashes = await defaultcommitNumberToHashes(cids); |
| this.hashCache.set(cacheKey, hashes); |
| return hashes; |
| } |
| |
| return []; |
| }; |
| |
| createRenderRoot() { |
| return this; |
| } |
| |
| protected willUpdate(changedProperties: PropertyValues): void { |
| if ( |
| changedProperties.has('trace') || |
| changedProperties.has('commitIndex') || |
| changedProperties.has('header') || |
| changedProperties.has('hashes') |
| ) { |
| this.recalcLink(changedProperties); |
| } |
| } |
| |
| reset(): void { |
| this.commitIndex = -1; |
| this.trace = []; |
| this.header = null; |
| this.hashes = null; |
| this._text = ''; |
| this._url = ''; |
| this._commitIds = null; |
| } |
| |
| clear(): void { |
| this._text = ''; |
| this._url = ''; |
| } |
| |
| /** Check start and end commits and determines if delta is more than 1. |
| * If so, it is a range and returns true. |
| * If not, it is a single commit and returns false. |
| * Returns false if no commits are set. |
| * @returns boolean |
| */ |
| isRange(): boolean | null { |
| if (!this._commitIds) { |
| return null; |
| } |
| if (this._commitIds[0] >= this._commitIds[1]) { |
| return false; |
| } |
| |
| if (this._commitIds[0] + 1 === this._commitIds[1]) { |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Sets the range explicitly. |
| * @param start The previous commit (exclusive). |
| * @param end The current commit (inclusive). |
| */ |
| async setRange(start: CommitNumber, end: CommitNumber): Promise<void> { |
| this._commitIds = [start, end]; |
| this.hashes = null; |
| this.updateText(); |
| await this.recalcLink(); |
| } |
| |
| /** |
| * Recalculates the link based on the current state of the object. |
| * If there is not enough information to build the link, it clears the |
| * current link. |
| */ |
| async recalcLink(changedProperties?: PropertyValues): Promise<void> { |
| this.currentRequestId++; |
| const requestId = this.currentRequestId; |
| |
| if (window.perf.commit_range_url === '') { |
| this.clear(); |
| return; |
| } |
| |
| if (this.commitIndex !== -1) { |
| const newCommitIds = this.setCommitIds(this.commitIndex); |
| if (!newCommitIds || newCommitIds.length !== 2) { |
| this.clear(); |
| return; |
| } |
| // If the commit IDs have changed then the hashes are no longer valid. |
| if ( |
| !this._commitIds || |
| this._commitIds[0] !== newCommitIds[0] || |
| this._commitIds[1] !== newCommitIds[1] |
| ) { |
| if (!changedProperties?.has('hashes')) { |
| this.hashes = null; |
| } |
| } |
| this._commitIds = newCommitIds; |
| } else if (!this._commitIds) { |
| // If commitIndex is -1 and no manual range set, clear. |
| this.clear(); |
| return; |
| } |
| |
| try { |
| if (!this._commitIds || this._commitIds.length !== 2) { |
| this.clear(); |
| return; |
| } |
| |
| this.updateText(); |
| // Clear URL while fetching new hashes or if irrelevant |
| this._url = ''; |
| |
| if (!this.hashes) { |
| // Run the commit numbers through cid lookup to get the hashes. |
| const hashes = await this.commitNumberToHashes(this._commitIds); |
| if (requestId !== this.currentRequestId) { |
| return; |
| } |
| this.hashes = hashes; |
| } |
| |
| // If we have the hashes, then we can build the link. |
| if (this.hashes && this.hashes.length > 1) { |
| const url = this._buildUrl(); |
| |
| if (requestId !== this.currentRequestId) { |
| return; |
| } |
| |
| this._url = url; |
| |
| // Ensure element is connected to tooltip before dispatching event. |
| if (this.isConnected) { |
| this.dispatchEvent( |
| new CustomEvent('commit-range-changed', { |
| bubbles: true, // Allows parent elements to catch the event. |
| composed: true, // Allows event to cross shadow DOM boundaries. |
| }) |
| ); |
| } |
| } |
| } catch (error) { |
| console.log(error); |
| this.clear(); |
| } |
| } |
| |
| // Should be as close as possible to perf/go/formatter. |
| private _buildUrl(): string { |
| let url = window.perf.commit_range_url; |
| |
| // Always replace {end} with the second hash. |
| if (url.includes('{end}')) { |
| url = url.replace('{end}', this.hashes![1]); |
| } |
| const isRange = this.isRange(); |
| if (isRange) { |
| // Handle range URLs (Googlesource) |
| if (url.includes('{begin}')) { |
| url = url.replace('{begin}', this.hashes![0]); |
| } else { |
| // We expect a single commit range, but there are gaps in data. |
| // We display hashes instead of commit positions not to confuse users. |
| if (window.perf.show_hash_ranges_in_tooltip ?? false) { |
| this._handleHashRange(); |
| } |
| } |
| } else { |
| // Handle single commit scenarios |
| if (url.includes('+log/{begin}..')) { |
| // Googlesource style: transform to single commit view |
| url = url.replace('+log/{begin}..', '+/'); |
| } else { |
| // Fallback for any other template, remove {begin} if it exists |
| if (url.includes('{begin}')) { |
| url = url.replace('{begin}', ''); |
| } |
| // If GitHub, show short hash instead of commit number. |
| if (url.includes('github')) { |
| this._text = TrimHash(this.hashes![1]); |
| } |
| } |
| } |
| return url; |
| } |
| |
| private async _handleHashRange(): Promise<void> { |
| const oldText = this._text; |
| this._text = 'loading...'; |
| const hash1 = TrimHash(this.hashes![1]); |
| const numberOfCommitsInRangeText = `(${ |
| this._commitIds![1] - this._commitIds![0] |
| } commits in this range)`; |
| // defaultcommitNumberToHashes expects exactly two hashes. |
| // We only care about the first one. For the other one, we grab whatever. |
| // This solution makes this branch compatible with the caching mechanic used |
| // in this.commitNumberToHashes. |
| const hashAfter0UntrimmedList = await this.commitNumberToHashes([ |
| CommitNumber(this._commitIds![0] + 1), |
| CommitNumber(this._commitIds![1]), |
| ]); |
| if (hashAfter0UntrimmedList.length < 1) { |
| console.log('failed to handle fetch commit info for beginning of the range.'); |
| this._text = oldText; |
| return; |
| } |
| const hashAfter0 = TrimHash(hashAfter0UntrimmedList[0]); |
| this._text = `${hashAfter0} - ${hash1} ${numberOfCommitsInRangeText}`; |
| } |
| |
| private updateText(): void { |
| if (!this._commitIds || this._commitIds.length !== 2) { |
| return; |
| } |
| let text = `${this._commitIds[1]}`; |
| // Check if there are no points between start and end. |
| const isRange = this.isRange(); |
| |
| if (isRange) { |
| // Add +1 to the previous commit to only show commits after previous. |
| text = `${this._commitIds[0] + 1} - ${this._commitIds[1]}`; |
| } |
| this._text = text; |
| } |
| |
| set autoload(val: boolean) { |
| this._autoload = val; |
| } |
| |
| setCommitIds(commitIndex: number): [CommitNumber, CommitNumber] | null { |
| if (this.trace.length === 0 || this.header === null) { |
| this.clear(); |
| return null; |
| } |
| // First the previous commit that has data. |
| let prevCommit = commitIndex - 1; |
| |
| while (prevCommit > 0 && this.trace[prevCommit] === MISSING_DATA_SENTINEL) { |
| prevCommit -= 1; |
| } |
| |
| // If we don't find a second commit then we can't present the information. |
| if (prevCommit < 0) { |
| this.clear(); |
| return null; |
| } |
| |
| const startOffset = this.header[prevCommit]?.offset ?? null; |
| const endOffset = this.header[commitIndex]?.offset ?? null; |
| if (startOffset === null || endOffset === null) { |
| this.clear(); |
| return null; |
| } |
| return [startOffset, endOffset]; |
| } |
| |
| render() { |
| if (this._url) { |
| return html`<a href="${this._url}" target="_blank">${this._text}</a>`; |
| } |
| return html`<span style="cursor: default">${this._text}</span>`; |
| } |
| } |