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
146
147
148
149
150
|
import React from 'dom-chef';
import select from 'select-dom';
import onetime from 'onetime';
import delegate from 'delegate-it';
import * as pageDetect from 'github-url-detection';
import features from '.';
import * as api from '../github-helpers/api';
import fetchDom from '../helpers/fetch-dom';
import postForm from '../helpers/post-form';
import {getConversationNumber, getRepoGQL, getCurrentBranch} from '../github-helpers';
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 getBaseReference = onetime(async (): Promise<string> => {
const {repository} = await api.v4(`
repository(${getRepoGQL()}) {
pullRequest(number: ${getConversationNumber()!}) {
baseRefOid
}
}
`);
return repository.pullRequest.baseRefOid;
});
async function getFile(filePath: string): Promise<{isTruncated: boolean; text: string} | null> {
const {repository} = await api.v4(`
repository(${getRepoGQL()}) {
file: object(expression: "${await getBaseReference()}:${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, filePath: 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 [, user, repository] = select<HTMLAnchorElement>('.commit-ref.head-ref a')!.pathname.split('/', 3);
pathname = `/${user}/${repository}/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; // Restore content (`value` is the name of the file content field)
form.elements.message.value = (form.elements.message as HTMLInputElement).placeholder
.replace(/^Create|^Update/, 'Restore');
await postForm(form);
}
const filesRestored = new WeakSet<HTMLButtonElement>();
async function handleRestoreFileClick(event: delegate.Event<MouseEvent, HTMLButtonElement>): Promise<void> {
const menuItem = event.delegateTarget;
// Only allow one click
if (filesRestored.has(menuItem)) {
return;
}
filesRestored.add(menuItem);
menuItem.textContent = 'Restoring…';
event.preventDefault();
event.stopPropagation();
try {
const filePath = menuItem.closest<HTMLDivElement>('[data-path]')!.dataset.path!;
const file = await getFile(filePath);
if (!file) {
// The file was created by this PR. Restore === Delete.
// If there was a way to tell if a file was created by the PR, we could skip `getFile`
await deleteFile(menuItem);
return;
}
if (file.isTruncated) {
showError(menuItem, 'Restore failed: File too big');
return;
}
await commitFileContent(menuItem, file.text, filePath);
// Hide file from view
menuItem.closest('.file')!.remove();
} catch (error) {
showError(menuItem, 'Restore failed. See console for details');
features.error(__filebasename, error);
}
}
function handleMenuOpening({delegateTarget: dropdown}: delegate.Event): void {
const editFile = select<HTMLAnchorElement>('[aria-label^="Change this"]', dropdown);
if (!editFile || select.exists('.rgh-restore-file', dropdown)) {
return;
}
editFile.after(
<button
className="pl-5 dropdown-item btn-link rgh-restore-file"
style={{whiteSpace: 'pre-wrap'}}
role="menuitem"
type="button"
>
Restore file
</button>
);
}
function init(): void {
// `useCapture` required to be fired before GitHub's handlers
delegate(document, '.file-header .js-file-header-dropdown', 'toggle', handleMenuOpening, true);
delegate(document, '.rgh-restore-file', 'click', handleRestoreFileClick, true);
}
void features.add({
id: __filebasename,
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: [
pageDetect.isPRFiles,
pageDetect.isPRCommit
],
init
});
|