-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
restore-file.tsx
150 lines (129 loc) · 3.99 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
144
145
146
147
148
149
150
import React from 'dom-chef';
import delegate, {DelegateEvent} from 'delegate-it';
import * as pageDetect from 'github-url-detection';
import features from '../feature-manager.js';
import api from '../github-helpers/api.js';
import showToast from '../github-helpers/toast.js';
import {getBranches} from '../github-helpers/pr-branches.js';
import getPrInfo from '../github-helpers/get-pr-info.js';
import observe from '../helpers/selector-observer.js';
async function getMergeBaseReference(): Promise<string> {
const {base, head} = getBranches();
// This v3 response is relatively large, but it doesn't seem to be available on v4
const response = await api.v3(`compare/${base.relative}...${head.relative}`);
return response.merge_base_commit.sha; // #4679
}
async function getHeadReference(): Promise<string> {
const {base} = getBranches();
const {headRefOid} = await getPrInfo(base.relative);
return headRefOid;
}
async function getFile(filePath: string): Promise<{isTruncated: boolean; text: string} | undefined> {
const {repository} = await api.v4(`
query getFile($owner: String!, $name: String!, $file: String!) {
repository(owner: $owner, name: $name) {
file: object(expression: $file) {
... on Blob {
isTruncated
text
}
}
}
}
`, {
variables: {
file: `${await getMergeBaseReference()}:${filePath}`,
},
});
return repository.file;
}
async function discardChanges(progress: (message: string) => void, originalFileName: string, newFileName: string): Promise<void> {
const [headReference, file] = await Promise.all([
getHeadReference(),
getFile(originalFileName),
]);
if (file?.isTruncated) {
throw new Error('File too big, you’ll have to use git');
}
const isNewFile = !file;
const isRenamed = originalFileName !== newFileName;
const contents = file ? btoa(unescape(encodeURIComponent(file.text))) : '';
const deleteNewFile = {deletions: [{path: newFileName}]};
const restoreOldFile = {additions: [{path: originalFileName, contents}]};
const fileChanges = isRenamed
? {...restoreOldFile, ...deleteNewFile} // Renamed, maybe also changed
: isNewFile
? deleteNewFile // New
: restoreOldFile; // Changes
const {nameWithOwner, branch: prBranch} = getBranches().head;
progress('Committing…');
await api.v4(`
mutation discardChanges ($input: CreateCommitOnBranchInput!) {
createCommitOnBranch(input: $input) {
commit {
oid
}
}
}
`, {
variables: {
input: {
branch: {
repositoryNameWithOwner: nameWithOwner,
branchName: prBranch,
},
expectedHeadOid: headReference,
fileChanges,
message: {
headline: `Discard changes to ${originalFileName}`,
},
},
},
});
}
async function handleClick(event: DelegateEvent<MouseEvent, HTMLButtonElement>): Promise<void> {
const menuItem = event.delegateTarget;
try {
const [originalFileName, newFileName = originalFileName] = menuItem
.closest('[data-path]')!
.querySelector('.Link--primary')!
.textContent!
.split(' → ');
await showToast(async progress => discardChanges(progress!, originalFileName, newFileName), {
message: 'Loading info…',
doneMessage: 'Changes discarded',
});
// Hide file from view
menuItem.closest('.file')!.remove();
} catch (error) {
features.log.error(import.meta.url, error);
}
}
function add(editFile: HTMLAnchorElement): void {
editFile.after(
<button
className="pl-5 dropdown-item btn-link rgh-restore-file"
role="menuitem"
type="button"
>
Discard changes
</button>,
);
}
function init(signal: AbortSignal): void {
observe('.js-file-header-dropdown a[aria-label^="Change this"]', add, {signal});
// `capture: true` required to be fired before GitHub's handlers
delegate('.rgh-restore-file', 'click', handleClick, {capture: true, signal});
}
void features.add(import.meta.url, {
include: [
pageDetect.isPRFiles,
pageDetect.isPRCommit,
],
init,
});
/*
Test URLs:
https://github.com/refined-github/sandbox/pull/16/files
https://github.com/refined-github/sandbox/pull/29/files
*/