Adds file create, rename, & delete support
This commit is contained in:
parent
28ea0f0aaa
commit
017129571a
|
@ -147,14 +147,13 @@
|
|||
"vscode:prepublish": "npm run compile"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/graphql": "4.5.0",
|
||||
"@octokit/rest": "17.11.0",
|
||||
"@octokit/graphql": "4.5.1",
|
||||
"@octokit/rest": "18.0.0",
|
||||
"fuzzysort": "1.1.4",
|
||||
"node-fetch": "2.6.0"
|
||||
"node-fetch": "2.6.0",
|
||||
"vscode-nls": "4.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-fetch": "2.5.7",
|
||||
"webpack": "4.43.0",
|
||||
"webpack-cli": "3.3.11"
|
||||
"@types/node-fetch": "2.5.7"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,10 @@ export function activate(context: ExtensionContext) {
|
|||
// });
|
||||
}
|
||||
|
||||
export function getRelativePath(rootUri: Uri, uri: Uri) {
|
||||
return uri.fsPath.substr(rootUri.fsPath.length + 1);
|
||||
}
|
||||
|
||||
export function getRootUri(uri: Uri) {
|
||||
return workspace.getWorkspaceFolder(uri)?.uri;
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import {
|
|||
Uri,
|
||||
workspace,
|
||||
} from 'vscode';
|
||||
import { IChangeStore, ContextStore } from './stores';
|
||||
import { ContextStore, IWritableChangeStore } from './stores';
|
||||
import { GitHubApiContext } from './github/api';
|
||||
|
||||
const emptyDisposable = { dispose: () => { /* noop */ } };
|
||||
|
@ -44,7 +44,7 @@ export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSe
|
|||
readonly scheme: string,
|
||||
private readonly originalScheme: string,
|
||||
contextStore: ContextStore<GitHubApiContext>,
|
||||
private readonly changeStore: IChangeStore,
|
||||
private readonly changeStore: IWritableChangeStore,
|
||||
private readonly fs: FileSystemProvider & FileSearchProvider & TextSearchProvider
|
||||
) {
|
||||
// TODO@eamodio listen for workspace folder changes
|
||||
|
@ -100,21 +100,19 @@ export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSe
|
|||
return stat;
|
||||
}
|
||||
|
||||
if (uri.path === '' || uri.path.lastIndexOf('/') === 0) {
|
||||
return { type: FileType.Directory, size: 0, ctime: 0, mtime: 0 };
|
||||
}
|
||||
|
||||
stat = await this.fs.stat(this.getOriginalResource(uri));
|
||||
return stat;
|
||||
}
|
||||
|
||||
async readDirectory(uri: Uri): Promise<[string, FileType][]> {
|
||||
const entries = await this.fs.readDirectory(this.getOriginalResource(uri));
|
||||
let entries = await this.fs.readDirectory(this.getOriginalResource(uri));
|
||||
entries = this.changeStore.updateDirectoryEntries(uri, entries);
|
||||
return entries;
|
||||
}
|
||||
|
||||
createDirectory(_uri: Uri): void | Thenable<void> {
|
||||
throw FileSystemError.NoPermissions;
|
||||
// TODO@eamodio only support files for now
|
||||
throw FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
async readFile(uri: Uri): Promise<Uint8Array> {
|
||||
|
@ -127,20 +125,60 @@ export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSe
|
|||
return data;
|
||||
}
|
||||
|
||||
async writeFile(uri: Uri, content: Uint8Array, _options: { create: boolean, overwrite: boolean }): Promise<void> {
|
||||
await this.changeStore.recordFileChange(uri, content, () => this.fs.readFile(this.getOriginalResource(uri)));
|
||||
async writeFile(uri: Uri, content: Uint8Array, options: { create: boolean, overwrite: boolean }): Promise<void> {
|
||||
let stat;
|
||||
try {
|
||||
stat = await this.stat(uri);
|
||||
if (!options.overwrite) {
|
||||
throw FileSystemError.FileExists();
|
||||
}
|
||||
} catch (ex) {
|
||||
if (ex instanceof FileSystemError && ex.code === 'FileNotFound') {
|
||||
if (!options.create) {
|
||||
throw FileSystemError.FileNotFound();
|
||||
}
|
||||
} else {
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
if (stat === undefined) {
|
||||
await this.changeStore.onFileCreated(uri, content);
|
||||
} else {
|
||||
await this.changeStore.onFileChanged(uri, content, () => this.fs.readFile(this.getOriginalResource(uri)));
|
||||
}
|
||||
}
|
||||
|
||||
delete(_uri: Uri, _options: { recursive: boolean }): void | Thenable<void> {
|
||||
throw FileSystemError.NoPermissions;
|
||||
async delete(uri: Uri, _options: { recursive: boolean }): Promise<void> {
|
||||
const stat = await this.stat(uri);
|
||||
if (stat.type !== FileType.File) {
|
||||
throw FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
await this.changeStore.onFileDeleted(uri);
|
||||
}
|
||||
|
||||
rename(_oldUri: Uri, _newUri: Uri, _options: { overwrite: boolean }): void | Thenable<void> {
|
||||
throw FileSystemError.NoPermissions;
|
||||
async rename(oldUri: Uri, newUri: Uri, options: { overwrite: boolean }): Promise<void> {
|
||||
const stat = await this.stat(oldUri);
|
||||
// TODO@eamodio only support files for now
|
||||
if (stat.type !== FileType.File) {
|
||||
throw FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
const content = await this.readFile(oldUri);
|
||||
await this.writeFile(newUri, content, { create: true, overwrite: options.overwrite });
|
||||
await this.delete(oldUri, { recursive: false });
|
||||
}
|
||||
|
||||
copy(_source: Uri, _destination: Uri, _options: { overwrite: boolean }): void | Thenable<void> {
|
||||
throw FileSystemError.NoPermissions;
|
||||
async copy(source: Uri, destination: Uri, options: { overwrite: boolean }): Promise<void> {
|
||||
const stat = await this.stat(source);
|
||||
// TODO@eamodio only support files for now
|
||||
if (stat.type !== FileType.File) {
|
||||
throw FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
const content = await this.readFile(source);
|
||||
await this.writeFile(destination, content, { create: true, overwrite: options.overwrite });
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
|
|
@ -17,7 +17,30 @@ export interface GitHubApiContext {
|
|||
timestamp: number;
|
||||
}
|
||||
|
||||
function getRootUri(uri: Uri) {
|
||||
interface CreateCommitOperation {
|
||||
type: 'created';
|
||||
path: string;
|
||||
content: string
|
||||
}
|
||||
|
||||
interface ChangeCommitOperation {
|
||||
type: 'changed';
|
||||
path: string;
|
||||
content: string
|
||||
}
|
||||
|
||||
interface DeleteCommitOperation {
|
||||
type: 'deleted';
|
||||
path: string;
|
||||
content: undefined
|
||||
}
|
||||
|
||||
export type CommitOperation = CreateCommitOperation | ChangeCommitOperation | DeleteCommitOperation;
|
||||
|
||||
type ArrayElement<T extends Array<unknown>> = T extends (infer U)[] ? U : never;
|
||||
type GitCreateTreeParamsTree = ArrayElement<NonNullable<Parameters<Octokit['git']['createTree']>[0]>['tree']>;
|
||||
|
||||
function getGitHubRootUri(uri: Uri) {
|
||||
const rootIndex = uri.path.indexOf('/', uri.path.indexOf('/', 1) + 1);
|
||||
return uri.with({
|
||||
path: uri.path.substring(0, rootIndex === -1 ? undefined : rootIndex),
|
||||
|
@ -86,7 +109,7 @@ export class GitHubApi implements Disposable {
|
|||
return new this._octokit(options);
|
||||
}
|
||||
|
||||
async commit(rootUri: Uri, message: string, files: { path: string; content: string }[]): Promise<string | undefined> {
|
||||
async commit(rootUri: Uri, message: string, operations: CommitOperation[]): Promise<string | undefined> {
|
||||
let { owner, repo, ref } = fromGitHubUri(rootUri);
|
||||
|
||||
try {
|
||||
|
@ -102,25 +125,63 @@ export class GitHubApi implements Disposable {
|
|||
throw new Error('Cannot commit — invalid context');
|
||||
}
|
||||
|
||||
const hasDeletes = operations.some(op => op.type === 'deleted');
|
||||
|
||||
const github = await this.octokit();
|
||||
const treeResp = await github.git.getTree({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
tree_sha: context.sha
|
||||
tree_sha: context.sha,
|
||||
recursive: hasDeletes ? 'true' : undefined,
|
||||
});
|
||||
|
||||
const updatedTreeItems: {
|
||||
path: string;
|
||||
mode?: '100644' | '100755' | '040000' | '160000' | '120000',
|
||||
type?: 'blob' | 'tree' | 'commit',
|
||||
sha?: string | undefined;
|
||||
content: string;
|
||||
}[] = [];
|
||||
// 0100000000000000 (040000): Directory
|
||||
// 1000000110100100 (100644): Regular non-executable file
|
||||
// 1000000110110100 (100664): Regular non-executable group-writeable file
|
||||
// 1000000111101101 (100755): Regular executable file
|
||||
// 1010000000000000 (120000): Symbolic link
|
||||
// 1110000000000000 (160000): Gitlink
|
||||
let updatedTree: GitCreateTreeParamsTree[];
|
||||
|
||||
for (const file of files) {
|
||||
for (const { path, mode, type } of treeResp.data.tree) {
|
||||
if (path === file.path) {
|
||||
updatedTreeItems.push({ path: path, mode: mode as any, type: type as any, content: file.content });
|
||||
if (hasDeletes) {
|
||||
updatedTree = treeResp.data.tree as GitCreateTreeParamsTree[];
|
||||
|
||||
for (const operation of operations) {
|
||||
switch (operation.type) {
|
||||
case 'created':
|
||||
updatedTree.push({ path: operation.path, mode: '100644', type: 'blob', content: operation.content });
|
||||
break;
|
||||
|
||||
case 'changed':
|
||||
const item = updatedTree.find(item => item.path === operation.path);
|
||||
if (item !== undefined) {
|
||||
updatedTree.push({ ...item, content: operation.content });
|
||||
}
|
||||
break;
|
||||
|
||||
case 'deleted':
|
||||
const index = updatedTree.findIndex(item => item.path === operation.path);
|
||||
if (index !== -1) {
|
||||
updatedTree.splice(index, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
updatedTree = [];
|
||||
|
||||
for (const operation of operations) {
|
||||
switch (operation.type) {
|
||||
case 'created':
|
||||
updatedTree.push({ path: operation.path, mode: '100644', type: 'blob', content: operation.content });
|
||||
break;
|
||||
|
||||
case 'changed':
|
||||
const item = treeResp.data.tree.find(item => item.path === operation.path) as GitCreateTreeParamsTree;
|
||||
if (item !== undefined) {
|
||||
updatedTree.push({ ...item, content: operation.content });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -128,8 +189,8 @@ export class GitHubApi implements Disposable {
|
|||
const updatedTreeResp = await github.git.createTree({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
base_tree: treeResp.data.sha,
|
||||
tree: updatedTreeItems
|
||||
base_tree: hasDeletes ? undefined : treeResp.data.sha,
|
||||
tree: updatedTree
|
||||
});
|
||||
|
||||
const resp = await github.git.createCommit({
|
||||
|
@ -354,7 +415,7 @@ export class GitHubApi implements Disposable {
|
|||
|
||||
private readonly pendingContextRequests = new Map<string, Promise<GitHubApiContext>>();
|
||||
async getContext(uri: Uri): Promise<GitHubApiContext> {
|
||||
const rootUri = getRootUri(uri);
|
||||
const rootUri = getGitHubRootUri(uri);
|
||||
|
||||
let pending = this.pendingContextRequests.get(rootUri.toString());
|
||||
if (pending === undefined) {
|
||||
|
|
|
@ -89,9 +89,8 @@ export class GitHubFS implements FileSystemProvider, FileSearchProvider, TextSea
|
|||
}
|
||||
|
||||
async stat(uri: Uri): Promise<FileStat> {
|
||||
const context = await this.github.getContext(uri);
|
||||
|
||||
if (uri.path === '' || uri.path.lastIndexOf('/') === 0) {
|
||||
const context = await this.github.getContext(uri);
|
||||
return { type: FileType.Directory, size: 0, ctime: 0, mtime: context?.timestamp };
|
||||
}
|
||||
|
||||
|
@ -107,9 +106,15 @@ export class GitHubFS implements FileSystemProvider, FileSearchProvider, TextSea
|
|||
this.getCache(uri),
|
||||
);
|
||||
|
||||
if (data === undefined) {
|
||||
throw FileSystemError.FileNotFound();
|
||||
}
|
||||
|
||||
const context = await this.github.getContext(uri);
|
||||
|
||||
return {
|
||||
type: typenameToFileType(data?.__typename),
|
||||
size: data?.byteSize ?? 0,
|
||||
type: typenameToFileType(data.__typename),
|
||||
size: data.byteSize ?? 0,
|
||||
ctime: 0,
|
||||
mtime: context?.timestamp,
|
||||
};
|
||||
|
@ -136,7 +141,7 @@ export class GitHubFS implements FileSystemProvider, FileSearchProvider, TextSea
|
|||
}
|
||||
|
||||
createDirectory(_uri: Uri): void | Thenable<void> {
|
||||
throw FileSystemError.NoPermissions;
|
||||
throw FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
async readFile(uri: Uri): Promise<Uint8Array> {
|
||||
|
@ -169,19 +174,19 @@ export class GitHubFS implements FileSystemProvider, FileSearchProvider, TextSea
|
|||
}
|
||||
|
||||
async writeFile(_uri: Uri, _content: Uint8Array, _options: { create: boolean, overwrite: boolean }): Promise<void> {
|
||||
throw FileSystemError.NoPermissions;
|
||||
throw FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
delete(_uri: Uri, _options: { recursive: boolean }): void | Thenable<void> {
|
||||
throw FileSystemError.NoPermissions;
|
||||
throw FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
rename(_oldUri: Uri, _newUri: Uri, _options: { overwrite: boolean }): void | Thenable<void> {
|
||||
throw FileSystemError.NoPermissions;
|
||||
throw FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
copy(_source: Uri, _destination: Uri, _options: { overwrite: boolean }): void | Thenable<void> {
|
||||
throw FileSystemError.NoPermissions;
|
||||
throw FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
|
|
@ -4,9 +4,13 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { CancellationToken, commands, Disposable, scm, SourceControl, SourceControlResourceGroup, SourceControlResourceState, Uri, workspace } from 'vscode';
|
||||
import { CancellationToken, commands, Disposable, scm, SourceControl, SourceControlResourceGroup, SourceControlResourceState, Uri, window, workspace } from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { IChangeStore } from './stores';
|
||||
import { GitHubApi } from './github/api';
|
||||
import { GitHubApi, CommitOperation } from './github/api';
|
||||
import { getRelativePath } from './extension';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
interface ScmProvider {
|
||||
sourceControl: SourceControl,
|
||||
|
@ -35,8 +39,8 @@ export class VirtualSCM implements Disposable {
|
|||
);
|
||||
|
||||
for (const { uri } of workspace.workspaceFolders ?? []) {
|
||||
for (const change of changeStore.getChanges(uri)) {
|
||||
this.update(uri, change.uri);
|
||||
for (const operation of changeStore.getChanges(uri)) {
|
||||
this.update(uri, operation.uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -62,18 +66,28 @@ export class VirtualSCM implements Disposable {
|
|||
}
|
||||
|
||||
async commitChanges(sourceControl: SourceControl): Promise<void> {
|
||||
const rootPath = sourceControl.rootUri!.fsPath;
|
||||
const files = this.changeStore
|
||||
const operations = this.changeStore
|
||||
.getChanges(sourceControl.rootUri!)
|
||||
.map<{ path: string; content: string }>(c => ({ path: c.uri.fsPath.substr(rootPath.length + 1), content: this.changeStore.getContent(c.uri)! }));
|
||||
if (!files.length) {
|
||||
// TODO@eamodio show message
|
||||
.map<CommitOperation>(operation => {
|
||||
const path = getRelativePath(sourceControl.rootUri!, operation.uri);
|
||||
switch (operation.type) {
|
||||
case 'created':
|
||||
return { type: operation.type, path: path, content: this.changeStore.getContent(operation.uri)! };
|
||||
case 'changed':
|
||||
return { type: operation.type, path: path, content: this.changeStore.getContent(operation.uri)! };
|
||||
case 'deleted':
|
||||
return { type: operation.type, path: path };
|
||||
}
|
||||
});
|
||||
if (!operations.length) {
|
||||
window.showInformationMessage(localize('no changes', "There are no changes to commit."));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const message = sourceControl.inputBox.value;
|
||||
if (message) {
|
||||
const sha = await this.github.commit(this.getOriginalResource(sourceControl.rootUri!), message, files);
|
||||
const sha = await this.github.commit(this.getOriginalResource(sourceControl.rootUri!), message, operations);
|
||||
if (sha !== undefined) {
|
||||
this.changeStore.acceptAll(sourceControl.rootUri!);
|
||||
sourceControl.inputBox.value = '';
|
||||
|
@ -82,7 +96,7 @@ export class VirtualSCM implements Disposable {
|
|||
}
|
||||
|
||||
discardChanges(uri: Uri): Promise<void> {
|
||||
return this.changeStore.discardChanges(uri);
|
||||
return this.changeStore.discard(uri);
|
||||
}
|
||||
|
||||
openChanges(uri: Uri) {
|
||||
|
@ -101,9 +115,12 @@ export class VirtualSCM implements Disposable {
|
|||
|
||||
const provider = this.createScmProvider(rootUri, folder.name);
|
||||
const group = this.createChangesGroup(provider);
|
||||
group.resourceStates = this.changeStore.getChanges(rootUri).map<SourceControlResourceState>(c => {
|
||||
group.resourceStates = this.changeStore.getChanges(rootUri).map<SourceControlResourceState>(op => {
|
||||
const rs: SourceControlResourceState = {
|
||||
resourceUri: c.uri,
|
||||
decorations: {
|
||||
strikeThrough: op.type === 'deleted'
|
||||
},
|
||||
resourceUri: op.uri,
|
||||
command: {
|
||||
command: 'githubBrowser.openChanges',
|
||||
title: 'Open Changes',
|
||||
|
|
|
@ -5,21 +5,21 @@
|
|||
|
||||
'use strict';
|
||||
import { commands, Event, EventEmitter, FileStat, FileType, Memento, TextDocumentShowOptions, Uri, ViewColumn } from 'vscode';
|
||||
import { getRootUri } from './extension';
|
||||
import { getRootUri, getRelativePath } from './extension';
|
||||
import { sha1 } from './sha1';
|
||||
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
interface CreateRecord<T extends string | Uri = string> {
|
||||
interface CreateOperation<T extends string | Uri = string> {
|
||||
type: 'created';
|
||||
size: number;
|
||||
timestamp: number;
|
||||
uri: T;
|
||||
hash: string;
|
||||
originalHash: undefined;
|
||||
originalHash: string;
|
||||
}
|
||||
|
||||
interface ChangeRecord<T extends string | Uri = string> {
|
||||
interface ChangeOperation<T extends string | Uri = string> {
|
||||
type: 'changed';
|
||||
size: number;
|
||||
timestamp: number;
|
||||
|
@ -28,88 +28,82 @@ interface ChangeRecord<T extends string | Uri = string> {
|
|||
originalHash: string;
|
||||
}
|
||||
|
||||
interface DeleteRecord<T extends string | Uri = string> {
|
||||
interface DeleteOperation<T extends string | Uri = string> {
|
||||
type: 'deleted';
|
||||
size: number;
|
||||
size: undefined;
|
||||
timestamp: number;
|
||||
uri: T;
|
||||
hash: undefined;
|
||||
originalHash: string;
|
||||
originalHash: undefined;
|
||||
}
|
||||
|
||||
export type Record = CreateRecord<Uri> | ChangeRecord<Uri> | DeleteRecord<Uri>;
|
||||
type StoredRecord = CreateRecord | ChangeRecord | DeleteRecord;
|
||||
export type Operation = CreateOperation<Uri> | ChangeOperation<Uri> | DeleteOperation<Uri>;
|
||||
type StoredOperation = CreateOperation | ChangeOperation | DeleteOperation;
|
||||
|
||||
const workingChangesKeyPrefix = 'github.working.changes|';
|
||||
const workingOperationsKeyPrefix = 'github.working.changes|';
|
||||
const workingFileKeyPrefix = 'github.working|';
|
||||
|
||||
function fromSerialized(change: StoredRecord): Record {
|
||||
return { ...change, uri: Uri.parse(change.uri) };
|
||||
function fromSerialized(operations: StoredOperation): Operation {
|
||||
return { ...operations, uri: Uri.parse(operations.uri) };
|
||||
}
|
||||
|
||||
interface CreatedFileChangeStoreEvent {
|
||||
type: 'created';
|
||||
rootUri: Uri;
|
||||
size: number;
|
||||
timestamp: number;
|
||||
uri: Uri;
|
||||
hash: string;
|
||||
originalHash: undefined;
|
||||
}
|
||||
|
||||
interface ChangedFileChangeStoreEvent {
|
||||
type: 'changed';
|
||||
rootUri: Uri;
|
||||
size: number;
|
||||
timestamp: number;
|
||||
uri: Uri;
|
||||
hash: string;
|
||||
originalHash: string;
|
||||
}
|
||||
|
||||
interface DeletedFileChangeStoreEvent {
|
||||
type: 'deleted';
|
||||
rootUri: Uri;
|
||||
size: number;
|
||||
timestamp: number;
|
||||
uri: Uri;
|
||||
|
||||
hash: undefined;
|
||||
originalHash: string;
|
||||
}
|
||||
|
||||
type ChangeStoreEvent = CreatedFileChangeStoreEvent | ChangedFileChangeStoreEvent | DeletedFileChangeStoreEvent;
|
||||
|
||||
function toChangeEvent(change: Record | StoredRecord, rootUri: Uri, uri?: Uri): ChangeStoreEvent {
|
||||
function toChangeStoreEvent(operation: Operation | StoredOperation, rootUri: Uri, uri?: Uri): ChangeStoreEvent {
|
||||
return {
|
||||
...change,
|
||||
type: operation.type,
|
||||
rootUri: rootUri,
|
||||
uri: uri ?? (typeof change.uri === 'string' ? Uri.parse(change.uri) : change.uri)
|
||||
uri: uri ?? (typeof operation.uri === 'string' ? Uri.parse(operation.uri) : operation.uri)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export interface IChangeStore {
|
||||
onDidChange: Event<ChangeStoreEvent>;
|
||||
|
||||
acceptAll(rootUri: Uri): Promise<void>;
|
||||
|
||||
discard(uri: Uri): Promise<void>;
|
||||
discardAll(rootUri: Uri): Promise<void>;
|
||||
discardChanges(uri: Uri): Promise<void>;
|
||||
|
||||
getChanges(rootUri: Uri): Record[];
|
||||
getChanges(rootUri: Uri): Operation[];
|
||||
getContent(uri: Uri): string | undefined;
|
||||
getStat(uri: Uri): FileStat | undefined;
|
||||
|
||||
hasChanges(rootUri: Uri): boolean;
|
||||
|
||||
openChanges(uri: Uri, original: Uri): void;
|
||||
openFile(uri: Uri): void;
|
||||
|
||||
recordFileChange(uri: Uri, content: Uint8Array, originalContent: () => Uint8Array | Thenable<Uint8Array>): Promise<void>;
|
||||
}
|
||||
|
||||
export class ChangeStore implements IChangeStore {
|
||||
export interface IWritableChangeStore {
|
||||
onDidChange: Event<ChangeStoreEvent>;
|
||||
|
||||
hasChanges(rootUri: Uri): boolean;
|
||||
|
||||
getContent(uri: Uri): string | undefined;
|
||||
getStat(uri: Uri): FileStat | undefined;
|
||||
updateDirectoryEntries(uri: Uri, entries: [string, FileType][]): [string, FileType][];
|
||||
|
||||
onFileChanged(uri: Uri, content: Uint8Array, originalContent: () => Uint8Array | Thenable<Uint8Array>): Promise<void>;
|
||||
onFileCreated(uri: Uri, content: Uint8Array): Promise<void>;
|
||||
onFileDeleted(uri: Uri): Promise<void>;
|
||||
}
|
||||
|
||||
export class ChangeStore implements IChangeStore, IWritableChangeStore {
|
||||
private _onDidChange = new EventEmitter<ChangeStoreEvent>();
|
||||
get onDidChange(): Event<ChangeStoreEvent> {
|
||||
return this._onDidChange.event;
|
||||
|
@ -118,28 +112,17 @@ export class ChangeStore implements IChangeStore {
|
|||
constructor(private readonly memento: Memento) { }
|
||||
|
||||
async acceptAll(rootUri: Uri): Promise<void> {
|
||||
const changes = this.getChanges(rootUri);
|
||||
const operations = this.getChanges(rootUri);
|
||||
|
||||
await this.saveWorkingChanges(rootUri, undefined);
|
||||
await this.saveWorkingOperations(rootUri, undefined);
|
||||
|
||||
for (const change of changes) {
|
||||
await this.discardWorkingContent(change.uri);
|
||||
this._onDidChange.fire(toChangeEvent(change, rootUri));
|
||||
for (const operation of operations) {
|
||||
await this.discardWorkingContent(operation.uri);
|
||||
this._onDidChange.fire(toChangeStoreEvent(operation, rootUri));
|
||||
}
|
||||
}
|
||||
|
||||
async discardAll(rootUri: Uri): Promise<void> {
|
||||
const changes = this.getChanges(rootUri);
|
||||
|
||||
await this.saveWorkingChanges(rootUri, undefined);
|
||||
|
||||
for (const change of changes) {
|
||||
await this.discardWorkingContent(change.uri);
|
||||
this._onDidChange.fire(toChangeEvent(change, rootUri));
|
||||
}
|
||||
}
|
||||
|
||||
async discardChanges(uri: Uri): Promise<void> {
|
||||
async discard(uri: Uri): Promise<void> {
|
||||
const rootUri = getRootUri(uri);
|
||||
if (rootUri === undefined) {
|
||||
return;
|
||||
|
@ -147,21 +130,36 @@ export class ChangeStore implements IChangeStore {
|
|||
|
||||
const key = uri.toString();
|
||||
|
||||
const changes = this.getWorkingChanges(rootUri);
|
||||
const index = changes.findIndex(c => c.uri === key);
|
||||
const operations = this.getWorkingOperations(rootUri);
|
||||
const index = operations.findIndex(c => c.uri === key);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [change] = changes.splice(index, 1);
|
||||
await this.saveWorkingChanges(rootUri, changes);
|
||||
const [operation] = operations.splice(index, 1);
|
||||
await this.saveWorkingOperations(rootUri, operations);
|
||||
await this.discardWorkingContent(uri);
|
||||
|
||||
this._onDidChange.fire(toChangeEvent(change, rootUri, uri));
|
||||
this._onDidChange.fire({
|
||||
type: operation.type === 'created' ? 'deleted' : operation.type === 'deleted' ? 'created' : 'changed',
|
||||
rootUri: rootUri,
|
||||
uri: uri
|
||||
});
|
||||
}
|
||||
|
||||
async discardAll(rootUri: Uri): Promise<void> {
|
||||
const operations = this.getChanges(rootUri);
|
||||
|
||||
await this.saveWorkingOperations(rootUri, undefined);
|
||||
|
||||
for (const operation of operations) {
|
||||
await this.discardWorkingContent(operation.uri);
|
||||
this._onDidChange.fire(toChangeStoreEvent(operation, rootUri));
|
||||
}
|
||||
}
|
||||
|
||||
getChanges(rootUri: Uri) {
|
||||
return this.getWorkingChanges(rootUri).map(c => fromSerialized(c));
|
||||
return this.getWorkingOperations(rootUri).map(c => fromSerialized(c));
|
||||
}
|
||||
|
||||
getContent(uri: Uri): string | undefined {
|
||||
|
@ -170,21 +168,170 @@ export class ChangeStore implements IChangeStore {
|
|||
|
||||
getStat(uri: Uri): FileStat | undefined {
|
||||
const key = uri.toString();
|
||||
const change = this.getChanges(getRootUri(uri)!).find(c => c.uri.toString() === key);
|
||||
if (change === undefined) {
|
||||
const operation = this.getChanges(getRootUri(uri)!).find(c => c.uri.toString() === key);
|
||||
if (operation === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
type: FileType.File,
|
||||
size: change.size,
|
||||
size: operation.size ?? 0,
|
||||
ctime: 0,
|
||||
mtime: change.timestamp
|
||||
mtime: operation.timestamp
|
||||
};
|
||||
}
|
||||
|
||||
hasChanges(rootUri: Uri): boolean {
|
||||
return this.getWorkingChanges(rootUri).length !== 0;
|
||||
return this.getWorkingOperations(rootUri).length !== 0;
|
||||
}
|
||||
|
||||
updateDirectoryEntries(uri: Uri, entries: [string, FileType][]): [string, FileType][] {
|
||||
const rootUri = getRootUri(uri);
|
||||
if (rootUri === undefined) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
const operations = this.getChanges(rootUri);
|
||||
for (const operation of operations) {
|
||||
switch (operation.type) {
|
||||
case 'changed':
|
||||
continue;
|
||||
case 'created': {
|
||||
const file = getRelativePath(rootUri, operation.uri);
|
||||
entries.push([file, FileType.File]);
|
||||
break;
|
||||
}
|
||||
case 'deleted': {
|
||||
const file = getRelativePath(rootUri, operation.uri);
|
||||
const index = entries.findIndex(([path]) => path === file);
|
||||
if (index !== -1) {
|
||||
entries.splice(index, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
async onFileChanged(uri: Uri, content: Uint8Array, originalContent: () => Uint8Array | Thenable<Uint8Array>): Promise<void> {
|
||||
const rootUri = getRootUri(uri);
|
||||
if (rootUri === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = uri.toString();
|
||||
|
||||
const operations = this.getWorkingOperations(rootUri);
|
||||
|
||||
const hash = await sha1(content);
|
||||
|
||||
let operation = operations.find(c => c.uri === key);
|
||||
if (operation === undefined) {
|
||||
const originalHash = await sha1(await originalContent!());
|
||||
if (hash === originalHash) {
|
||||
return;
|
||||
}
|
||||
|
||||
operation = {
|
||||
type: 'changed',
|
||||
size: content.byteLength,
|
||||
timestamp: Date.now(),
|
||||
uri: key,
|
||||
hash: hash!,
|
||||
originalHash: originalHash
|
||||
} as ChangeOperation;
|
||||
operations.push(operation);
|
||||
|
||||
await this.saveWorkingOperations(rootUri, operations);
|
||||
await this.saveWorkingContent(uri, textDecoder.decode(content));
|
||||
} else if (hash! === operation.originalHash) {
|
||||
operations.splice(operations.indexOf(operation), 1);
|
||||
|
||||
await this.saveWorkingOperations(rootUri, operations);
|
||||
await this.discardWorkingContent(uri);
|
||||
} else if (operation.hash !== hash) {
|
||||
operation.hash = hash!;
|
||||
operation.timestamp = Date.now();
|
||||
|
||||
await this.saveWorkingOperations(rootUri, operations);
|
||||
await this.saveWorkingContent(uri, textDecoder.decode(content));
|
||||
}
|
||||
|
||||
this._onDidChange.fire(toChangeStoreEvent(operation, rootUri, uri));
|
||||
}
|
||||
|
||||
async onFileCreated(uri: Uri, content: Uint8Array): Promise<void> {
|
||||
const rootUri = getRootUri(uri);
|
||||
if (rootUri === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = uri.toString();
|
||||
|
||||
const operations = this.getWorkingOperations(rootUri);
|
||||
|
||||
const hash = await sha1(content);
|
||||
|
||||
let operation = operations.find(c => c.uri === key);
|
||||
if (operation === undefined) {
|
||||
operation = {
|
||||
type: 'created',
|
||||
size: content.byteLength,
|
||||
timestamp: Date.now(),
|
||||
uri: key,
|
||||
hash: hash!,
|
||||
originalHash: hash!
|
||||
} as CreateOperation;
|
||||
operations.push(operation);
|
||||
|
||||
await this.saveWorkingOperations(rootUri, operations);
|
||||
await this.saveWorkingContent(uri, textDecoder.decode(content));
|
||||
} else {
|
||||
// Shouldn't happen, but if it does just update the contents
|
||||
operation.hash = hash!;
|
||||
operation.timestamp = Date.now();
|
||||
|
||||
await this.saveWorkingOperations(rootUri, operations);
|
||||
await this.saveWorkingContent(uri, textDecoder.decode(content));
|
||||
}
|
||||
|
||||
this._onDidChange.fire(toChangeStoreEvent(operation, rootUri, uri));
|
||||
}
|
||||
|
||||
async onFileDeleted(uri: Uri): Promise<void> {
|
||||
const rootUri = getRootUri(uri);
|
||||
if (rootUri === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = uri.toString();
|
||||
|
||||
const operations = this.getWorkingOperations(rootUri);
|
||||
|
||||
let operation = operations.find(c => c.uri === key);
|
||||
if (operation !== undefined) {
|
||||
operations.splice(operations.indexOf(operation), 1);
|
||||
}
|
||||
|
||||
const wasCreated = operation?.type === 'created';
|
||||
|
||||
operation = {
|
||||
type: 'deleted',
|
||||
timestamp: Date.now(),
|
||||
uri: key,
|
||||
} as DeleteOperation;
|
||||
|
||||
// Only track the delete, if we weren't tracking the create
|
||||
if (!wasCreated) {
|
||||
operations.push(operation);
|
||||
}
|
||||
|
||||
await this.saveWorkingOperations(rootUri, operations);
|
||||
await this.discardWorkingContent(uri);
|
||||
|
||||
this._onDidChange.fire(toChangeStoreEvent(operation, rootUri, uri));
|
||||
}
|
||||
|
||||
async openChanges(uri: Uri, original: Uri) {
|
||||
|
@ -207,59 +354,12 @@ export class ChangeStore implements IChangeStore {
|
|||
await commands.executeCommand('vscode.open', uri, opts);
|
||||
}
|
||||
|
||||
async recordFileChange(uri: Uri, content: Uint8Array, originalContent: () => Uint8Array | Thenable<Uint8Array>): Promise<void> {
|
||||
const rootUri = getRootUri(uri);
|
||||
if (rootUri === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = uri.toString();
|
||||
|
||||
const changes = this.getWorkingChanges(rootUri);
|
||||
|
||||
const hash = await sha1(content);
|
||||
|
||||
let change = changes.find(c => c.uri === key);
|
||||
if (change === undefined) {
|
||||
const originalHash = await sha1(await originalContent!());
|
||||
if (hash === originalHash) {
|
||||
return;
|
||||
}
|
||||
|
||||
change = {
|
||||
type: 'changed',
|
||||
size: content.byteLength,
|
||||
timestamp: Date.now(),
|
||||
uri: key,
|
||||
hash: hash!,
|
||||
originalHash: originalHash
|
||||
} as ChangeRecord;
|
||||
changes.push(change);
|
||||
|
||||
await this.saveWorkingChanges(rootUri, changes);
|
||||
await this.saveWorkingContent(uri, textDecoder.decode(content));
|
||||
} else if (hash! === change.originalHash) {
|
||||
changes.splice(changes.indexOf(change), 1);
|
||||
|
||||
await this.saveWorkingChanges(rootUri, changes);
|
||||
await this.discardWorkingContent(uri);
|
||||
} else if (change.hash !== hash) {
|
||||
change.hash = hash!;
|
||||
change.timestamp = Date.now();
|
||||
|
||||
await this.saveWorkingChanges(rootUri, changes);
|
||||
await this.saveWorkingContent(uri, textDecoder.decode(content));
|
||||
}
|
||||
|
||||
this._onDidChange.fire(toChangeEvent(change, rootUri, uri));
|
||||
private getWorkingOperations(rootUri: Uri): StoredOperation[] {
|
||||
return this.memento.get(`${workingOperationsKeyPrefix}${rootUri.toString()}`, []);
|
||||
}
|
||||
|
||||
private getWorkingChanges(rootUri: Uri): StoredRecord[] {
|
||||
return this.memento.get(`${workingChangesKeyPrefix}${rootUri.toString()}`, []);
|
||||
}
|
||||
|
||||
private async saveWorkingChanges(rootUri: Uri, changes: StoredRecord[] | undefined): Promise<void> {
|
||||
await this.memento.update(`${workingChangesKeyPrefix}${rootUri.toString()}`, changes);
|
||||
private async saveWorkingOperations(rootUri: Uri, operations: StoredOperation[] | undefined): Promise<void> {
|
||||
await this.memento.update(`${workingOperationsKeyPrefix}${rootUri.toString()}`, operations);
|
||||
}
|
||||
|
||||
private async saveWorkingContent(uri: Uri, content: string): Promise<void> {
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue