blob: 6b010cf87b63c1ccf1af1f083cbd2c18967424f1 [file]
/**
* @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>`;
}
}