-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
revert-file.tsx
145 lines (127 loc) · 4.62 KB
/
revert-file.tsx
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import React from 'dom-chef';
import select from 'select-dom';
import onetime from 'onetime';
import delegate, {DelegateEvent} from 'delegate-it';
import * as api from '../libs/api';
import features from '../libs/features';
import fetchDom from '../libs/fetch-dom';
import postForm from '../libs/post-form';
import {getDiscussionNumber, getRepoGQL, getRepoURL, getCurrentBranch} from '../libs/utils';
function showError(menuItem: HTMLButtonElement, error: string): void {
menuItem.disabled = true;
menuItem.style.background = 'none'; // Disables hover background color
menuItem.textContent = error;
}
/**
Get the current base commit of this PR. It should change after rebases and merges in this PR.
This value is not consistently available on the page (appears in `/files` but not when only 1 commit is selected)
*/
const getBaseRef = onetime(async (): Promise<string> => {
const {repository} = await api.v4(`
repository(${getRepoGQL()}) {
pullRequest(number: ${getDiscussionNumber()}) {
baseRefOid
}
}
`);
return repository.pullRequest.baseRefOid;
});
async function getFile(menuItem: Element): Promise<{isTruncated: boolean; text: string}> {
const filePath = menuItem.closest<HTMLElement>('[data-path]')!.dataset.path!;
const {repository} = await api.v4(`
repository(${getRepoGQL()}) {
file: object(expression: "${await getBaseRef()}:${filePath}") {
... on Blob {
isTruncated
text
}
}
}
`);
return repository.file;
}
async function deleteFile(menuItem: Element): Promise<void> {
menuItem.textContent = 'Deleting…';
const deleteFileLink = select<HTMLAnchorElement>('a[aria-label^="Delete this"]', menuItem.parentElement!)!;
const form = await fetchDom<HTMLFormElement>(deleteFileLink.href, '#new_blob');
await postForm(form);
}
async function commitFileContent(menuItem: Element, content: string): Promise<void> {
let {pathname} = (menuItem.previousElementSibling as HTMLAnchorElement);
// Check if file was deleted by PR
if (menuItem.closest('[data-file-deleted="true"]')) {
menuItem.textContent = 'Undeleting…';
const filePath = pathname.split('/')[5]; // The URL was something like /$user/$repo/blob/$startingCommit/$path
pathname = `/${getRepoURL()}/new/${getCurrentBranch()}?filename=` + filePath;
} else {
menuItem.textContent = 'Committing…';
}
// This is either an `edit` or `create` form
const form = await fetchDom<HTMLFormElement>(pathname, '.js-blob-form');
form.elements.value.value = content; // Revert content (`value` is the name of the file content field)
form.elements.message.value = (form.elements.message as HTMLInputElement).placeholder
.replace(/^Update/, 'Revert')
.replace(/^Create/, 'Restore');
await postForm(form);
}
async function handleRevertFileClick(event: React.MouseEvent<HTMLButtonElement>): Promise<void> {
const menuItem = event.currentTarget;
// Allow only one click
// TODO: change JSX event types to be plain browser events
menuItem.removeEventListener('click', handleRevertFileClick as any);
menuItem.textContent = 'Reverting…';
event.preventDefault();
event.stopPropagation();
try {
const file = await getFile(menuItem);
if (!file) {
// The file was created by this PR. Revert === Delete.
// If there was a way to tell if a file was created by the PR, we could skip `getFile`
// TODO: find this info on the page ("was this file created by this PR?")
await deleteFile(menuItem);
return;
}
if (file.isTruncated) {
showError(menuItem, 'Revert failed: File too big');
return;
}
await commitFileContent(menuItem, file.text);
// Hide file from view
menuItem.closest('.file')!.remove();
} catch (error) {
showError(menuItem, 'Revert failed. See console for details');
throw error;
}
}
function handleMenuOpening(event: DelegateEvent): void {
const dropdown = event.delegateTarget.nextElementSibling!;
const editFile = select<HTMLAnchorElement>('[aria-label^="Change this"]', dropdown);
if (!editFile || select.exists('.rgh-revert-file', dropdown)) {
return;
}
editFile.after(
<button
className="pl-5 dropdown-item btn-link rgh-revert-file"
style={{whiteSpace: 'pre-wrap'}}
role="menuitem"
type="button"
onClick={handleRevertFileClick}
>
Revert changes
</button>
);
}
function init(): void {
delegate('#files', '.js-file-header-dropdown > summary', 'click', handleMenuOpening);
}
features.add({
id: __featureName__,
description: 'Adds button to revert all the changes to a file in a PR.',
screenshot: 'https://user-images.githubusercontent.com/1402241/62826118-73b7bb00-bbe0-11e9-9449-2dd64c469bb9.gif',
include: [
features.isPRFiles,
features.isPRCommit
],
load: features.onAjaxedPages,
init
});