Skip to content

Commit 0171295

Browse files
author
Eric Amodio
committed
Adds file create, rename, & delete support
1 parent 28ea0f0 commit 0171295

8 files changed

Lines changed: 447 additions & 2866 deletions

File tree

extensions/github-browser/package.json

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -147,14 +147,13 @@
147147
"vscode:prepublish": "npm run compile"
148148
},
149149
"dependencies": {
150-
"@octokit/graphql": "4.5.0",
151-
"@octokit/rest": "17.11.0",
150+
"@octokit/graphql": "4.5.1",
151+
"@octokit/rest": "18.0.0",
152152
"fuzzysort": "1.1.4",
153-
"node-fetch": "2.6.0"
153+
"node-fetch": "2.6.0",
154+
"vscode-nls": "4.1.2"
154155
},
155156
"devDependencies": {
156-
"@types/node-fetch": "2.5.7",
157-
"webpack": "4.43.0",
158-
"webpack-cli": "3.3.11"
157+
"@types/node-fetch": "2.5.7"
159158
}
160159
}

extensions/github-browser/src/extension.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ export function activate(context: ExtensionContext) {
4646
// });
4747
}
4848

49+
export function getRelativePath(rootUri: Uri, uri: Uri) {
50+
return uri.fsPath.substr(rootUri.fsPath.length + 1);
51+
}
52+
4953
export function getRootUri(uri: Uri) {
5054
return workspace.getWorkspaceFolder(uri)?.uri;
5155
}

extensions/github-browser/src/fs.ts

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
Uri,
2727
workspace,
2828
} from 'vscode';
29-
import { IChangeStore, ContextStore } from './stores';
29+
import { ContextStore, IWritableChangeStore } from './stores';
3030
import { GitHubApiContext } from './github/api';
3131

3232
const emptyDisposable = { dispose: () => { /* noop */ } };
@@ -44,7 +44,7 @@ export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSe
4444
readonly scheme: string,
4545
private readonly originalScheme: string,
4646
contextStore: ContextStore<GitHubApiContext>,
47-
private readonly changeStore: IChangeStore,
47+
private readonly changeStore: IWritableChangeStore,
4848
private readonly fs: FileSystemProvider & FileSearchProvider & TextSearchProvider
4949
) {
5050
// TODO@eamodio listen for workspace folder changes
@@ -100,21 +100,19 @@ export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSe
100100
return stat;
101101
}
102102

103-
if (uri.path === '' || uri.path.lastIndexOf('/') === 0) {
104-
return { type: FileType.Directory, size: 0, ctime: 0, mtime: 0 };
105-
}
106-
107103
stat = await this.fs.stat(this.getOriginalResource(uri));
108104
return stat;
109105
}
110106

111107
async readDirectory(uri: Uri): Promise<[string, FileType][]> {
112-
const entries = await this.fs.readDirectory(this.getOriginalResource(uri));
108+
let entries = await this.fs.readDirectory(this.getOriginalResource(uri));
109+
entries = this.changeStore.updateDirectoryEntries(uri, entries);
113110
return entries;
114111
}
115112

116113
createDirectory(_uri: Uri): void | Thenable<void> {
117-
throw FileSystemError.NoPermissions;
114+
// TODO@eamodio only support files for now
115+
throw FileSystemError.NoPermissions();
118116
}
119117

120118
async readFile(uri: Uri): Promise<Uint8Array> {
@@ -127,20 +125,60 @@ export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSe
127125
return data;
128126
}
129127

130-
async writeFile(uri: Uri, content: Uint8Array, _options: { create: boolean, overwrite: boolean }): Promise<void> {
131-
await this.changeStore.recordFileChange(uri, content, () => this.fs.readFile(this.getOriginalResource(uri)));
128+
async writeFile(uri: Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): Promise<void> {
129+
let stat;
130+
try {
131+
stat = await this.stat(uri);
132+
if (!options.overwrite) {
133+
throw FileSystemError.FileExists();
134+
}
135+
} catch (ex) {
136+
if (ex instanceof FileSystemError && ex.code === 'FileNotFound') {
137+
if (!options.create) {
138+
throw FileSystemError.FileNotFound();
139+
}
140+
} else {
141+
throw ex;
142+
}
143+
}
144+
145+
if (stat === undefined) {
146+
await this.changeStore.onFileCreated(uri, content);
147+
} else {
148+
await this.changeStore.onFileChanged(uri, content, () => this.fs.readFile(this.getOriginalResource(uri)));
149+
}
132150
}
133151

134-
delete(_uri: Uri, _options: { recursive: boolean }): void | Thenable<void> {
135-
throw FileSystemError.NoPermissions;
152+
async delete(uri: Uri, _options: { recursive: boolean }): Promise<void> {
153+
const stat = await this.stat(uri);
154+
if (stat.type !== FileType.File) {
155+
throw FileSystemError.NoPermissions();
156+
}
157+
158+
await this.changeStore.onFileDeleted(uri);
136159
}
137160

138-
rename(_oldUri: Uri, _newUri: Uri, _options: { overwrite: boolean }): void | Thenable<void> {
139-
throw FileSystemError.NoPermissions;
161+
async rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }): Promise<void> {
162+
const stat = await this.stat(oldUri);
163+
// TODO@eamodio only support files for now
164+
if (stat.type !== FileType.File) {
165+
throw FileSystemError.NoPermissions();
166+
}
167+
168+
const content = await this.readFile(oldUri);
169+
await this.writeFile(newUri, content, { create: true, overwrite: options.overwrite });
170+
await this.delete(oldUri, { recursive: false });
140171
}
141172

142-
copy(_source: Uri, _destination: Uri, _options: { overwrite: boolean }): void | Thenable<void> {
143-
throw FileSystemError.NoPermissions;
173+
async copy(source: Uri, destination: Uri, options: { overwrite: boolean }): Promise<void> {
174+
const stat = await this.stat(source);
175+
// TODO@eamodio only support files for now
176+
if (stat.type !== FileType.File) {
177+
throw FileSystemError.NoPermissions();
178+
}
179+
180+
const content = await this.readFile(source);
181+
await this.writeFile(destination, content, { create: true, overwrite: options.overwrite });
144182
}
145183

146184
//#endregion

extensions/github-browser/src/github/api.ts

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,30 @@ export interface GitHubApiContext {
1717
timestamp: number;
1818
}
1919

20-
function getRootUri(uri: Uri) {
20+
interface CreateCommitOperation {
21+
type: 'created';
22+
path: string;
23+
content: string
24+
}
25+
26+
interface ChangeCommitOperation {
27+
type: 'changed';
28+
path: string;
29+
content: string
30+
}
31+
32+
interface DeleteCommitOperation {
33+
type: 'deleted';
34+
path: string;
35+
content: undefined
36+
}
37+
38+
export type CommitOperation = CreateCommitOperation | ChangeCommitOperation | DeleteCommitOperation;
39+
40+
type ArrayElement<T extends Array<unknown>> = T extends (infer U)[] ? U : never;
41+
type GitCreateTreeParamsTree = ArrayElement<NonNullable<Parameters<Octokit['git']['createTree']>[0]>['tree']>;
42+
43+
function getGitHubRootUri(uri: Uri) {
2144
const rootIndex = uri.path.indexOf('/', uri.path.indexOf('/', 1) + 1);
2245
return uri.with({
2346
path: uri.path.substring(0, rootIndex === -1 ? undefined : rootIndex),
@@ -86,7 +109,7 @@ export class GitHubApi implements Disposable {
86109
return new this._octokit(options);
87110
}
88111

89-
async commit(rootUri: Uri, message: string, files: { path: string; content: string }[]): Promise<string | undefined> {
112+
async commit(rootUri: Uri, message: string, operations: CommitOperation[]): Promise<string | undefined> {
90113
let { owner, repo, ref } = fromGitHubUri(rootUri);
91114

92115
try {
@@ -102,34 +125,72 @@ export class GitHubApi implements Disposable {
102125
throw new Error('Cannot commit — invalid context');
103126
}
104127

128+
const hasDeletes = operations.some(op => op.type === 'deleted');
129+
105130
const github = await this.octokit();
106131
const treeResp = await github.git.getTree({
107132
owner: owner,
108133
repo: repo,
109-
tree_sha: context.sha
134+
tree_sha: context.sha,
135+
recursive: hasDeletes ? 'true' : undefined,
110136
});
111137

112-
const updatedTreeItems: {
113-
path: string;
114-
mode?: '100644' | '100755' | '040000' | '160000' | '120000',
115-
type?: 'blob' | 'tree' | 'commit',
116-
sha?: string | undefined;
117-
content: string;
118-
}[] = [];
119-
120-
for (const file of files) {
121-
for (const { path, mode, type } of treeResp.data.tree) {
122-
if (path === file.path) {
123-
updatedTreeItems.push({ path: path, mode: mode as any, type: type as any, content: file.content });
138+
// 0100000000000000 (040000): Directory
139+
// 1000000110100100 (100644): Regular non-executable file
140+
// 1000000110110100 (100664): Regular non-executable group-writeable file
141+
// 1000000111101101 (100755): Regular executable file
142+
// 1010000000000000 (120000): Symbolic link
143+
// 1110000000000000 (160000): Gitlink
144+
let updatedTree: GitCreateTreeParamsTree[];
145+
146+
if (hasDeletes) {
147+
updatedTree = treeResp.data.tree as GitCreateTreeParamsTree[];
148+
149+
for (const operation of operations) {
150+
switch (operation.type) {
151+
case 'created':
152+
updatedTree.push({ path: operation.path, mode: '100644', type: 'blob', content: operation.content });
153+
break;
154+
155+
case 'changed':
156+
const item = updatedTree.find(item => item.path === operation.path);
157+
if (item !== undefined) {
158+
updatedTree.push({ ...item, content: operation.content });
159+
}
160+
break;
161+
162+
case 'deleted':
163+
const index = updatedTree.findIndex(item => item.path === operation.path);
164+
if (index !== -1) {
165+
updatedTree.splice(index, 1);
166+
}
167+
break;
168+
}
169+
}
170+
} else {
171+
updatedTree = [];
172+
173+
for (const operation of operations) {
174+
switch (operation.type) {
175+
case 'created':
176+
updatedTree.push({ path: operation.path, mode: '100644', type: 'blob', content: operation.content });
177+
break;
178+
179+
case 'changed':
180+
const item = treeResp.data.tree.find(item => item.path === operation.path) as GitCreateTreeParamsTree;
181+
if (item !== undefined) {
182+
updatedTree.push({ ...item, content: operation.content });
183+
}
184+
break;
124185
}
125186
}
126187
}
127188

128189
const updatedTreeResp = await github.git.createTree({
129190
owner: owner,
130191
repo: repo,
131-
base_tree: treeResp.data.sha,
132-
tree: updatedTreeItems
192+
base_tree: hasDeletes ? undefined : treeResp.data.sha,
193+
tree: updatedTree
133194
});
134195

135196
const resp = await github.git.createCommit({
@@ -354,7 +415,7 @@ export class GitHubApi implements Disposable {
354415

355416
private readonly pendingContextRequests = new Map<string, Promise<GitHubApiContext>>();
356417
async getContext(uri: Uri): Promise<GitHubApiContext> {
357-
const rootUri = getRootUri(uri);
418+
const rootUri = getGitHubRootUri(uri);
358419

359420
let pending = this.pendingContextRequests.get(rootUri.toString());
360421
if (pending === undefined) {

extensions/github-browser/src/github/fs.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,8 @@ export class GitHubFS implements FileSystemProvider, FileSearchProvider, TextSea
8989
}
9090

9191
async stat(uri: Uri): Promise<FileStat> {
92-
const context = await this.github.getContext(uri);
93-
9492
if (uri.path === '' || uri.path.lastIndexOf('/') === 0) {
93+
const context = await this.github.getContext(uri);
9594
return { type: FileType.Directory, size: 0, ctime: 0, mtime: context?.timestamp };
9695
}
9796

@@ -107,9 +106,15 @@ export class GitHubFS implements FileSystemProvider, FileSearchProvider, TextSea
107106
this.getCache(uri),
108107
);
109108

109+
if (data === undefined) {
110+
throw FileSystemError.FileNotFound();
111+
}
112+
113+
const context = await this.github.getContext(uri);
114+
110115
return {
111-
type: typenameToFileType(data?.__typename),
112-
size: data?.byteSize ?? 0,
116+
type: typenameToFileType(data.__typename),
117+
size: data.byteSize ?? 0,
113118
ctime: 0,
114119
mtime: context?.timestamp,
115120
};
@@ -136,7 +141,7 @@ export class GitHubFS implements FileSystemProvider, FileSearchProvider, TextSea
136141
}
137142

138143
createDirectory(_uri: Uri): void | Thenable<void> {
139-
throw FileSystemError.NoPermissions;
144+
throw FileSystemError.NoPermissions();
140145
}
141146

142147
async readFile(uri: Uri): Promise<Uint8Array> {
@@ -169,19 +174,19 @@ export class GitHubFS implements FileSystemProvider, FileSearchProvider, TextSea
169174
}
170175

171176
async writeFile(_uri: Uri, _content: Uint8Array, _options: { create: boolean, overwrite: boolean }): Promise<void> {
172-
throw FileSystemError.NoPermissions;
177+
throw FileSystemError.NoPermissions();
173178
}
174179

175180
delete(_uri: Uri, _options: { recursive: boolean }): void | Thenable<void> {
176-
throw FileSystemError.NoPermissions;
181+
throw FileSystemError.NoPermissions();
177182
}
178183

179184
rename(_oldUri: Uri, _newUri: Uri, _options: { overwrite: boolean }): void | Thenable<void> {
180-
throw FileSystemError.NoPermissions;
185+
throw FileSystemError.NoPermissions();
181186
}
182187

183188
copy(_source: Uri, _destination: Uri, _options: { overwrite: boolean }): void | Thenable<void> {
184-
throw FileSystemError.NoPermissions;
189+
throw FileSystemError.NoPermissions();
185190
}
186191

187192
//#endregion

0 commit comments

Comments
 (0)