summaryrefslogtreecommitdiff
path: root/source/features/show-associated-branch-prs-on-fork.tsx
blob: 062cc3ba8874a042dd4b735f47c25819e0e4772d (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
101
102
103
104
105
106
107
108
109
110
111
112
import React from 'dom-chef';
import cache from 'webext-storage-cache';
import onetime from 'onetime';
import {observe} from 'selector-observer';
import MergeIcon from 'octicon/git-merge.svg';
import * as pageDetect from 'github-url-detection';
import PullRequestIcon from 'octicon/git-pull-request.svg';

import features from '.';
import * as api from '../github-helpers/api';
import {getRepoGQL, getRepo, upperCaseFirst} from '../github-helpers';

interface PullRequest {
	number: number;
	state: string;
	isDraft: boolean;
	url: string;
}

const getPullRequestsAssociatedWithBranch = cache.function(async (): Promise<Record<string, PullRequest>> => {
	const {repository} = await api.v4(`
		repository(${getRepoGQL()}) {
			refs(refPrefix: "refs/heads/", last: 100) {
				nodes {
					name
					associatedPullRequests(last: 1, orderBy: {field: CREATED_AT, direction: DESC}) {
						nodes {
							number
							state
							isDraft
							url
							timelineItems(last: 1, itemTypes: [HEAD_REF_DELETED_EVENT, HEAD_REF_RESTORED_EVENT]) {
								nodes {
									__typename
								}
							}
						}
					}
				}
			}
		}
	`);

	const pullRequests: Record<string, PullRequest> = {};
	for (const {name, associatedPullRequests} of repository.refs.nodes) {
		const [prInfo] = associatedPullRequests.nodes;
		// Check if the ref was deleted, since the result includes pr's that are not in fact related to this branch but rather to the branch name.
		const headRefWasDeleted = prInfo?.timelineItems.nodes[0]?.__typename === 'HeadRefDeletedEvent';
		if (prInfo && !headRefWasDeleted) {
			prInfo.state = prInfo.isDraft && prInfo.state === 'OPEN' ? 'Draft' : upperCaseFirst(prInfo.state);
			pullRequests[name] = prInfo;
		}
	}

	return pullRequests;
}, {
	maxAge: {hours: 1},
	staleWhileRevalidate: {days: 4},
	cacheKey: () => 'associatedBranchPullRequests:' + getRepo()!.nameWithOwner
});

const stateClass: Record<string, string> = {
	Open: '--green',
	Closed: '--red',
	Merged: '--purple',
	Draft: ''
};

async function init(): Promise<void> {
	const associatedPullRequests = await getPullRequestsAssociatedWithBranch();

	observe('.test-compare-link', {
		add(branchCompareLink) {
			const branchName = branchCompareLink.closest('[branch]')!.getAttribute('branch')!;
			const prInfo = associatedPullRequests[branchName];
			if (prInfo) {
				const StateIcon = prInfo.state === 'Merged' ? MergeIcon : PullRequestIcon;

				branchCompareLink.replaceWith(
					<div className="d-inline-block text-right ml-3">
						<a
							data-issue-and-pr-hovercards-enabled
							href={prInfo.url}
							className="muted-link"
							data-hovercard-type="pull_request"
							data-hovercard-url={prInfo.url + '/hovercard'}
						>
							#{prInfo.number}{' '}
						</a>
						<a
							className={`State State${stateClass[prInfo.state]} State--small ml-1 no-underline`}
							title={`Status: ${prInfo.state}`}
							href={prInfo.url}
						>
							<StateIcon width={10} height={14}/> {prInfo.state}
						</a>
					</div>);
			}
		}
	});
}

void features.add(__filebasename, {
	include: [
		pageDetect.isBranches
	],
	exclude: [
		() => !pageDetect.isForkedRepo()
	],
	awaitDomReady: false,
	init: onetime(init)
});