mirror of
https://github.com/Microsoft/vscode
synced 2024-08-24 19:46:05 +00:00
Removes internal gh browser & adds external
This commit is contained in:
parent
c2224dab6c
commit
ea63321bc6
|
@ -240,7 +240,8 @@ const excludedWebExtensions = excludedCommonExtensions.concat([
|
|||
]);
|
||||
|
||||
const marketplaceWebExtensions = [
|
||||
'ms-vscode.references-view'
|
||||
'ms-vscode.references-view',
|
||||
'ms-vscode.github-browser'
|
||||
];
|
||||
|
||||
interface IBuiltInExtension {
|
||||
|
@ -309,7 +310,7 @@ export function packageLocalExtensionsStream(forWeb: boolean): Stream {
|
|||
export function packageMarketplaceExtensionsStream(forWeb: boolean): Stream {
|
||||
const marketplaceExtensionsDescriptions = (
|
||||
builtInExtensions
|
||||
.filter(({ name }) => (forWeb ? marketplaceWebExtensions.indexOf(name) >= 0 : true))
|
||||
.filter(({ name }) => (forWeb ? marketplaceWebExtensions.indexOf(name) >= 0 : true))
|
||||
);
|
||||
const marketplaceExtensionsStream = minifyExtensionResources(
|
||||
es.merge(
|
||||
|
|
3
extensions/github-browser/.gitignore
vendored
3
extensions/github-browser/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
|||
dist
|
||||
out
|
||||
node_modules
|
|
@ -1,11 +0,0 @@
|
|||
.vscode/**
|
||||
build/**
|
||||
dist/**
|
||||
out/**
|
||||
src/**
|
||||
typings/**
|
||||
.gitignore
|
||||
extension-browser.webpack.config.js
|
||||
extension.webpack.config.js
|
||||
tsconfig.json
|
||||
yarn.lock
|
|
@ -1,7 +0,0 @@
|
|||
# GitHub FileSystem for Visual Studio Code
|
||||
|
||||
**Notice:** This extension is bundled with Visual Studio Code. It can be disabled but not uninstalled.
|
||||
|
||||
## Features
|
||||
|
||||
This extension provides remote GitHub repository features for VS Code.
|
|
@ -1,25 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const withBrowserDefaults = require('../shared.webpack.config').browser;
|
||||
|
||||
const config = withBrowserDefaults({
|
||||
context: __dirname,
|
||||
node: false,
|
||||
entry: {
|
||||
extension: './src/extension.ts'
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'node-fetch': path.resolve(__dirname, 'node_modules/node-fetch/browser.js')
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = config;
|
|
@ -1,17 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
//@ts-check
|
||||
|
||||
'use strict';
|
||||
|
||||
const withDefaults = require('../shared.webpack.config');
|
||||
|
||||
module.exports = withDefaults({
|
||||
context: __dirname,
|
||||
entry: {
|
||||
extension: './src/extension.ts'
|
||||
}
|
||||
});
|
|
@ -1,168 +0,0 @@
|
|||
{
|
||||
"name": "github-browser",
|
||||
"displayName": "%displayName%",
|
||||
"description": "%description%",
|
||||
"publisher": "vscode",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"vscode": "^1.45.0"
|
||||
},
|
||||
"enableProposedApi": true,
|
||||
"private": true,
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"onFileSystem:codespace",
|
||||
"onFileSystem:github",
|
||||
"onCommand:githubBrowser.openRepository"
|
||||
],
|
||||
"browser": "./dist/browser/extension.js",
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "githubBrowser.openRepository",
|
||||
"title": "Open GitHub Repository...",
|
||||
"category": "GitHub Browser"
|
||||
},
|
||||
{
|
||||
"command": "githubBrowser.commit",
|
||||
"title": "Commit",
|
||||
"icon": "$(check)",
|
||||
"category": "GitHub Browser"
|
||||
},
|
||||
{
|
||||
"command": "githubBrowser.discardChanges",
|
||||
"title": "Discard Changes",
|
||||
"icon": "$(discard)",
|
||||
"category": "GitHub Browser"
|
||||
},
|
||||
{
|
||||
"command": "githubBrowser.openChanges",
|
||||
"title": "Open Changes",
|
||||
"icon": "$(git-compare)",
|
||||
"category": "GitHub Browser"
|
||||
},
|
||||
{
|
||||
"command": "githubBrowser.openFile",
|
||||
"title": "Open File",
|
||||
"icon": "$(go-to-file)",
|
||||
"category": "GitHub Browser"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "githubBrowser.openRepository",
|
||||
"when": "config.githubBrowser.openRepository"
|
||||
},
|
||||
{
|
||||
"command": "githubBrowser.commit",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "githubBrowser.discardChanges",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "githubBrowser.openChanges",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "githubBrowser.openFile",
|
||||
"when": "false"
|
||||
}
|
||||
],
|
||||
"scm/title": [
|
||||
{
|
||||
"command": "githubBrowser.commit",
|
||||
"group": "navigation",
|
||||
"when": "scmProvider == github"
|
||||
}
|
||||
],
|
||||
"scm/resourceState/context": [
|
||||
{
|
||||
"command": "githubBrowser.openFile",
|
||||
"when": "scmProvider == github && scmResourceGroup == github.changes",
|
||||
"group": "inline@0"
|
||||
},
|
||||
{
|
||||
"command": "githubBrowser.discardChanges",
|
||||
"when": "scmProvider == github && scmResourceGroup == github.changes",
|
||||
"group": "inline@1"
|
||||
},
|
||||
{
|
||||
"command": "githubBrowser.openChanges",
|
||||
"when": "scmProvider == github && scmResourceGroup == github.changes",
|
||||
"group": "navigation@0"
|
||||
},
|
||||
{
|
||||
"command": "githubBrowser.openFile",
|
||||
"when": "scmProvider == github && scmResourceGroup == github.changes",
|
||||
"group": "navigation@1"
|
||||
},
|
||||
{
|
||||
"command": "githubBrowser.discardChanges",
|
||||
"when": "scmProvider == github && scmResourceGroup == github.changes",
|
||||
"group": "1_modification@0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"resourceLabelFormatters": [
|
||||
{
|
||||
"scheme": "github",
|
||||
"authority": "HEAD",
|
||||
"formatting": {
|
||||
"label": "github.com${path}",
|
||||
"separator": "/",
|
||||
"workspaceSuffix": "GitHub"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scheme": "github",
|
||||
"authority": "*",
|
||||
"formatting": {
|
||||
"label": "github.com${path} (${authority})",
|
||||
"separator": "/",
|
||||
"workspaceSuffix": "GitHub"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scheme": "codespace",
|
||||
"authority": "HEAD",
|
||||
"formatting": {
|
||||
"label": "github.com${path}",
|
||||
"separator": "/",
|
||||
"workspaceSuffix": "GitHub"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scheme": "codespace",
|
||||
"authority": "*",
|
||||
"formatting": {
|
||||
"label": "github.com${path} (${authority})",
|
||||
"separator": "/",
|
||||
"workspaceSuffix": "GitHub"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"compile": "gulp compile-extension:github-browser",
|
||||
"compile-web": "npx webpack-cli --config extension-browser.webpack.config --mode none",
|
||||
"watch": "gulp watch-extension:github-browser",
|
||||
"watch-web": "npx webpack-cli --config extension-browser.webpack.config --mode none --watch --info-verbosity verbose",
|
||||
"vscode:prepublish": "npm run compile"
|
||||
},
|
||||
"dependencies": {
|
||||
"@octokit/graphql": "4.5.1",
|
||||
"@octokit/rest": "18.0.0",
|
||||
"fuzzysort": "1.1.4",
|
||||
"node-fetch": "2.6.0",
|
||||
"vscode-nls": "4.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node-fetch": "2.5.7"
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"displayName": "GitHub Browser",
|
||||
"description": "Remotely browse a GitHub repository"
|
||||
}
|
|
@ -1,380 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { commands, Event, EventEmitter, FileStat, FileType, Memento, TextDocumentShowOptions, Uri, ViewColumn } from 'vscode';
|
||||
import { getRootUri, getRelativePath, isChild } from './extension';
|
||||
import { sha1 } from './sha1';
|
||||
|
||||
const textDecoder = new TextDecoder();
|
||||
|
||||
interface CreateOperation<T extends string | Uri = string> {
|
||||
type: 'created';
|
||||
size: number;
|
||||
timestamp: number;
|
||||
uri: T;
|
||||
hash: string;
|
||||
originalHash: string;
|
||||
}
|
||||
|
||||
interface ChangeOperation<T extends string | Uri = string> {
|
||||
type: 'changed';
|
||||
size: number;
|
||||
timestamp: number;
|
||||
uri: T;
|
||||
hash: string;
|
||||
originalHash: string;
|
||||
}
|
||||
|
||||
interface DeleteOperation<T extends string | Uri = string> {
|
||||
type: 'deleted';
|
||||
size: undefined;
|
||||
timestamp: number;
|
||||
uri: T;
|
||||
hash: undefined;
|
||||
originalHash: undefined;
|
||||
}
|
||||
|
||||
export type Operation = CreateOperation<Uri> | ChangeOperation<Uri> | DeleteOperation<Uri>;
|
||||
type StoredOperation = CreateOperation | ChangeOperation | DeleteOperation;
|
||||
|
||||
const workingOperationsKeyPrefix = 'github.working.changes|';
|
||||
const workingFileKeyPrefix = 'github.working|';
|
||||
|
||||
function fromSerialized(operations: StoredOperation): Operation {
|
||||
return { ...operations, uri: Uri.parse(operations.uri) };
|
||||
}
|
||||
|
||||
export interface ChangeStoreEvent {
|
||||
type: 'created' | 'changed' | 'deleted';
|
||||
rootUri: Uri;
|
||||
uri: Uri;
|
||||
}
|
||||
|
||||
function toChangeStoreEvent(operation: Operation | StoredOperation, rootUri: Uri, uri?: Uri): ChangeStoreEvent {
|
||||
return {
|
||||
type: operation.type,
|
||||
rootUri: rootUri,
|
||||
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>;
|
||||
|
||||
hasChanges(rootUri: Uri): boolean;
|
||||
|
||||
getChanges(rootUri: Uri): Operation[];
|
||||
getContent(uri: Uri): string | undefined;
|
||||
|
||||
openChanges(uri: Uri, original: Uri): void;
|
||||
openFile(uri: Uri): void;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
constructor(private readonly memento: Memento) { }
|
||||
|
||||
async acceptAll(rootUri: Uri): Promise<void> {
|
||||
const operations = this.getChanges(rootUri);
|
||||
|
||||
await this.saveWorkingOperations(rootUri, undefined);
|
||||
|
||||
const events: ChangeStoreEvent[] = [];
|
||||
|
||||
for (const operation of operations) {
|
||||
await this.discardWorkingContent(operation.uri);
|
||||
events.push(toChangeStoreEvent(operation, rootUri));
|
||||
}
|
||||
|
||||
for (const e of events) {
|
||||
this._onDidChange.fire(e);
|
||||
}
|
||||
}
|
||||
|
||||
async discard(uri: Uri): Promise<void> {
|
||||
const rootUri = getRootUri(uri);
|
||||
if (rootUri === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = uri.toString();
|
||||
|
||||
const operations = this.getWorkingOperations(rootUri);
|
||||
const index = operations.findIndex(c => c.uri === key);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [operation] = operations.splice(index, 1);
|
||||
await this.saveWorkingOperations(rootUri, operations);
|
||||
await this.discardWorkingContent(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);
|
||||
|
||||
const events: ChangeStoreEvent[] = [];
|
||||
|
||||
for (const operation of operations) {
|
||||
await this.discardWorkingContent(operation.uri);
|
||||
events.push(toChangeStoreEvent(operation, rootUri));
|
||||
}
|
||||
|
||||
for (const e of events) {
|
||||
this._onDidChange.fire(e);
|
||||
}
|
||||
}
|
||||
|
||||
getChanges(rootUri: Uri) {
|
||||
return this.getWorkingOperations(rootUri).map(c => fromSerialized(c));
|
||||
}
|
||||
|
||||
getContent(uri: Uri): string | undefined {
|
||||
return this.memento.get(`${workingFileKeyPrefix}${uri.toString()}`);
|
||||
}
|
||||
|
||||
getStat(uri: Uri): FileStat | undefined {
|
||||
const key = uri.toString();
|
||||
const operation = this.getChanges(getRootUri(uri)!).find(c => c.uri.toString() === key);
|
||||
if (operation === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
type: FileType.File,
|
||||
size: operation.size ?? 0,
|
||||
ctime: 0,
|
||||
mtime: operation.timestamp
|
||||
};
|
||||
}
|
||||
|
||||
hasChanges(rootUri: Uri): boolean {
|
||||
return this.getWorkingOperations(rootUri).length !== 0;
|
||||
}
|
||||
|
||||
updateDirectoryEntries(uri: Uri, entries: [string, FileType][]): [string, FileType][] {
|
||||
const rootUri = getRootUri(uri);
|
||||
if (rootUri === undefined) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
const folderPath = getRelativePath(rootUri, uri);
|
||||
|
||||
const operations = this.getChanges(rootUri);
|
||||
for (const operation of operations) {
|
||||
switch (operation.type) {
|
||||
case 'changed':
|
||||
continue;
|
||||
|
||||
case 'created': {
|
||||
const filePath = getRelativePath(rootUri, operation.uri);
|
||||
if (isChild(folderPath, filePath)) {
|
||||
entries.push([filePath, FileType.File]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'deleted': {
|
||||
const filePath = getRelativePath(rootUri, operation.uri);
|
||||
if (isChild(folderPath, filePath)) {
|
||||
const index = entries.findIndex(([path]) => path === filePath);
|
||||
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) {
|
||||
const opts: TextDocumentShowOptions = {
|
||||
preserveFocus: false,
|
||||
preview: true,
|
||||
viewColumn: ViewColumn.Active
|
||||
};
|
||||
|
||||
await commands.executeCommand('vscode.diff', original, uri, `${uri.fsPath} (Working Tree)`, opts);
|
||||
}
|
||||
|
||||
async openFile(uri: Uri) {
|
||||
const opts: TextDocumentShowOptions = {
|
||||
preserveFocus: false,
|
||||
preview: false,
|
||||
viewColumn: ViewColumn.Active
|
||||
};
|
||||
|
||||
await commands.executeCommand('vscode.open', uri, opts);
|
||||
}
|
||||
|
||||
private getWorkingOperations(rootUri: Uri): StoredOperation[] {
|
||||
return this.memento.get(`${workingOperationsKeyPrefix}${rootUri.toString()}`, []);
|
||||
}
|
||||
|
||||
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> {
|
||||
await this.memento.update(`${workingFileKeyPrefix}${uri.toString()}`, content);
|
||||
}
|
||||
|
||||
private async discardWorkingContent(uri: Uri): Promise<void> {
|
||||
await this.memento.update(`${workingFileKeyPrefix}${uri.toString()}`, undefined);
|
||||
}
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { Event, EventEmitter, Memento, Uri, workspace } from 'vscode';
|
||||
|
||||
export interface WorkspaceFolderContext<T> {
|
||||
context: T;
|
||||
name: string;
|
||||
folderUri: Uri;
|
||||
}
|
||||
|
||||
export class ContextStore<T> {
|
||||
private _onDidChange = new EventEmitter<Uri>();
|
||||
get onDidChange(): Event<Uri> {
|
||||
return this._onDidChange.event;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly scheme: string,
|
||||
private readonly originalScheme: string,
|
||||
private readonly memento: Memento,
|
||||
) { }
|
||||
|
||||
delete(uri: Uri) {
|
||||
return this.set(uri, undefined);
|
||||
}
|
||||
|
||||
get(uri: Uri): T | undefined {
|
||||
return this.memento.get<T>(`${this.originalScheme}.context|${this.getOriginalResource(uri).toString()}`);
|
||||
}
|
||||
|
||||
getForWorkspace(): WorkspaceFolderContext<T>[] {
|
||||
const folders = workspace.workspaceFolders?.filter(f => f.uri.scheme === this.scheme || f.uri.scheme === this.originalScheme) ?? [];
|
||||
return folders.map(f => ({ context: this.get(f.uri)!, name: f.name, folderUri: f.uri })).filter(c => c.context !== undefined);
|
||||
}
|
||||
|
||||
async set(uri: Uri, context: T | undefined) {
|
||||
uri = this.getOriginalResource(uri);
|
||||
await this.memento.update(`${this.originalScheme}.context|${uri.toString()}`, context);
|
||||
this._onDidChange.fire(uri);
|
||||
}
|
||||
|
||||
getOriginalResource(uri: Uri): Uri {
|
||||
return uri.with({ scheme: this.originalScheme });
|
||||
}
|
||||
|
||||
getWorkspaceResource(uri: Uri): Uri {
|
||||
return uri.with({ scheme: this.scheme });
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { commands, ExtensionContext, Uri, window, workspace } from 'vscode';
|
||||
import { ChangeStore } from './changeStore';
|
||||
import { ContextStore } from './contextStore';
|
||||
import { VirtualFS } from './fs';
|
||||
import { GitHubApiContext, GitHubApi } from './github/api';
|
||||
import { GitHubFS } from './github/fs';
|
||||
import { VirtualSCM } from './scm';
|
||||
import { StatusBar } from './statusbar';
|
||||
|
||||
const repositoryRegex = /^(?:(?:https:\/\/)?github.com\/)?([^\/]+)\/([^\/]+?)(?:\/|.git|$)/i;
|
||||
|
||||
export async function activate(context: ExtensionContext) {
|
||||
const contextStore = new ContextStore<GitHubApiContext>('codespace', GitHubFS.scheme, context.workspaceState);
|
||||
const changeStore = new ChangeStore(context.workspaceState);
|
||||
|
||||
const githubApi = new GitHubApi(contextStore);
|
||||
const gitHubFS = new GitHubFS(githubApi);
|
||||
const virtualFS = new VirtualFS('codespace', contextStore, changeStore, gitHubFS);
|
||||
|
||||
context.subscriptions.push(
|
||||
githubApi,
|
||||
gitHubFS,
|
||||
virtualFS,
|
||||
new VirtualSCM(GitHubFS.scheme, githubApi, changeStore),
|
||||
new StatusBar(contextStore, changeStore),
|
||||
);
|
||||
|
||||
commands.registerCommand('githubBrowser.openRepository', async () => {
|
||||
const value = await window.showInputBox({
|
||||
placeHolder: 'e.g. https://github.com/microsoft/vscode',
|
||||
prompt: 'Enter a GitHub repository url',
|
||||
validateInput: value => repositoryRegex.test(value) ? undefined : 'Invalid repository url'
|
||||
});
|
||||
|
||||
if (value) {
|
||||
const match = repositoryRegex.exec(value);
|
||||
if (match) {
|
||||
const [, owner, repo] = match;
|
||||
|
||||
const uri = Uri.parse(`codespace://HEAD/${owner}/${repo}`);
|
||||
openWorkspace(uri, repo, 'currentWindow');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getRelativePath(rootUri: Uri, uri: Uri) {
|
||||
return uri.path.substr(rootUri.path.length + 1);
|
||||
}
|
||||
|
||||
export function getRootUri(uri: Uri) {
|
||||
return workspace.getWorkspaceFolder(uri)?.uri;
|
||||
}
|
||||
|
||||
export function isChild(folderPath: string, filePath: string) {
|
||||
return isDescendent(folderPath, filePath) && filePath.substr(folderPath.length + (folderPath.endsWith('/') ? 0 : 1)).split('/').length === 1;
|
||||
}
|
||||
|
||||
export function isDescendent(folderPath: string, filePath: string) {
|
||||
return folderPath.length === 0 || filePath.startsWith(folderPath.endsWith('/') ? folderPath : `${folderPath}/`);
|
||||
}
|
||||
|
||||
const shaRegex = /^[0-9a-f]{40}$/;
|
||||
export function isSha(ref: string) {
|
||||
return shaRegex.test(ref);
|
||||
}
|
||||
|
||||
function openWorkspace(uri: Uri, name: string, location: 'currentWindow' | 'newWindow' | 'addToCurrentWorkspace') {
|
||||
if (location === 'addToCurrentWorkspace') {
|
||||
const count = (workspace.workspaceFolders && workspace.workspaceFolders.length) || 0;
|
||||
return workspace.updateWorkspaceFolders(count, 0, { uri: uri, name: name });
|
||||
}
|
||||
|
||||
return commands.executeCommand('vscode.openFolder', uri, location === 'newWindow');
|
||||
}
|
|
@ -1,216 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import {
|
||||
CancellationToken,
|
||||
Disposable,
|
||||
Event,
|
||||
EventEmitter,
|
||||
FileChangeEvent,
|
||||
FileChangeType,
|
||||
FileSearchOptions,
|
||||
FileSearchProvider,
|
||||
FileSearchQuery,
|
||||
FileStat,
|
||||
FileSystemError,
|
||||
FileSystemProvider,
|
||||
FileType,
|
||||
Progress,
|
||||
TextSearchOptions,
|
||||
TextSearchProvider,
|
||||
TextSearchQuery,
|
||||
TextSearchResult,
|
||||
Uri,
|
||||
workspace,
|
||||
} from 'vscode';
|
||||
import { IWritableChangeStore } from './changeStore';
|
||||
import { ContextStore } from './contextStore';
|
||||
import { GitHubApiContext } from './github/api';
|
||||
|
||||
const emptyDisposable = { dispose: () => { /* noop */ } };
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSearchProvider, Disposable {
|
||||
private _onDidChangeFile = new EventEmitter<FileChangeEvent[]>();
|
||||
get onDidChangeFile(): Event<FileChangeEvent[]> {
|
||||
return this._onDidChangeFile.event;
|
||||
}
|
||||
|
||||
private readonly disposable: Disposable;
|
||||
|
||||
constructor(
|
||||
readonly scheme: string,
|
||||
private readonly contextStore: ContextStore<GitHubApiContext>,
|
||||
private readonly changeStore: IWritableChangeStore,
|
||||
private readonly fs: FileSystemProvider & FileSearchProvider & TextSearchProvider
|
||||
) {
|
||||
// TODO@eamodio listen for workspace folder changes
|
||||
for (const context of contextStore.getForWorkspace()) {
|
||||
// If we have a saved context, but no longer have any changes, reset the context
|
||||
// We only do this on startup/reload to keep things consistent
|
||||
if (!changeStore.hasChanges(context.folderUri)) {
|
||||
console.log('Clear context', context.folderUri.toString());
|
||||
contextStore.delete(context.folderUri);
|
||||
}
|
||||
}
|
||||
|
||||
this.disposable = Disposable.from(
|
||||
workspace.registerFileSystemProvider(scheme, this, { isCaseSensitive: true }),
|
||||
workspace.registerFileSearchProvider(scheme, this),
|
||||
workspace.registerTextSearchProvider(scheme, this),
|
||||
changeStore.onDidChange(e => {
|
||||
switch (e.type) {
|
||||
case 'created':
|
||||
this._onDidChangeFile.fire([{ type: FileChangeType.Created, uri: e.uri }]);
|
||||
break;
|
||||
case 'changed':
|
||||
this._onDidChangeFile.fire([{ type: FileChangeType.Changed, uri: e.uri }]);
|
||||
break;
|
||||
case 'deleted':
|
||||
this._onDidChangeFile.fire([{ type: FileChangeType.Deleted, uri: e.uri }]);
|
||||
break;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable?.dispose();
|
||||
}
|
||||
|
||||
private getOriginalResource(uri: Uri): Uri {
|
||||
return this.contextStore.getOriginalResource(uri);
|
||||
}
|
||||
|
||||
private getWorkspaceResource(uri: Uri): Uri {
|
||||
return this.contextStore.getWorkspaceResource(uri);
|
||||
}
|
||||
|
||||
//#region FileSystemProvider
|
||||
|
||||
watch(): Disposable {
|
||||
return emptyDisposable;
|
||||
}
|
||||
|
||||
async stat(uri: Uri): Promise<FileStat> {
|
||||
let stat = this.changeStore.getStat(uri);
|
||||
if (stat !== undefined) {
|
||||
return stat;
|
||||
}
|
||||
|
||||
stat = await this.fs.stat(this.getOriginalResource(uri));
|
||||
return stat;
|
||||
}
|
||||
|
||||
async readDirectory(uri: Uri): Promise<[string, FileType][]> {
|
||||
let entries = await this.fs.readDirectory(this.getOriginalResource(uri));
|
||||
entries = this.changeStore.updateDirectoryEntries(uri, entries);
|
||||
return entries;
|
||||
}
|
||||
|
||||
createDirectory(_uri: Uri): void | Thenable<void> {
|
||||
// TODO@eamodio only support files for now
|
||||
throw FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
async readFile(uri: Uri): Promise<Uint8Array> {
|
||||
const content = this.changeStore.getContent(uri);
|
||||
if (content !== undefined) {
|
||||
return textEncoder.encode(content);
|
||||
}
|
||||
|
||||
const data = await this.fs.readFile(this.getOriginalResource(uri));
|
||||
return data;
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
//#region FileSearchProvider
|
||||
|
||||
provideFileSearchResults(
|
||||
query: FileSearchQuery,
|
||||
options: FileSearchOptions,
|
||||
token: CancellationToken,
|
||||
) {
|
||||
return this.fs.provideFileSearchResults(query, { ...options, folder: this.getOriginalResource(options.folder) }, token);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region TextSearchProvider
|
||||
|
||||
provideTextSearchResults(
|
||||
query: TextSearchQuery,
|
||||
options: TextSearchOptions,
|
||||
progress: Progress<TextSearchResult>,
|
||||
token: CancellationToken,
|
||||
) {
|
||||
return this.fs.provideTextSearchResults(
|
||||
query,
|
||||
{ ...options, folder: this.getOriginalResource(options.folder) },
|
||||
{ report: (result: TextSearchResult) => progress.report({ ...result, uri: this.getWorkspaceResource(result.uri) }) },
|
||||
token
|
||||
);
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const emptyStr = '';
|
||||
|
||||
function defaultResolver(...args: any[]): string {
|
||||
if (args.length === 1) {
|
||||
const arg0 = args[0];
|
||||
if (arg0 === undefined || arg0 === null) {
|
||||
return emptyStr;
|
||||
}
|
||||
if (typeof arg0 === 'string') {
|
||||
return arg0;
|
||||
}
|
||||
if (typeof arg0 === 'number' || typeof arg0 === 'boolean') {
|
||||
return String(arg0);
|
||||
}
|
||||
|
||||
return JSON.stringify(arg0);
|
||||
}
|
||||
|
||||
return JSON.stringify(args);
|
||||
}
|
||||
|
||||
function iPromise<T>(obj: T | Promise<T>): obj is Promise<T> {
|
||||
return typeof (obj as Promise<T>)?.then === 'function';
|
||||
}
|
||||
|
||||
export function gate<T extends (...arg: any) => any>(resolver?: (...args: Parameters<T>) => string) {
|
||||
return (_target: any, key: string, descriptor: PropertyDescriptor) => {
|
||||
let fn: Function | undefined;
|
||||
if (typeof descriptor.value === 'function') {
|
||||
fn = descriptor.value;
|
||||
} else if (typeof descriptor.get === 'function') {
|
||||
fn = descriptor.get;
|
||||
}
|
||||
if (fn === undefined || fn === null) {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
const gateKey = `$gate$${key}`;
|
||||
|
||||
descriptor.value = function (this: any, ...args: any[]) {
|
||||
const prop =
|
||||
args.length === 0 ? gateKey : `${gateKey}$${(resolver ?? defaultResolver)(...(args as Parameters<T>))}`;
|
||||
|
||||
if (!Object.prototype.hasOwnProperty.call(this, prop)) {
|
||||
Object.defineProperty(this, prop, {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: true,
|
||||
value: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
let promise = this[prop];
|
||||
if (promise === undefined) {
|
||||
let result;
|
||||
try {
|
||||
result = fn!.apply(this, args);
|
||||
if (result === undefined || fn === null || !iPromise(result)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
this[prop] = promise = result
|
||||
.then((r: any) => {
|
||||
this[prop] = undefined;
|
||||
return r;
|
||||
})
|
||||
.catch(ex => {
|
||||
this[prop] = undefined;
|
||||
throw ex;
|
||||
});
|
||||
} catch (ex) {
|
||||
this[prop] = undefined;
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
return promise;
|
||||
};
|
||||
};
|
||||
}
|
|
@ -1,504 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { authentication, AuthenticationSession, Disposable, Event, EventEmitter, Range, Uri } from 'vscode';
|
||||
import { graphql } from '@octokit/graphql';
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { ContextStore } from '../contextStore';
|
||||
import { fromGitHubUri } from './fs';
|
||||
import { isSha } from '../extension';
|
||||
import { Iterables } from '../iterables';
|
||||
|
||||
export interface GitHubApiContext {
|
||||
requestRef: string;
|
||||
|
||||
branch: string;
|
||||
sha: string | undefined;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
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),
|
||||
query: ''
|
||||
});
|
||||
}
|
||||
|
||||
export class GitHubApi implements Disposable {
|
||||
private _onDidChangeContext = new EventEmitter<Uri>();
|
||||
get onDidChangeContext(): Event<Uri> {
|
||||
return this._onDidChangeContext.event;
|
||||
}
|
||||
|
||||
private readonly disposable: Disposable;
|
||||
|
||||
constructor(private readonly context: ContextStore<GitHubApiContext>) {
|
||||
this.disposable = Disposable.from(
|
||||
context.onDidChange(e => this._onDidChangeContext.fire(e))
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
}
|
||||
|
||||
private _session: AuthenticationSession | undefined;
|
||||
async ensureAuthenticated() {
|
||||
if (this._session === undefined) {
|
||||
const providers = await authentication.getProviderIds();
|
||||
if (!providers.includes('github')) {
|
||||
await new Promise(resolve => {
|
||||
authentication.onDidChangeAuthenticationProviders(e => {
|
||||
if (e.added.find(provider => provider.id === 'github')) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
this._session = await authentication.getSession('github', ['repo'], { createIfNone: true });
|
||||
}
|
||||
|
||||
return this._session;
|
||||
}
|
||||
|
||||
private _graphql: typeof graphql | undefined;
|
||||
private async graphql() {
|
||||
if (this._graphql === undefined) {
|
||||
const session = await this.ensureAuthenticated();
|
||||
this._graphql = graphql.defaults({
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this._graphql;
|
||||
}
|
||||
|
||||
private _octokit: typeof Octokit | undefined;
|
||||
private async octokit(options?: ConstructorParameters<typeof Octokit>[0]) {
|
||||
if (this._octokit === undefined) {
|
||||
const session = await this.ensureAuthenticated();
|
||||
this._octokit = Octokit.defaults({ auth: `token ${session.accessToken}` });
|
||||
}
|
||||
return new this._octokit(options);
|
||||
}
|
||||
|
||||
async commit(rootUri: Uri, message: string, operations: CommitOperation[]): Promise<string | undefined> {
|
||||
const { owner, repo } = fromGitHubUri(rootUri);
|
||||
|
||||
try {
|
||||
const context = await this.getContext(rootUri);
|
||||
if (context.sha === undefined) {
|
||||
throw new Error(`Cannot commit to Uri(${rootUri.toString(true)}); Invalid context sha`);
|
||||
}
|
||||
|
||||
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,
|
||||
recursive: hasDeletes ? 'true' : undefined,
|
||||
});
|
||||
|
||||
// 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[];
|
||||
|
||||
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 index = updatedTree.findIndex(item => item.path === operation.path);
|
||||
if (index !== -1) {
|
||||
const { path, mode, type } = updatedTree[index];
|
||||
updatedTree.splice(index, 1, { path: path, mode: mode, type: type, 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) {
|
||||
const { path, mode, type } = item;
|
||||
updatedTree.push({ path: path, mode: mode, type: type, content: operation.content });
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatedTreeResp = await github.git.createTree({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
base_tree: hasDeletes ? undefined : treeResp.data.sha,
|
||||
tree: updatedTree
|
||||
});
|
||||
|
||||
const resp = await github.git.createCommit({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
message: message,
|
||||
tree: updatedTreeResp.data.sha,
|
||||
parents: [context.sha]
|
||||
});
|
||||
|
||||
this.updateContext(rootUri, { ...context, sha: resp.data.sha, timestamp: Date.now() });
|
||||
|
||||
// TODO@eamodio need to send a file change for any open files
|
||||
|
||||
await github.git.updateRef({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
ref: `heads/${context.branch}`,
|
||||
sha: resp.data.sha
|
||||
});
|
||||
|
||||
return resp.data.sha;
|
||||
} catch (ex) {
|
||||
console.log(ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
async defaultBranchQuery(uri: Uri) {
|
||||
const { owner, repo } = fromGitHubUri(uri);
|
||||
|
||||
try {
|
||||
const query = `query defaultBranch($owner: String!, $repo: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
defaultBranchRef {
|
||||
name
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const rsp = await this.gqlQuery<{
|
||||
repository: { defaultBranchRef: { name: string; target: { oid: string } } | null | undefined };
|
||||
}>(query, {
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
});
|
||||
return rsp?.repository?.defaultBranchRef?.name ?? undefined;
|
||||
} catch (ex) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async filesQuery(uri: Uri) {
|
||||
const { owner, repo, ref } = fromGitHubUri(uri);
|
||||
|
||||
try {
|
||||
const context = await this.getContext(uri);
|
||||
|
||||
const resp = await (await this.octokit()).git.getTree({
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
recursive: '1',
|
||||
tree_sha: context?.sha ?? ref,
|
||||
});
|
||||
return Iterables.filterMap(resp.data.tree, p => p.type === 'blob' ? p.path : undefined);
|
||||
} catch (ex) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async fsQuery<T>(uri: Uri, innerQuery: string): Promise<T | undefined> {
|
||||
const { owner, repo, path, ref } = fromGitHubUri(uri);
|
||||
|
||||
try {
|
||||
const context = await this.getContext(uri);
|
||||
|
||||
const query = `query fs($owner: String!, $repo: String!, $path: String) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
object(expression: $path) {
|
||||
${innerQuery}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const rsp = await this.gqlQuery<{
|
||||
repository: { object: T | null | undefined };
|
||||
}>(query, {
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
path: `${context.sha ?? ref}:${path}`,
|
||||
});
|
||||
return rsp?.repository?.object ?? undefined;
|
||||
} catch (ex) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async latestCommitQuery(uri: Uri) {
|
||||
const { owner, repo, ref } = fromGitHubUri(uri);
|
||||
|
||||
try {
|
||||
if (ref === 'HEAD') {
|
||||
const query = `query latest($owner: String!, $repo: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
defaultBranchRef {
|
||||
target {
|
||||
oid
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const rsp = await this.gqlQuery<{
|
||||
repository: { defaultBranchRef: { name: string; target: { oid: string } } | null | undefined };
|
||||
}>(query, {
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
});
|
||||
return rsp?.repository?.defaultBranchRef?.target.oid ?? undefined;
|
||||
}
|
||||
|
||||
const query = `query latest($owner: String!, $repo: String!, $ref: String!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
ref(qualifiedName: $ref) {
|
||||
target {
|
||||
oid
|
||||
}
|
||||
}
|
||||
}
|
||||
}`;
|
||||
|
||||
const rsp = await this.gqlQuery<{
|
||||
repository: { ref: { target: { oid: string } } | null | undefined };
|
||||
}>(query, {
|
||||
owner: owner,
|
||||
repo: repo,
|
||||
ref: ref ?? 'HEAD',
|
||||
});
|
||||
return rsp?.repository?.ref?.target.oid ?? undefined;
|
||||
} catch (ex) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async searchQuery(
|
||||
query: string,
|
||||
uri: Uri,
|
||||
options: { maxResults?: number; context?: { before?: number; after?: number } },
|
||||
): Promise<SearchQueryResults> {
|
||||
const { owner, repo, ref } = fromGitHubUri(uri);
|
||||
|
||||
// If we have a specific ref, don't try to search, because GitHub search only works against the default branch
|
||||
if (ref !== 'HEAD') {
|
||||
return { matches: [], limitHit: true };
|
||||
}
|
||||
|
||||
try {
|
||||
const resp = await (await this.octokit({
|
||||
request: {
|
||||
headers: {
|
||||
accept: 'application/vnd.github.v3.text-match+json',
|
||||
},
|
||||
}
|
||||
})).search.code({
|
||||
q: `${query} repo:${owner}/${repo}`,
|
||||
});
|
||||
|
||||
// Since GitHub doesn't return ANY line numbers just fake it at the top of the file 😢
|
||||
const range = new Range(0, 0, 0, 0);
|
||||
|
||||
const matches: SearchQueryMatch[] = [];
|
||||
|
||||
let counter = 0;
|
||||
let match: SearchQueryMatch;
|
||||
for (const item of resp.data.items) {
|
||||
for (const m of (item as typeof item & { text_matches: GitHubSearchTextMatch[] }).text_matches) {
|
||||
counter++;
|
||||
if (options.maxResults !== undefined && counter > options.maxResults) {
|
||||
return { matches: matches, limitHit: true };
|
||||
}
|
||||
|
||||
match = {
|
||||
path: item.path,
|
||||
ranges: [],
|
||||
preview: m.fragment,
|
||||
matches: [],
|
||||
};
|
||||
|
||||
for (const lm of m.matches) {
|
||||
let line = 0;
|
||||
let shartChar = 0;
|
||||
let endChar = 0;
|
||||
for (let i = 0; i < lm.indices[1]; i++) {
|
||||
if (i === lm.indices[0]) {
|
||||
shartChar = endChar;
|
||||
}
|
||||
|
||||
if (m.fragment[i] === '\n') {
|
||||
line++;
|
||||
endChar = 0;
|
||||
} else {
|
||||
endChar++;
|
||||
}
|
||||
}
|
||||
|
||||
match.ranges.push(range);
|
||||
match.matches.push(new Range(line, shartChar, line, endChar));
|
||||
}
|
||||
|
||||
matches.push(match);
|
||||
}
|
||||
}
|
||||
|
||||
return { matches: matches, limitHit: false };
|
||||
} catch (ex) {
|
||||
return { matches: [], limitHit: true };
|
||||
}
|
||||
}
|
||||
|
||||
private async gqlQuery<T>(query: string, variables: { [key: string]: string | number }): Promise<T | undefined> {
|
||||
return (await this.graphql())<T>(query, variables);
|
||||
}
|
||||
|
||||
private readonly pendingContextRequests = new Map<string, Promise<GitHubApiContext>>();
|
||||
async getContext(uri: Uri): Promise<GitHubApiContext> {
|
||||
const rootUri = getGitHubRootUri(uri);
|
||||
|
||||
let pending = this.pendingContextRequests.get(rootUri.toString());
|
||||
if (pending === undefined) {
|
||||
pending = this.getContextCore(rootUri);
|
||||
this.pendingContextRequests.set(rootUri.toString(), pending);
|
||||
}
|
||||
|
||||
try {
|
||||
return await pending;
|
||||
} finally {
|
||||
this.pendingContextRequests.delete(rootUri.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private readonly rootUriToContextMap = new Map<string, GitHubApiContext>();
|
||||
|
||||
private async getContextCore(rootUri: Uri): Promise<GitHubApiContext> {
|
||||
const key = rootUri.toString();
|
||||
let context = this.rootUriToContextMap.get(key);
|
||||
|
||||
// Check if we have a cached a context
|
||||
if (context?.sha !== undefined) {
|
||||
return context;
|
||||
}
|
||||
|
||||
// Check if we have a saved context
|
||||
context = this.context.get(rootUri);
|
||||
if (context?.sha !== undefined) {
|
||||
this.rootUriToContextMap.set(key, context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const { ref } = fromGitHubUri(rootUri);
|
||||
|
||||
// If the requested ref looks like a sha, then use it
|
||||
if (isSha(ref)) {
|
||||
context = { requestRef: ref, branch: ref, sha: ref, timestamp: Date.now() };
|
||||
} else {
|
||||
let branch;
|
||||
if (ref === 'HEAD') {
|
||||
branch = await this.defaultBranchQuery(rootUri);
|
||||
if (branch === undefined) {
|
||||
throw new Error(`Cannot get context for Uri(${rootUri.toString(true)}); unable to get default branch`);
|
||||
}
|
||||
} else {
|
||||
branch = ref;
|
||||
}
|
||||
|
||||
// Query for the latest sha for the give ref
|
||||
const sha = await this.latestCommitQuery(rootUri);
|
||||
context = { requestRef: ref, branch: branch, sha: sha, timestamp: Date.now() };
|
||||
}
|
||||
|
||||
this.updateContext(rootUri, context);
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
private updateContext(rootUri: Uri, context: GitHubApiContext) {
|
||||
this.rootUriToContextMap.set(rootUri.toString(), context);
|
||||
this.context.set(rootUri, context);
|
||||
}
|
||||
}
|
||||
|
||||
interface GitHubSearchTextMatch {
|
||||
object_url: string;
|
||||
object_type: string;
|
||||
property: string;
|
||||
fragment: string;
|
||||
matches: {
|
||||
text: string;
|
||||
indices: number[];
|
||||
}[];
|
||||
}
|
||||
|
||||
interface SearchQueryMatch {
|
||||
path: string;
|
||||
ranges: Range[];
|
||||
preview: string;
|
||||
matches: Range[];
|
||||
}
|
||||
|
||||
interface SearchQueryResults {
|
||||
matches: SearchQueryMatch[];
|
||||
limitHit: boolean;
|
||||
}
|
|
@ -1,332 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import {
|
||||
CancellationToken,
|
||||
Disposable,
|
||||
Event,
|
||||
EventEmitter,
|
||||
FileChangeEvent,
|
||||
FileSearchOptions,
|
||||
FileSearchProvider,
|
||||
FileSearchQuery,
|
||||
FileStat,
|
||||
FileSystemError,
|
||||
FileSystemProvider,
|
||||
FileType,
|
||||
Progress,
|
||||
TextSearchComplete,
|
||||
TextSearchOptions,
|
||||
TextSearchProvider,
|
||||
TextSearchQuery,
|
||||
TextSearchResult,
|
||||
Uri,
|
||||
workspace,
|
||||
} from 'vscode';
|
||||
import * as fuzzySort from 'fuzzysort';
|
||||
import fetch from 'node-fetch';
|
||||
import { GitHubApi } from './api';
|
||||
import { Iterables } from '../iterables';
|
||||
import { getRootUri } from '../extension';
|
||||
|
||||
const emptyDisposable = { dispose: () => { /* noop */ } };
|
||||
const replaceBackslashRegex = /(\/|\\)/g;
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
interface Fuzzysort extends Fuzzysort.Fuzzysort {
|
||||
prepareSlow(target: string): Fuzzysort.Prepared;
|
||||
cleanup(): void;
|
||||
}
|
||||
|
||||
export class GitHubFS implements FileSystemProvider, FileSearchProvider, TextSearchProvider, Disposable {
|
||||
static scheme = 'github';
|
||||
|
||||
private _onDidChangeFile = new EventEmitter<FileChangeEvent[]>();
|
||||
get onDidChangeFile(): Event<FileChangeEvent[]> {
|
||||
return this._onDidChangeFile.event;
|
||||
}
|
||||
|
||||
private readonly disposable: Disposable;
|
||||
private fsCache = new Map<string, Map<string, any>>();
|
||||
|
||||
constructor(private readonly github: GitHubApi) {
|
||||
this.disposable = Disposable.from(
|
||||
workspace.registerFileSystemProvider(GitHubFS.scheme, this, {
|
||||
isCaseSensitive: true,
|
||||
isReadonly: true
|
||||
}),
|
||||
workspace.registerFileSearchProvider(GitHubFS.scheme, this),
|
||||
workspace.registerTextSearchProvider(GitHubFS.scheme, this),
|
||||
github.onDidChangeContext(e => this.fsCache.delete(e.toString()))
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable?.dispose();
|
||||
}
|
||||
|
||||
private getCache(uri: Uri) {
|
||||
const rootUri = getRootUri(uri);
|
||||
if (rootUri === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let cache = this.fsCache.get(rootUri.toString());
|
||||
if (cache === undefined) {
|
||||
cache = new Map<string, any>();
|
||||
this.fsCache.set(rootUri.toString(), cache);
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
//#region FileSystemProvider
|
||||
|
||||
watch(): Disposable {
|
||||
return emptyDisposable;
|
||||
}
|
||||
|
||||
async stat(uri: Uri): Promise<FileStat> {
|
||||
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 };
|
||||
}
|
||||
|
||||
const data = await this.fsQuery<{
|
||||
__typename: string;
|
||||
byteSize: number | undefined;
|
||||
}>(
|
||||
uri,
|
||||
`__typename
|
||||
...on Blob {
|
||||
byteSize
|
||||
}`,
|
||||
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,
|
||||
ctime: 0,
|
||||
mtime: context?.timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
async readDirectory(uri: Uri): Promise<[string, FileType][]> {
|
||||
const data = await this.fsQuery<{
|
||||
entries: { name: string; type: string }[];
|
||||
}>(
|
||||
uri,
|
||||
`... on Tree {
|
||||
entries {
|
||||
name
|
||||
type
|
||||
}
|
||||
}`,
|
||||
this.getCache(uri),
|
||||
);
|
||||
|
||||
return (data?.entries ?? []).map<[string, FileType]>(e => [
|
||||
e.name,
|
||||
typenameToFileType(e.type),
|
||||
]);
|
||||
}
|
||||
|
||||
createDirectory(_uri: Uri): void | Thenable<void> {
|
||||
throw FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
async readFile(uri: Uri): Promise<Uint8Array> {
|
||||
const data = await this.fsQuery<{
|
||||
oid: string;
|
||||
isBinary: boolean;
|
||||
text: string;
|
||||
}>(
|
||||
uri,
|
||||
`... on Blob {
|
||||
oid,
|
||||
isBinary,
|
||||
text
|
||||
}`,
|
||||
);
|
||||
|
||||
if (data?.isBinary) {
|
||||
const { owner, repo, path } = fromGitHubUri(uri);
|
||||
// e.g. https://raw.githubusercontent.com/eamodio/vscode-gitlens/HEAD/images/gitlens-icon.png
|
||||
const downloadUri = uri.with({
|
||||
scheme: 'https',
|
||||
authority: 'raw.githubusercontent.com',
|
||||
path: `/${owner}/${repo}/HEAD/${path}`,
|
||||
});
|
||||
|
||||
return downloadBinary(downloadUri);
|
||||
}
|
||||
|
||||
return textEncoder.encode(data?.text ?? '');
|
||||
}
|
||||
|
||||
async writeFile(_uri: Uri, _content: Uint8Array, _options: { create: boolean, overwrite: boolean }): Promise<void> {
|
||||
throw FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
delete(_uri: Uri, _options: { recursive: boolean }): void | Thenable<void> {
|
||||
throw FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
rename(_oldUri: Uri, _newUri: Uri, _options: { overwrite: boolean }): void | Thenable<void> {
|
||||
throw FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
copy(_source: Uri, _destination: Uri, _options: { overwrite: boolean }): void | Thenable<void> {
|
||||
throw FileSystemError.NoPermissions();
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region FileSearchProvider
|
||||
|
||||
private fileSearchCache = new Map<string, Fuzzysort.Prepared[]>();
|
||||
|
||||
async provideFileSearchResults(
|
||||
query: FileSearchQuery,
|
||||
options: FileSearchOptions,
|
||||
token: CancellationToken,
|
||||
): Promise<Uri[]> {
|
||||
let searchable = this.fileSearchCache.get(options.folder.toString(true));
|
||||
if (searchable === undefined) {
|
||||
const matches = await this.github.filesQuery(options.folder);
|
||||
if (matches === undefined || token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
searchable = [...Iterables.map(matches, m => (fuzzySort as Fuzzysort).prepareSlow(m))];
|
||||
this.fileSearchCache.set(options.folder.toString(true), searchable);
|
||||
}
|
||||
|
||||
if (options.maxResults === undefined || options.maxResults === 0 || options.maxResults >= searchable.length) {
|
||||
const results = searchable.map(m => Uri.joinPath(options.folder, m.target));
|
||||
return results;
|
||||
}
|
||||
|
||||
const results = fuzzySort
|
||||
.go(query.pattern.replace(replaceBackslashRegex, '/'), searchable, {
|
||||
allowTypo: true,
|
||||
limit: options.maxResults,
|
||||
})
|
||||
.map(m => Uri.joinPath(options.folder, m.target));
|
||||
|
||||
(fuzzySort as Fuzzysort).cleanup();
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
//#region TextSearchProvider
|
||||
|
||||
async provideTextSearchResults(
|
||||
query: TextSearchQuery,
|
||||
options: TextSearchOptions,
|
||||
progress: Progress<TextSearchResult>,
|
||||
_token: CancellationToken,
|
||||
): Promise<TextSearchComplete> {
|
||||
const results = await this.github.searchQuery(
|
||||
query.pattern,
|
||||
options.folder,
|
||||
{ maxResults: options.maxResults, context: { before: options.beforeContext, after: options.afterContext } },
|
||||
);
|
||||
if (results === undefined) { return { limitHit: true }; }
|
||||
|
||||
let uri;
|
||||
for (const m of results.matches) {
|
||||
uri = Uri.joinPath(options.folder, m.path);
|
||||
|
||||
progress.report({
|
||||
uri: uri,
|
||||
ranges: m.ranges,
|
||||
preview: {
|
||||
text: m.preview,
|
||||
matches: m.matches,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { limitHit: false };
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
private async fsQuery<T>(uri: Uri, query: string, cache?: Map<string, any>): Promise<T | undefined> {
|
||||
const key = `${uri.toString()}:${getHashCode(query)}`;
|
||||
|
||||
let data = cache?.get(key);
|
||||
if (data !== undefined) {
|
||||
return data as T;
|
||||
}
|
||||
|
||||
data = await this.github.fsQuery<T>(uri, query);
|
||||
cache?.set(key, data);
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadBinary(uri: Uri) {
|
||||
const resp = await fetch(uri.toString());
|
||||
const array = new Uint8Array(await resp.arrayBuffer());
|
||||
return array;
|
||||
}
|
||||
|
||||
function typenameToFileType(typename: string | undefined | null) {
|
||||
if (typename) {
|
||||
typename = typename.toLocaleLowerCase();
|
||||
}
|
||||
|
||||
switch (typename) {
|
||||
case 'blob':
|
||||
return FileType.File;
|
||||
case 'tree':
|
||||
return FileType.Directory;
|
||||
default:
|
||||
return FileType.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
type RepoInfo = { owner: string; repo: string; path: string | undefined; ref: string };
|
||||
export function fromGitHubUri(uri: Uri): RepoInfo {
|
||||
const [, owner, repo, ...rest] = uri.path.split('/');
|
||||
|
||||
let ref;
|
||||
if (uri.authority) {
|
||||
ref = uri.authority;
|
||||
// The casing of HEAD is important for the GitHub api to work
|
||||
if (/HEAD/i.test(ref)) {
|
||||
ref = 'HEAD';
|
||||
}
|
||||
}
|
||||
return { owner: owner, repo: repo, path: rest.join('/'), ref: ref ?? 'HEAD' };
|
||||
}
|
||||
|
||||
function getHashCode(s: string): number {
|
||||
let hash = 0;
|
||||
|
||||
if (s.length === 0) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
let char;
|
||||
const len = s.length;
|
||||
for (let i = 0; i < len; i++) {
|
||||
char = s.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash |= 0; // Convert to 32bit integer
|
||||
}
|
||||
return hash;
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
export namespace Iterables {
|
||||
export function* filterMap<T, TMapped>(
|
||||
source: Iterable<T> | IterableIterator<T>,
|
||||
predicateMapper: (item: T) => TMapped | undefined | null,
|
||||
): Iterable<TMapped> {
|
||||
for (const item of source) {
|
||||
const mapped = predicateMapper(item);
|
||||
if (mapped !== undefined && mapped !== null) {
|
||||
yield mapped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function* map<T, TMapped>(
|
||||
source: Iterable<T> | IterableIterator<T>,
|
||||
mapper: (item: T) => TMapped,
|
||||
): Iterable<TMapped> {
|
||||
for (const item of source) {
|
||||
yield mapper(item);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,177 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { CancellationToken, commands, Disposable, scm, SourceControl, SourceControlResourceGroup, SourceControlResourceState, Uri, window, workspace } from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { IChangeStore } from './changeStore';
|
||||
import { GitHubApi, CommitOperation } from './github/api';
|
||||
import { getRelativePath } from './extension';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
interface ScmProvider {
|
||||
sourceControl: SourceControl,
|
||||
groups: SourceControlResourceGroup[]
|
||||
}
|
||||
|
||||
export class VirtualSCM implements Disposable {
|
||||
private readonly providers: ScmProvider[] = [];
|
||||
|
||||
private disposable: Disposable;
|
||||
|
||||
constructor(
|
||||
private readonly originalScheme: string,
|
||||
private readonly github: GitHubApi,
|
||||
private readonly changeStore: IChangeStore,
|
||||
) {
|
||||
this.registerCommands();
|
||||
|
||||
// TODO@eamodio listen for workspace folder changes
|
||||
for (const folder of workspace.workspaceFolders ?? []) {
|
||||
this.createScmProvider(folder.uri, folder.name);
|
||||
|
||||
for (const operation of changeStore.getChanges(folder.uri)) {
|
||||
this.update(folder.uri, operation.uri);
|
||||
}
|
||||
}
|
||||
|
||||
this.disposable = Disposable.from(
|
||||
changeStore.onDidChange(e => this.update(e.rootUri, e.uri)),
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
}
|
||||
|
||||
private registerCommands() {
|
||||
commands.registerCommand('githubBrowser.commit', (sourceControl: SourceControl | undefined) => {
|
||||
// TODO@eamodio remove this hack once I figure out why the args are missing
|
||||
if (sourceControl === undefined && this.providers.length === 1) {
|
||||
sourceControl = this.providers[0].sourceControl;
|
||||
}
|
||||
|
||||
if (sourceControl === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.commitChanges(sourceControl);
|
||||
});
|
||||
|
||||
commands.registerCommand('githubBrowser.discardChanges', (resourceState: SourceControlResourceState) =>
|
||||
this.discardChanges(resourceState.resourceUri)
|
||||
);
|
||||
|
||||
commands.registerCommand('githubBrowser.openChanges', (resourceState: SourceControlResourceState) =>
|
||||
this.openChanges(resourceState.resourceUri)
|
||||
);
|
||||
|
||||
commands.registerCommand('githubBrowser.openFile', (resourceState: SourceControlResourceState) =>
|
||||
this.openFile(resourceState.resourceUri)
|
||||
);
|
||||
}
|
||||
|
||||
async commitChanges(sourceControl: SourceControl): Promise<void> {
|
||||
const operations = this.changeStore
|
||||
.getChanges(sourceControl.rootUri!)
|
||||
.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, operations);
|
||||
if (sha !== undefined) {
|
||||
this.changeStore.acceptAll(sourceControl.rootUri!);
|
||||
sourceControl.inputBox.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
discardChanges(uri: Uri): Promise<void> {
|
||||
return this.changeStore.discard(uri);
|
||||
}
|
||||
|
||||
openChanges(uri: Uri) {
|
||||
return this.changeStore.openChanges(uri, this.getOriginalResource(uri));
|
||||
}
|
||||
|
||||
openFile(uri: Uri) {
|
||||
return this.changeStore.openFile(uri);
|
||||
}
|
||||
|
||||
private update(rootUri: Uri, uri: Uri) {
|
||||
const folder = workspace.getWorkspaceFolder(uri);
|
||||
if (folder === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = this.createScmProvider(rootUri, folder.name);
|
||||
const group = this.createChangesGroup(provider);
|
||||
group.resourceStates = this.changeStore.getChanges(rootUri).map<SourceControlResourceState>(op => {
|
||||
const rs: SourceControlResourceState = {
|
||||
decorations: {
|
||||
strikeThrough: op.type === 'deleted'
|
||||
},
|
||||
resourceUri: op.uri,
|
||||
command: {
|
||||
command: 'githubBrowser.openChanges',
|
||||
title: 'Open Changes',
|
||||
}
|
||||
};
|
||||
rs.command!.arguments = [rs];
|
||||
return rs;
|
||||
});
|
||||
}
|
||||
|
||||
private createScmProvider(rootUri: Uri, name: string) {
|
||||
let provider = this.providers.find(sc => sc.sourceControl.rootUri?.toString() === rootUri.toString());
|
||||
if (provider === undefined) {
|
||||
const sourceControl = scm.createSourceControl('github', name, rootUri);
|
||||
sourceControl.quickDiffProvider = { provideOriginalResource: uri => this.getOriginalResource(uri) };
|
||||
sourceControl.acceptInputCommand = {
|
||||
command: 'githubBrowser.commit',
|
||||
title: 'Commit',
|
||||
arguments: [sourceControl]
|
||||
};
|
||||
sourceControl.inputBox.placeholder = `Message (Ctrl+Enter to commit '${name}')`;
|
||||
// sourceControl.inputBox.validateInput = value => value ? undefined : 'Invalid commit message';
|
||||
|
||||
provider = { sourceControl: sourceControl, groups: [] };
|
||||
this.createChangesGroup(provider);
|
||||
this.providers.push(provider);
|
||||
}
|
||||
|
||||
return provider;
|
||||
}
|
||||
|
||||
private createChangesGroup(provider: ScmProvider) {
|
||||
let group = provider.groups.find(g => g.id === 'github.changes');
|
||||
if (group === undefined) {
|
||||
group = provider.sourceControl.createResourceGroup('github.changes', 'Changes');
|
||||
provider.groups.push(group);
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
private getOriginalResource(uri: Uri, _token?: CancellationToken): Uri {
|
||||
return uri.with({ scheme: this.originalScheme });
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const textDecoder = new TextDecoder();
|
||||
const textEncoder = new TextEncoder();
|
||||
|
||||
declare let WEBWORKER: boolean;
|
||||
|
||||
export async function sha1(s: string | Uint8Array): Promise<string> {
|
||||
while (true) {
|
||||
try {
|
||||
if (WEBWORKER) {
|
||||
const hash = await globalThis.crypto.subtle.digest({ name: 'sha-1' }, typeof s === 'string' ? textEncoder.encode(s) : s);
|
||||
// Use encodeURIComponent to avoid issues with btoa and Latin-1 characters
|
||||
return globalThis.btoa(encodeURIComponent(textDecoder.decode(hash)));
|
||||
} else {
|
||||
return (await import('crypto')).createHash('sha1').update(s).digest('base64');
|
||||
}
|
||||
} catch (ex) {
|
||||
if (ex instanceof ReferenceError) {
|
||||
(global as any).WEBWORKER = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { Disposable, StatusBarAlignment, StatusBarItem, Uri, window, workspace } from 'vscode';
|
||||
import { ChangeStoreEvent, IChangeStore } from './changeStore';
|
||||
import { GitHubApiContext } from './github/api';
|
||||
import { isSha } from './extension';
|
||||
import { ContextStore, WorkspaceFolderContext } from './contextStore';
|
||||
|
||||
export class StatusBar implements Disposable {
|
||||
private readonly disposable: Disposable;
|
||||
|
||||
private readonly items = new Map<string, StatusBarItem>();
|
||||
|
||||
constructor(
|
||||
private readonly contextStore: ContextStore<GitHubApiContext>,
|
||||
private readonly changeStore: IChangeStore
|
||||
) {
|
||||
this.disposable = Disposable.from(
|
||||
contextStore.onDidChange(this.onContextsChanged, this),
|
||||
changeStore.onDidChange(this.onChanged, this)
|
||||
);
|
||||
|
||||
for (const context of this.contextStore.getForWorkspace()) {
|
||||
this.createOrUpdateStatusBarItem(context);
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposable?.dispose();
|
||||
this.items.forEach(i => i.dispose());
|
||||
}
|
||||
|
||||
private createOrUpdateStatusBarItem(wc: WorkspaceFolderContext<GitHubApiContext>) {
|
||||
let item = this.items.get(wc.folderUri.toString());
|
||||
if (item === undefined) {
|
||||
item = window.createStatusBarItem({
|
||||
id: `githubBrowser.branch:${wc.folderUri.toString()}`,
|
||||
name: `GitHub Browser: ${wc.name}`,
|
||||
alignment: StatusBarAlignment.Left,
|
||||
priority: 1000
|
||||
});
|
||||
}
|
||||
|
||||
if (isSha(wc.context.branch)) {
|
||||
item.text = `$(git-commit) ${wc.context.branch.substr(0, 8)}`;
|
||||
item.tooltip = `${wc.name} \u2022 ${wc.context.branch.substr(0, 8)}`;
|
||||
} else {
|
||||
item.text = `$(git-branch) ${wc.context.branch}`;
|
||||
item.tooltip = `${wc.name} \u2022 ${wc.context.branch}${wc.context.sha ? ` @ ${wc.context.sha?.substr(0, 8)}` : ''}`;
|
||||
}
|
||||
|
||||
const hasChanges = this.changeStore.hasChanges(wc.folderUri);
|
||||
if (hasChanges) {
|
||||
item.text += '*';
|
||||
}
|
||||
|
||||
item.show();
|
||||
|
||||
this.items.set(wc.folderUri.toString(), item);
|
||||
}
|
||||
|
||||
private onContextsChanged(uri: Uri) {
|
||||
const folder = workspace.getWorkspaceFolder(this.contextStore.getWorkspaceResource(uri));
|
||||
if (folder === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = this.contextStore.get(uri);
|
||||
if (context === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.createOrUpdateStatusBarItem({
|
||||
context: context,
|
||||
name: folder.name,
|
||||
folderUri: folder.uri,
|
||||
});
|
||||
}
|
||||
|
||||
private onChanged(e: ChangeStoreEvent) {
|
||||
const item = this.items.get(e.rootUri.toString());
|
||||
if (item !== undefined) {
|
||||
const hasChanges = this.changeStore.hasChanges(e.rootUri);
|
||||
if (hasChanges) {
|
||||
if (!item.text.endsWith('*')) {
|
||||
item.text += '*';
|
||||
}
|
||||
} else {
|
||||
if (item.text.endsWith('*')) {
|
||||
item.text = item.text.substr(0, item.text.length - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
||||
/// <reference path="../../../types/lib.textEncoder.d.ts" />
|
|
@ -1,14 +0,0 @@
|
|||
{
|
||||
"extends": "../shared.tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"lib": [
|
||||
"es2018",
|
||||
"dom"
|
||||
],
|
||||
"outDir": "./out"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
|
@ -1,332 +0,0 @@
|
|||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@octokit/auth-token@^2.4.0":
|
||||
version "2.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.2.tgz#10d0ae979b100fa6b72fa0e8e63e27e6d0dbff8a"
|
||||
integrity sha512-jE/lE/IKIz2v1+/P0u4fJqv0kYwXOTujKemJMFr6FeopsxlIK3+wKDCJGnysg81XID5TgZQbIfuJ5J0lnTiuyQ==
|
||||
dependencies:
|
||||
"@octokit/types" "^5.0.0"
|
||||
|
||||
"@octokit/core@^3.0.0":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/core/-/core-3.1.0.tgz#9c3c9b23f7504668cfa057f143ccbf0c645a0ac9"
|
||||
integrity sha512-yPyQSmxIXLieEIRikk2w8AEtWkFdfG/LXcw1KvEtK3iP0ENZLW/WYQmdzOKqfSaLhooz4CJ9D+WY79C8ZliACw==
|
||||
dependencies:
|
||||
"@octokit/auth-token" "^2.4.0"
|
||||
"@octokit/graphql" "^4.3.1"
|
||||
"@octokit/request" "^5.4.0"
|
||||
"@octokit/types" "^5.0.0"
|
||||
before-after-hook "^2.1.0"
|
||||
universal-user-agent "^5.0.0"
|
||||
|
||||
"@octokit/endpoint@^6.0.1":
|
||||
version "6.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-6.0.3.tgz#dd09b599662d7e1b66374a177ab620d8cdf73487"
|
||||
integrity sha512-Y900+r0gIz+cWp6ytnkibbD95ucEzDSKzlEnaWS52hbCDNcCJYO5mRmWW7HRAnDc7am+N/5Lnd8MppSaTYx1Yg==
|
||||
dependencies:
|
||||
"@octokit/types" "^5.0.0"
|
||||
is-plain-object "^3.0.0"
|
||||
universal-user-agent "^5.0.0"
|
||||
|
||||
"@octokit/graphql@4.5.1", "@octokit/graphql@^4.3.1":
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-4.5.1.tgz#162aed1490320b88ce34775b3f6b8de945529fa9"
|
||||
integrity sha512-qgMsROG9K2KxDs12CO3bySJaYoUu2aic90qpFrv7A8sEBzZ7UFGvdgPKiLw5gOPYEYbS0Xf8Tvf84tJutHPulQ==
|
||||
dependencies:
|
||||
"@octokit/request" "^5.3.0"
|
||||
"@octokit/types" "^5.0.0"
|
||||
universal-user-agent "^5.0.0"
|
||||
|
||||
"@octokit/plugin-paginate-rest@^2.2.0":
|
||||
version "2.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.2.3.tgz#a6ad4377e7e7832fb4bdd9d421e600cb7640ac27"
|
||||
integrity sha512-eKTs91wXnJH8Yicwa30jz6DF50kAh7vkcqCQ9D7/tvBAP5KKkg6I2nNof8Mp/65G0Arjsb4QcOJcIEQY+rK1Rg==
|
||||
dependencies:
|
||||
"@octokit/types" "^5.0.0"
|
||||
|
||||
"@octokit/plugin-request-log@^1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz#eef87a431300f6148c39a7f75f8cfeb218b2547e"
|
||||
integrity sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw==
|
||||
|
||||
"@octokit/plugin-rest-endpoint-methods@4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.0.0.tgz#b02a2006dda8e908c3f8ab381dd5475ef5a810a8"
|
||||
integrity sha512-emS6gysz4E9BNi9IrCl7Pm4kR+Az3MmVB0/DoDCmF4U48NbYG3weKyDlgkrz6Jbl4Mu4nDx8YWZwC4HjoTdcCA==
|
||||
dependencies:
|
||||
"@octokit/types" "^5.0.0"
|
||||
deprecation "^2.3.1"
|
||||
|
||||
"@octokit/request-error@^2.0.0":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-2.0.2.tgz#0e76b83f5d8fdda1db99027ea5f617c2e6ba9ed0"
|
||||
integrity sha512-2BrmnvVSV1MXQvEkrb9zwzP0wXFNbPJij922kYBTLIlIafukrGOb+ABBT2+c6wZiuyWDH1K1zmjGQ0toN/wMWw==
|
||||
dependencies:
|
||||
"@octokit/types" "^5.0.1"
|
||||
deprecation "^2.0.0"
|
||||
once "^1.4.0"
|
||||
|
||||
"@octokit/request@^5.3.0", "@octokit/request@^5.4.0":
|
||||
version "5.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.5.tgz#8df65bd812047521f7e9db6ff118c06ba84ac10b"
|
||||
integrity sha512-atAs5GAGbZedvJXXdjtKljin+e2SltEs48B3naJjqWupYl2IUBbB/CJisyjbNHcKpHzb3E+OYEZ46G8eakXgQg==
|
||||
dependencies:
|
||||
"@octokit/endpoint" "^6.0.1"
|
||||
"@octokit/request-error" "^2.0.0"
|
||||
"@octokit/types" "^5.0.0"
|
||||
deprecation "^2.0.0"
|
||||
is-plain-object "^3.0.0"
|
||||
node-fetch "^2.3.0"
|
||||
once "^1.4.0"
|
||||
universal-user-agent "^5.0.0"
|
||||
|
||||
"@octokit/rest@18.0.0":
|
||||
version "18.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.0.0.tgz#7f401d9ce13530ad743dfd519ae62ce49bcc0358"
|
||||
integrity sha512-4G/a42lry9NFGuuECnua1R1eoKkdBYJap97jYbWDNYBOUboWcM75GJ1VIcfvwDV/pW0lMPs7CEmhHoVrSV5shg==
|
||||
dependencies:
|
||||
"@octokit/core" "^3.0.0"
|
||||
"@octokit/plugin-paginate-rest" "^2.2.0"
|
||||
"@octokit/plugin-request-log" "^1.0.0"
|
||||
"@octokit/plugin-rest-endpoint-methods" "4.0.0"
|
||||
|
||||
"@octokit/types@^5.0.0", "@octokit/types@^5.0.1":
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@octokit/types/-/types-5.0.1.tgz#5459e9a5e9df8565dcc62c17a34491904d71971e"
|
||||
integrity sha512-GorvORVwp244fGKEt3cgt/P+M0MGy4xEDbckw+K5ojEezxyMDgCaYPKVct+/eWQfZXOT7uq0xRpmrl/+hliabA==
|
||||
dependencies:
|
||||
"@types/node" ">= 8"
|
||||
|
||||
"@types/node-fetch@2.5.7":
|
||||
version "2.5.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c"
|
||||
integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
form-data "^3.0.0"
|
||||
|
||||
"@types/node@*", "@types/node@>= 8":
|
||||
version "14.0.14"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.14.tgz#24a0b5959f16ac141aeb0c5b3cd7a15b7c64cbce"
|
||||
integrity sha512-syUgf67ZQpaJj01/tRTknkMNoBBLWJOBODF0Zm4NrXmiSuxjymFrxnTu1QVYRubhVkRcZLYZG8STTwJRdVm/WQ==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
|
||||
|
||||
before-after-hook@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635"
|
||||
integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A==
|
||||
|
||||
combined-stream@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
|
||||
dependencies:
|
||||
delayed-stream "~1.0.0"
|
||||
|
||||
cross-spawn@^6.0.0:
|
||||
version "6.0.5"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
|
||||
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
|
||||
dependencies:
|
||||
nice-try "^1.0.4"
|
||||
path-key "^2.0.1"
|
||||
semver "^5.5.0"
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
delayed-stream@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk=
|
||||
|
||||
deprecation@^2.0.0, deprecation@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919"
|
||||
integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==
|
||||
|
||||
end-of-stream@^1.1.0:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||
integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==
|
||||
dependencies:
|
||||
once "^1.4.0"
|
||||
|
||||
execa@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
|
||||
integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
|
||||
dependencies:
|
||||
cross-spawn "^6.0.0"
|
||||
get-stream "^4.0.0"
|
||||
is-stream "^1.1.0"
|
||||
npm-run-path "^2.0.0"
|
||||
p-finally "^1.0.0"
|
||||
signal-exit "^3.0.0"
|
||||
strip-eof "^1.0.0"
|
||||
|
||||
form-data@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682"
|
||||
integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
fuzzysort@1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/fuzzysort/-/fuzzysort-1.1.4.tgz#a0510206ed44532cbb52cf797bf5a3cb12acd4ba"
|
||||
integrity sha512-JzK/lHjVZ6joAg3OnCjylwYXYVjRiwTY6Yb25LvfpJHK8bjisfnZJ5bY8aVWwTwCXgxPNgLAtmHL+Hs5q1ddLQ==
|
||||
|
||||
get-stream@^4.0.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
|
||||
integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
|
||||
dependencies:
|
||||
pump "^3.0.0"
|
||||
|
||||
is-plain-object@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-3.0.1.tgz#662d92d24c0aa4302407b0d45d21f2251c85f85b"
|
||||
integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==
|
||||
|
||||
is-stream@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
|
||||
|
||||
isexe@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
|
||||
|
||||
macos-release@^2.2.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f"
|
||||
integrity sha512-OHhSbtcviqMPt7yfw5ef5aghS2jzFVKEFyCJndQt2YpSQ9qRVSEv2axSJI1paVThEu+FFGs584h/1YhxjVqajA==
|
||||
|
||||
mime-db@1.44.0:
|
||||
version "1.44.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
|
||||
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
|
||||
|
||||
mime-types@^2.1.12:
|
||||
version "2.1.27"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
|
||||
integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
|
||||
dependencies:
|
||||
mime-db "1.44.0"
|
||||
|
||||
nice-try@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
|
||||
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
|
||||
|
||||
node-fetch@2.6.0, node-fetch@^2.3.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
|
||||
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
|
||||
|
||||
npm-run-path@^2.0.0:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
|
||||
integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
|
||||
dependencies:
|
||||
path-key "^2.0.0"
|
||||
|
||||
once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
os-name@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801"
|
||||
integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg==
|
||||
dependencies:
|
||||
macos-release "^2.2.0"
|
||||
windows-release "^3.1.0"
|
||||
|
||||
p-finally@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
|
||||
integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
|
||||
|
||||
path-key@^2.0.0, path-key@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
|
||||
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
|
||||
|
||||
pump@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
|
||||
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
|
||||
dependencies:
|
||||
end-of-stream "^1.1.0"
|
||||
once "^1.3.1"
|
||||
|
||||
semver@^5.5.0:
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||
|
||||
shebang-command@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
|
||||
integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=
|
||||
dependencies:
|
||||
shebang-regex "^1.0.0"
|
||||
|
||||
shebang-regex@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3"
|
||||
integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=
|
||||
|
||||
signal-exit@^3.0.0:
|
||||
version "3.0.3"
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.3.tgz#a1410c2edd8f077b08b4e253c8eacfcaf057461c"
|
||||
integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==
|
||||
|
||||
strip-eof@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
|
||||
integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
|
||||
|
||||
universal-user-agent@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-5.0.0.tgz#a3182aa758069bf0e79952570ca757de3579c1d9"
|
||||
integrity sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q==
|
||||
dependencies:
|
||||
os-name "^3.1.0"
|
||||
|
||||
vscode-nls@4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-4.1.2.tgz#ca8bf8bb82a0987b32801f9fddfdd2fb9fd3c167"
|
||||
integrity sha512-7bOHxPsfyuCqmP+hZXscLhiHwe7CSuFE4hyhbs22xPIhQ4jv99FcR4eBzfYYVLP356HNFpdvz63FFb/xw6T4Iw==
|
||||
|
||||
which@^1.2.9:
|
||||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
|
||||
integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
|
||||
windows-release@^3.1.0:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.3.1.tgz#cb4e80385f8550f709727287bf71035e209c4ace"
|
||||
integrity sha512-Pngk/RDCaI/DkuHPlGTdIkDiTAnAkyMjoQMZqRsxydNl1qGXNIoZrB7RK8g53F2tEgQBMqQJHQdYZuQEEAu54A==
|
||||
dependencies:
|
||||
execa "^1.0.0"
|
||||
|
||||
wrappy@1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
18
product.json
18
product.json
|
@ -25,7 +25,8 @@
|
|||
"extensionAllowedProposedApi": [
|
||||
"ms-vscode.vscode-js-profile-flame",
|
||||
"ms-vscode.vscode-js-profile-table",
|
||||
"ms-vscode.references-view"
|
||||
"ms-vscode.references-view",
|
||||
"ms-vscode.github-browser"
|
||||
],
|
||||
"builtInExtensions": [
|
||||
{
|
||||
|
@ -117,6 +118,21 @@
|
|||
},
|
||||
"publisherDisplayName": "Microsoft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ms-vscode.github-browser",
|
||||
"version": "0.0.1",
|
||||
"repo": "https://github.com/Microsoft/vscode-github-browser",
|
||||
"metadata": {
|
||||
"id": "c1bcff4b-4ecb-466e-b8f6-b02788b5fb5a",
|
||||
"publisherId": {
|
||||
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
|
||||
"publisherName": "ms-vscode",
|
||||
"displayName": "Microsoft",
|
||||
"flags": "verified"
|
||||
},
|
||||
"publisherDisplayName": "Microsoft"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -275,7 +275,8 @@ async function handleRoot(req, res) {
|
|||
}
|
||||
|
||||
const [owner, repo, ...branch] = gh.split('/', 3);
|
||||
folderUri = { scheme: 'github', authority: branch.join('/') || 'HEAD', path: `/${owner}/${repo}` };
|
||||
const ref = branch.join('/');
|
||||
folderUri = { scheme: 'github', authority: `${owner}+${repo}${ref ? `+${ref}` : ''}`, path: '/' };
|
||||
} else {
|
||||
let cs = qs.get('cs');
|
||||
if (cs) {
|
||||
|
@ -284,7 +285,8 @@ async function handleRoot(req, res) {
|
|||
}
|
||||
|
||||
const [owner, repo, ...branch] = cs.split('/');
|
||||
folderUri = { scheme: 'codespace', authority: branch.join('/') || 'HEAD', path: `/${owner}/${repo}` };
|
||||
const ref = branch.join('/');
|
||||
folderUri = { scheme: 'codespace', authority: `${owner}+${repo}${ref ? `+${ref}` : ''}`, path: '/' };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,8 @@ if (isWeb) {
|
|||
nameShort: 'VSCode Web Dev',
|
||||
urlProtocol: 'code-oss',
|
||||
extensionAllowedProposedApi: [
|
||||
'ms-vscode.references-view'
|
||||
'ms-vscode.references-view',
|
||||
'ms-vscode.github-browser'
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue