forked from refined-github/refined-github
-
Notifications
You must be signed in to change notification settings - Fork 1
/
restore-file.tsx
143 lines (123 loc) · 4.4 KB
/
restore-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
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, getPRRepositoryInfo} 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 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…';
pathname = `/${getPRRepositoryInfo().url!}/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.
// This code won’t be reached if `highlight-deleted-and-added-files-in-diffs` works.
showError(menuItem, 'Nothing to restore. Delete file instead');
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: unknown) {
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;
}
if (editFile.closest('.file-header')!.querySelector('[aria-label="File added"]')) {
// The file is new. "Restoring" it means deleting it, which is already possible.
// Depends on `highlight-deleted-and-added-files-in-diffs`.
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(__filebasename, {
include: [
pageDetect.isPRFiles,
pageDetect.isPRCommit
],
init
});