summaryrefslogtreecommitdiff
path: root/source/features/deep-reblame.tsx
blob: 78fb90e7f9ff2b05578454cdd95d0bb001bcf40d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import './deep-reblame.css';
import mem from 'mem';
import React from 'dom-chef';
import select from 'select-dom';
import {VersionsIcon} from '@primer/octicons-react';
import * as pageDetect from 'github-url-detection';
import delegate, {DelegateEvent} from 'delegate-it';

import features from '../feature-manager.js';
import api from '../github-helpers/api.js';
import GitHubFileURL from '../github-helpers/github-file-url.js';
import showToast from '../github-helpers/toast.js';
import looseParseInt from '../helpers/loose-parse-int.js';
import observe from '../helpers/selector-observer.js';
import GetPullRequestBlameCommit from './deep-reblame.gql';

const getPullRequestBlameCommit = mem(async (commit: string, prNumbers: number[], currentFilename: string): Promise<string> => {
	const {repository} = await api.v4(GetPullRequestBlameCommit, {
		variables: {
			commit,
			file: commit + ':' + currentFilename,
		},
	});

	const associatedPR = repository.object.associatedPullRequests.nodes[0];

	if (!associatedPR || !prNumbers.includes(associatedPR.number) || associatedPR.mergeCommit.oid !== commit) {
		throw new Error('The PR linked in the title didn’t create this commit');
	}

	if (!repository.file) {
		throw new Error('The file was renamed and Refined GitHub can’t find it');
	}

	return associatedPR.commits.nodes[0].commit.oid;
});

async function redirectToBlameCommit(event: DelegateEvent<MouseEvent, HTMLAnchorElement | HTMLButtonElement>): Promise<void> {
	const blameElement = event.delegateTarget;
	if (blameElement instanceof HTMLAnchorElement && !event.altKey) {
		return; // Unmodified click on regular link: let it proceed
	}

	event.preventDefault();
	blameElement.blur(); // Hide tooltip after click, it’s shown on :focus

	const blameHunk = blameElement.closest('.blame-hunk')!;
	const prNumbers = select.all('.issue-link', blameHunk).map(pr => looseParseInt(pr));
	const prCommit = select('a.message', blameHunk)!.pathname.split('/').pop()!;
	const blameUrl = new GitHubFileURL(location.href);

	await showToast(async () => {
		blameUrl.branch = await getPullRequestBlameCommit(prCommit, prNumbers, blameUrl.filePath);
		blameUrl.hash = 'L' + select('.js-line-number', blameHunk)!.textContent;
		location.href = blameUrl.href;
	}, {
		message: 'Fetching pull request',
		doneMessage: 'Redirecting',
	});
}

function addButton(pullRequest: HTMLElement): void {
	const hunk = pullRequest.closest('.blame-hunk')!;

	const reblameLink = select('.reblame-link', hunk);
	if (reblameLink) {
		reblameLink.setAttribute('aria-label', 'View blame prior to this change. Hold `Alt` to extract commits from this PR first');
		reblameLink.classList.add('rgh-deep-reblame');
	} else {
		select('.blob-reblame', hunk)!.append(
			<button
				type="button"
				aria-label="View blame prior to this change (extracts commits from this PR first)"
				className="reblame-link btn-link no-underline tooltipped tooltipped-e d-inline-block pr-1 rgh-deep-reblame"
			>
				<VersionsIcon/>
			</button>,
		);
	}
}

function init(signal: AbortSignal): void {
	delegate('.rgh-deep-reblame', 'click', redirectToBlameCommit, {signal});
	observe('[data-hovercard-type="pull_request"]', addButton, {signal});
}

void features.add(import.meta.url, {
	include: [
		pageDetect.isBlame,
	],
	init,
});

/*

Test URLs:

https://github.com/refined-github/refined-github/blame/main/source/refined-github.ts

*/