Merge from vscode d5e9aa0227e057a60c82568bf31c04730dc15dcd (#11276)

* Merge from vscode d5e9aa0227e057a60c82568bf31c04730dc15dcd

* fix tests
This commit is contained in:
Anthony Dresser
2020-07-09 13:40:02 -07:00
committed by GitHub
parent f812e4fa5d
commit 047c221834
135 changed files with 1772 additions and 954 deletions

View File

@@ -1878,7 +1878,7 @@
"dependencies": {
"byline": "^5.0.0",
"file-type": "^7.2.0",
"iconv-lite-umd": "0.6.5",
"iconv-lite-umd": "0.6.7",
"jschardet": "2.1.1",
"vscode-extension-telemetry": "0.1.1",
"vscode-nls": "^4.0.0",

View File

@@ -7,7 +7,6 @@ import { Model } from '../model';
import { GitExtension, Repository, API } from './git';
import { ApiRepository, ApiImpl } from './api1';
import { Event, EventEmitter } from 'vscode';
import { latchEvent } from '../util';
export function deprecated(_target: any, key: string, descriptor: any): void {
if (typeof descriptor.value !== 'function') {
@@ -26,14 +25,20 @@ export class GitExtensionImpl implements GitExtension {
enabled: boolean = false;
private _onDidChangeEnablement = new EventEmitter<boolean>();
readonly onDidChangeEnablement: Event<boolean> = latchEvent(this._onDidChangeEnablement.event);
readonly onDidChangeEnablement: Event<boolean> = this._onDidChangeEnablement.event;
private _model: Model | undefined = undefined;
set model(model: Model | undefined) {
this._model = model;
this.enabled = !!model;
const enabled = !!model;
if (this.enabled === enabled) {
return;
}
this.enabled = enabled;
this._onDidChangeEnablement.fire(this.enabled);
}
@@ -73,4 +78,4 @@ export class GitExtensionImpl implements GitExtension {
return new ApiImpl(this._model);
}
}
}

View File

@@ -176,8 +176,7 @@ export async function activate(context: ExtensionContext): Promise<GitExtension>
return result;
}
// @ts-expect-error
async function checkGitVersion(info: IGit): Promise<void> {
async function checkGitv1(info: IGit): Promise<void> {
const config = workspace.getConfiguration('git');
const shouldIgnore = config.get<boolean>('ignoreLegacyWarning') === true;
@@ -204,3 +203,27 @@ async function checkGitVersion(info: IGit): Promise<void> {
await config.update('ignoreLegacyWarning', true, true);
}
}
async function checkGitWindows(info: IGit): Promise<void> {
if (!/^2\.(25|26)\./.test(info.version)) {
return;
}
const update = localize('updateGit', "Update Git");
const choice = await window.showWarningMessage(
localize('git2526', "There are known issues with the installed Git {0}. Please update to Git >= 2.27 for the git features to work correctly.", info.version),
update
);
if (choice === update) {
commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/'));
}
}
// @ts-expect-error
async function checkGitVersion(info: IGit): Promise<void> {
await checkGitv1(info);
if (process.platform === 'win32') {
await checkGitWindows(info);
}
}

View File

@@ -865,7 +865,7 @@ export class Repository implements Disposable {
}
async getInputTemplate(): Promise<string> {
const commitMessage = (await Promise.all([this.repository.getMergeMessage(), this.repository.getSquashMessage()])).find(msg => msg !== undefined);
const commitMessage = (await Promise.all([this.repository.getMergeMessage(), this.repository.getSquashMessage()])).find(msg => !!msg);
if (commitMessage) {
return commitMessage;

View File

@@ -44,18 +44,6 @@ export function filterEvent<T>(event: Event<T>, filter: (e: T) => boolean): Even
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables);
}
export function latchEvent<T>(event: Event<T>): Event<T> {
let firstCall = true;
let cache: T;
return filterEvent(event, value => {
let shouldEmit = firstCall || value !== cache;
firstCall = false;
cache = value;
return shouldEmit;
});
}
export function anyEvent<T>(...events: Event<T>[]): Event<T> {
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
const result = combinedDisposable(events.map(event => event(i => listener.call(thisArgs, i))));

View File

@@ -425,10 +425,10 @@ https-proxy-agent@^2.2.1:
agent-base "^4.3.0"
debug "^3.1.0"
iconv-lite-umd@0.6.5:
version "0.6.5"
resolved "https://registry.yarnpkg.com/iconv-lite-umd/-/iconv-lite-umd-0.6.5.tgz#6a1f621a3b4d125f72feff813a9839e1ebd6c722"
integrity sha512-WDegH4al+e3n3jTOStRvm+jzDA3JMUQGgzdAsMxAgcgB0Oi72HjfdsoX08ieKsy3rKexXVjWZr41aOIUaCZnMg==
iconv-lite-umd@0.6.7:
version "0.6.7"
resolved "https://registry.yarnpkg.com/iconv-lite-umd/-/iconv-lite-umd-0.6.7.tgz#ee437e34b30f15dc00ec93ea65065e672770777c"
integrity sha512-DT90zb7wL1B3I6DmYUMcfJeVdY19XigzDj5AtXbXEw9Jfi0+AVAxfn7ytvY7Xhr+GFn7nd7hPonapC37oo7iAQ==
inflight@^1.0.4:
version "1.0.6"

View File

@@ -21,6 +21,11 @@
"main": "./out/extension.js",
"contributes": {
"commands": [
{
"command": "githubBrowser.openRepository",
"title": "Open GitHub Repository...",
"category": "GitHub Browser"
},
{
"command": "githubBrowser.commit",
"title": "Commit",
@@ -48,6 +53,10 @@
],
"menus": {
"commandPalette": [
{
"command": "githubBrowser.openRepository",
"when": "config.githubBrowser.openRepository"
},
{
"command": "githubBrowser.commit",
"when": "false"

View File

@@ -47,31 +47,17 @@ function fromSerialized(operations: StoredOperation): Operation {
return { ...operations, uri: Uri.parse(operations.uri) };
}
interface CreatedFileChangeStoreEvent {
type: 'created';
export interface ChangeStoreEvent {
type: 'created' | 'changed' | 'deleted';
rootUri: Uri;
uri: Uri;
}
interface ChangedFileChangeStoreEvent {
type: 'changed';
rootUri: Uri;
uri: Uri;
}
interface DeletedFileChangeStoreEvent {
type: 'deleted';
rootUri: Uri;
uri: Uri;
}
type ChangeStoreEvent = CreatedFileChangeStoreEvent | ChangedFileChangeStoreEvent | DeletedFileChangeStoreEvent;
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)
uri: uri ?? (typeof operation.uri === 'string' ? Uri.parse(operation.uri) : operation.uri),
};
}
@@ -82,6 +68,8 @@ export interface IChangeStore {
discard(uri: Uri): Promise<void>;
discardAll(rootUri: Uri): Promise<void>;
hasChanges(rootUri: Uri): boolean;
getChanges(rootUri: Uri): Operation[];
getContent(uri: Uri): string | undefined;
@@ -116,9 +104,15 @@ export class ChangeStore implements IChangeStore, IWritableChangeStore {
await this.saveWorkingOperations(rootUri, undefined);
const events: ChangeStoreEvent[] = [];
for (const operation of operations) {
await this.discardWorkingContent(operation.uri);
this._onDidChange.fire(toChangeStoreEvent(operation, rootUri));
events.push(toChangeStoreEvent(operation, rootUri));
}
for (const e of events) {
this._onDidChange.fire(e);
}
}
@@ -143,7 +137,7 @@ export class ChangeStore implements IChangeStore, IWritableChangeStore {
this._onDidChange.fire({
type: operation.type === 'created' ? 'deleted' : operation.type === 'deleted' ? 'created' : 'changed',
rootUri: rootUri,
uri: uri
uri: uri,
});
}
@@ -152,9 +146,15 @@ export class ChangeStore implements IChangeStore, IWritableChangeStore {
await this.saveWorkingOperations(rootUri, undefined);
const events: ChangeStoreEvent[] = [];
for (const operation of operations) {
await this.discardWorkingContent(operation.uri);
this._onDidChange.fire(toChangeStoreEvent(operation, rootUri));
events.push(toChangeStoreEvent(operation, rootUri));
}
for (const e of events) {
this._onDidChange.fire(e);
}
}

View File

@@ -4,9 +4,13 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Event, EventEmitter, Memento, Uri } from 'vscode';
import { Event, EventEmitter, Memento, Uri, workspace } from 'vscode';
export const contextKeyPrefix = 'github.context|';
export interface WorkspaceFolderContext<T> {
context: T;
name: string;
folderUri: Uri;
}
export class ContextStore<T> {
private _onDidChange = new EventEmitter<Uri>();
@@ -14,23 +18,36 @@ export class ContextStore<T> {
return this._onDidChange.event;
}
constructor(private readonly memento: Memento, private readonly scheme: string) { }
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>(`${contextKeyPrefix}${uri.toString()}`);
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) {
if (uri.scheme !== this.scheme) {
throw new Error(`Invalid context scheme: ${uri.scheme}`);
}
await this.memento.update(`${contextKeyPrefix}${uri.toString()}`, context);
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 });
}
}

View File

@@ -3,48 +3,50 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExtensionContext, Uri, workspace } from 'vscode';
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;
const repositoryRegex = /^(?:(?:https:\/\/)?github.com\/)?([^\/]+)\/([^\/]+?)(?:\/|.git|$)/i;
export function activate(context: ExtensionContext) {
const contextStore = new ContextStore<GitHubApiContext>(context.workspaceState, GitHubFS.scheme);
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', GitHubFS.scheme, contextStore, changeStore, gitHubFS);
const virtualFS = new VirtualFS('codespace', contextStore, changeStore, gitHubFS);
context.subscriptions.push(
githubApi,
gitHubFS,
virtualFS,
new VirtualSCM(GitHubFS.scheme, githubApi, changeStore)
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'
// });
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;
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');
// }
// }
// });
const uri = Uri.parse(`codespace://HEAD/${owner}/${repo}`);
openWorkspace(uri, repo, 'currentWindow');
}
}
});
}
export function getRelativePath(rootUri: Uri, uri: Uri) {
@@ -63,11 +65,16 @@ export function isDescendent(folderPath: string, filePath: string) {
return folderPath.length === 0 || filePath.startsWith(folderPath.endsWith('/') ? folderPath : `${folderPath}/`);
}
// 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 });
// }
const shaRegex = /^[0-9a-f]{40}$/;
export function isSha(ref: string) {
return shaRegex.test(ref);
}
// return commands.executeCommand('vscode.openFolder', uri, location === 'newWindow');
// }
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');
}

View File

@@ -43,26 +43,22 @@ export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSe
constructor(
readonly scheme: string,
private readonly originalScheme: string,
contextStore: ContextStore<GitHubApiContext>,
private readonly contextStore: ContextStore<GitHubApiContext>,
private readonly changeStore: IWritableChangeStore,
private readonly fs: FileSystemProvider & FileSearchProvider & TextSearchProvider
) {
// TODO@eamodio listen for workspace folder changes
for (const folder of workspace.workspaceFolders ?? []) {
const uri = this.getOriginalResource(folder.uri);
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 (contextStore.get(uri) !== undefined && !changeStore.hasChanges(folder.uri)) {
contextStore.delete(uri);
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.registerFileSystemProvider(scheme, this, { isCaseSensitive: true }),
workspace.registerFileSearchProvider(scheme, this),
workspace.registerTextSearchProvider(scheme, this),
changeStore.onDidChange(e => {
@@ -86,11 +82,11 @@ export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSe
}
private getOriginalResource(uri: Uri): Uri {
return uri.with({ scheme: this.originalScheme });
return this.contextStore.getOriginalResource(uri);
}
private getVirtualResource(uri: Uri): Uri {
return uri.with({ scheme: this.scheme });
private getWorkspaceResource(uri: Uri): Uri {
return this.contextStore.getWorkspaceResource(uri);
}
//#region FileSystemProvider
@@ -211,7 +207,7 @@ export class VirtualFS implements FileSystemProvider, FileSearchProvider, TextSe
return this.fs.provideTextSearchResults(
query,
{ ...options, folder: this.getOriginalResource(options.folder) },
{ report: (result: TextSearchResult) => progress.report({ ...result, uri: this.getVirtualResource(result.uri) }) },
{ report: (result: TextSearchResult) => progress.report({ ...result, uri: this.getWorkspaceResource(result.uri) }) },
token
);
}

View File

@@ -6,14 +6,16 @@
import { authentication, AuthenticationSession, Disposable, Event, EventEmitter, Range, Uri } from 'vscode';
import { graphql } from '@octokit/graphql';
import { Octokit } from '@octokit/rest';
import { fromGitHubUri } from './fs';
import { ContextStore } from '../contextStore';
import { fromGitHubUri } from './fs';
import { isSha } from '../extension';
import { Iterables } from '../iterables';
export const shaRegex = /^[0-9a-f]{40}$/;
export interface GitHubApiContext {
sha: string;
requestRef: string;
branch: string;
sha: string | undefined;
timestamp: number;
}
@@ -110,19 +112,12 @@ export class GitHubApi implements Disposable {
}
async commit(rootUri: Uri, message: string, operations: CommitOperation[]): Promise<string | undefined> {
let { owner, repo, ref } = fromGitHubUri(rootUri);
const { owner, repo } = fromGitHubUri(rootUri);
try {
if (ref === undefined || ref === 'HEAD') {
ref = await this.defaultBranchQuery(rootUri);
if (ref === undefined) {
throw new Error('Cannot commit — invalid ref');
}
}
const context = await this.getContext(rootUri);
if (context.sha === undefined) {
throw new Error('Cannot commit — invalid context');
throw new Error(`Cannot commit to Uri(${rootUri.toString(true)}); Invalid context sha`);
}
const hasDeletes = operations.some(op => op.type === 'deleted');
@@ -204,14 +199,14 @@ export class GitHubApi implements Disposable {
parents: [context.sha]
});
this.updateContext(rootUri, { sha: resp.data.sha, timestamp: Date.now() });
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/${ref}`,
ref: `heads/${context.branch}`,
sha: resp.data.sha
});
@@ -256,7 +251,7 @@ export class GitHubApi implements Disposable {
owner: owner,
repo: repo,
recursive: '1',
tree_sha: context?.sha ?? ref ?? 'HEAD',
tree_sha: context?.sha ?? ref,
});
return Iterables.filterMap(resp.data.tree, p => p.type === 'blob' ? p.path : undefined);
} catch (ex) {
@@ -283,7 +278,7 @@ export class GitHubApi implements Disposable {
}>(query, {
owner: owner,
repo: repo,
path: `${context.sha ?? ref ?? 'HEAD'}:${path}`,
path: `${context.sha ?? ref}:${path}`,
});
return rsp?.repository?.object ?? undefined;
} catch (ex) {
@@ -295,7 +290,7 @@ export class GitHubApi implements Disposable {
const { owner, repo, ref } = fromGitHubUri(uri);
try {
if (ref === undefined || ref === 'HEAD') {
if (ref === 'HEAD') {
const query = `query latest($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
defaultBranchRef {
@@ -322,6 +317,7 @@ export class GitHubApi implements Disposable {
oid
}
}
}
}`;
const rsp = await this.gqlQuery<{
@@ -345,7 +341,7 @@ export class GitHubApi implements Disposable {
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 === undefined) {
if (ref !== 'HEAD') {
return { matches: [], limitHit: true };
}
@@ -436,29 +432,46 @@ export class GitHubApi implements Disposable {
private readonly rootUriToContextMap = new Map<string, GitHubApiContext>();
private async getContextCore(rootUri: Uri): Promise<GitHubApiContext> {
let context = this.rootUriToContextMap.get(rootUri.toString());
if (context === undefined) {
const { ref } = fromGitHubUri(rootUri);
if (ref !== undefined && shaRegex.test(ref)) {
context = { sha: ref, timestamp: Date.now() };
} else {
context = this.context.get(rootUri);
if (context?.sha === undefined) {
const sha = await this.latestCommitQuery(rootUri);
if (sha !== undefined) {
context = { sha: sha, timestamp: Date.now() };
} else {
context = undefined;
}
}
}
const key = rootUri.toString();
let context = this.rootUriToContextMap.get(key);
if (context !== undefined) {
this.updateContext(rootUri, context);
}
// Check if we have a cached a context
if (context?.sha !== undefined) {
return context;
}
return context ?? { sha: rootUri.authority, timestamp: Date.now() };
// 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) {

View File

@@ -299,7 +299,7 @@ function typenameToFileType(typename: string | undefined | null) {
}
}
type RepoInfo = { owner: string; repo: string; path: string | undefined; ref?: string };
type RepoInfo = { owner: string; repo: string; path: string | undefined; ref: string };
export function fromGitHubUri(uri: Uri): RepoInfo {
const [, owner, repo, ...rest] = uri.path.split('/');
@@ -311,7 +311,7 @@ export function fromGitHubUri(uri: Uri): RepoInfo {
ref = 'HEAD';
}
}
return { owner: owner, repo: repo, path: rest.join('/'), ref: ref };
return { owner: owner, repo: repo, path: rest.join('/'), ref: ref ?? 'HEAD' };
}
function getHashCode(s: string): number {

View File

@@ -32,17 +32,15 @@ export class VirtualSCM implements Disposable {
// 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)),
);
for (const { uri } of workspace.workspaceFolders ?? []) {
for (const operation of changeStore.getChanges(uri)) {
this.update(uri, operation.uri);
}
}
}
dispose() {
@@ -50,7 +48,18 @@ export class VirtualSCM implements Disposable {
}
private registerCommands() {
commands.registerCommand('githubBrowser.commit', (...args: any[]) => this.commitChanges(args[0]));
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)

View File

@@ -0,0 +1,99 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. 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);
}
}
}
}
}

View File

@@ -6,9 +6,10 @@
import * as vscode from 'vscode';
import { API as GitAPI } from './typings/git';
import { publishRepository } from './publish';
import { combinedDisposable } from './util';
export function registerCommands(gitAPI: GitAPI): vscode.Disposable[] {
const disposables = [];
export function registerCommands(gitAPI: GitAPI): vscode.Disposable {
const disposables: vscode.Disposable[] = [];
disposables.push(vscode.commands.registerCommand('github.publish', async () => {
try {
@@ -18,5 +19,5 @@ export function registerCommands(gitAPI: GitAPI): vscode.Disposable[] {
}
}));
return disposables;
return combinedDisposable(disposables);
}

View File

@@ -3,23 +3,41 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { Disposable, ExtensionContext, extensions } from 'vscode';
import { GithubRemoteSourceProvider } from './remoteSourceProvider';
import { GitExtension } from './typings/git';
import { registerCommands } from './commands';
import { GithubCredentialProviderManager } from './credentialProvider';
import { dispose, combinedDisposable } from './util';
export async function activate(context: vscode.ExtensionContext) {
const gitExtension = vscode.extensions.getExtension<GitExtension>('vscode.git')!.exports;
export function activate(context: ExtensionContext): void {
const disposables = new Set<Disposable>();
context.subscriptions.push(combinedDisposable(disposables));
try {
const gitAPI = gitExtension.getAPI(1);
const init = () => {
try {
const gitAPI = gitExtension.getAPI(1);
context.subscriptions.push(...registerCommands(gitAPI));
context.subscriptions.push(gitAPI.registerRemoteSourceProvider(new GithubRemoteSourceProvider(gitAPI)));
context.subscriptions.push(new GithubCredentialProviderManager(gitAPI));
} catch (err) {
console.error('Could not initialize GitHub extension');
console.warn(err);
}
disposables.add(registerCommands(gitAPI));
disposables.add(gitAPI.registerRemoteSourceProvider(new GithubRemoteSourceProvider(gitAPI)));
disposables.add(new GithubCredentialProviderManager(gitAPI));
} catch (err) {
console.error('Could not initialize GitHub extension');
console.warn(err);
}
};
const onDidChangeGitExtensionEnablement = (enabled: boolean) => {
if (!enabled) {
dispose(disposables);
disposables.clear();
} else {
init();
}
};
const gitExtension = extensions.getExtension<GitExtension>('vscode.git')!.exports;
context.subscriptions.push(gitExtension.onDidChangeEnablement(onDidChangeGitExtensionEnablement));
onDidChangeGitExtensionEnablement(gitExtension.enabled);
}

View File

@@ -5,10 +5,10 @@
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as path from 'path';
import { promises as fs } from 'fs';
import { API as GitAPI, Repository } from './typings/git';
import { getOctokit } from './auth';
import { TextEncoder } from 'util';
import { basename } from 'path';
const localize = nls.loadMessageBundle();
@@ -28,10 +28,12 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
return;
}
let folder: vscode.WorkspaceFolder;
let folder: vscode.Uri;
if (vscode.workspace.workspaceFolders.length === 1) {
folder = vscode.workspace.workspaceFolders[0];
if (repository) {
folder = repository.rootUri;
} else if (vscode.workspace.workspaceFolders.length === 1) {
folder = vscode.workspace.workspaceFolders[0].uri;
} else {
const picks = vscode.workspace.workspaceFolders.map(folder => ({ label: folder.name, folder }));
const placeHolder = localize('pick folder', "Pick a folder to publish to GitHub");
@@ -41,14 +43,14 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
return;
}
folder = pick.folder;
folder = pick.folder.uri;
}
let quickpick = vscode.window.createQuickPick<vscode.QuickPickItem & { repo?: string, auth?: 'https' | 'ssh' }>();
quickpick.ignoreFocusOut = true;
quickpick.placeholder = 'Repository Name';
quickpick.value = folder.name;
quickpick.value = basename(folder.fsPath);
quickpick.show();
quickpick.busy = true;
@@ -97,37 +99,49 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
return;
}
quickpick = vscode.window.createQuickPick();
quickpick.placeholder = localize('ignore', "Select which files should be included in the repository.");
quickpick.canSelectMany = true;
quickpick.show();
if (!repository) {
const gitignore = vscode.Uri.joinPath(folder, '.gitignore');
let shouldGenerateGitignore = false;
try {
quickpick.busy = true;
const repositoryPath = folder.uri.fsPath;
const currentPath = path.join(repositoryPath);
const children = await fs.readdir(currentPath);
quickpick.items = children.map(name => ({ label: name }));
quickpick.selectedItems = quickpick.items;
quickpick.busy = false;
const result = await Promise.race([
new Promise<readonly vscode.QuickPickItem[]>(c => quickpick.onDidAccept(() => c(quickpick.selectedItems))),
new Promise<undefined>(c => quickpick.onDidHide(() => c(undefined)))
]);
if (!result) {
return;
try {
await vscode.workspace.fs.stat(gitignore);
} catch (err) {
shouldGenerateGitignore = true;
}
const ignored = new Set(children);
result.forEach(c => ignored.delete(c.label));
if (shouldGenerateGitignore) {
quickpick = vscode.window.createQuickPick();
quickpick.placeholder = localize('ignore', "Select which files should be included in the repository.");
quickpick.canSelectMany = true;
quickpick.show();
const raw = [...ignored].map(i => `/${i}`).join('\n');
await fs.writeFile(path.join(repositoryPath, '.gitignore'), raw, 'utf8');
} finally {
quickpick.dispose();
try {
quickpick.busy = true;
const children = (await vscode.workspace.fs.readDirectory(folder)).map(([name]) => name);
quickpick.items = children.map(name => ({ label: name }));
quickpick.selectedItems = quickpick.items;
quickpick.busy = false;
const result = await Promise.race([
new Promise<readonly vscode.QuickPickItem[]>(c => quickpick.onDidAccept(() => c(quickpick.selectedItems))),
new Promise<undefined>(c => quickpick.onDidHide(() => c(undefined)))
]);
if (!result) {
return;
}
const ignored = new Set(children);
result.forEach(c => ignored.delete(c.label));
const raw = [...ignored].map(i => `/${i}`).join('\n');
const encoder = new TextEncoder();
await vscode.workspace.fs.writeFile(gitignore, encoder.encode(raw));
} finally {
quickpick.dispose();
}
}
}
const githubRepository = await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, cancellable: false, title: 'Publish to GitHub' }, async progress => {
@@ -143,7 +157,7 @@ export async function publishRepository(gitAPI: GitAPI, repository?: Repository)
progress.report({ message: 'Creating first commit', increment: 25 });
if (!repository) {
repository = await gitAPI.init(folder.uri) || undefined;
repository = await gitAPI.init(folder) || undefined;
if (!repository) {
return;

View File

@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
export function dispose(arg: vscode.Disposable | Iterable<vscode.Disposable>): void {
if (arg instanceof vscode.Disposable) {
arg.dispose();
} else {
for (const disposable of arg) {
disposable.dispose();
}
}
}
export function combinedDisposable(disposables: Iterable<vscode.Disposable>): vscode.Disposable {
return {
dispose() {
dispose(disposables);
}
};
}

View File

@@ -3,7 +3,7 @@
"version": "0.0.1",
"description": "Dependencies shared by all extensions",
"dependencies": {
"typescript": "3.9.5"
"typescript": "3.9.6"
},
"scripts": {
"postinstall": "node ./postinstall"

View File

@@ -122,5 +122,11 @@
"foreground": "#CBEDCB",
}
}
]
],
"semanticTokenColors": {
"newOperator": "#FFFFFF",
"stringLiteral": "#ce9178",
"customLiteral": "#DCDCAA",
"numberLiteral": "#b5cea8",
}
}

View File

@@ -20,7 +20,7 @@
"statusBarItem.remoteBackground": "#16825D",
"sideBarSectionHeader.background": "#0000",
"sideBarSectionHeader.border": "#61616130",
"notebook.cellFocusBackground": "#c8ddf150",
"notebook.focusedCellBackground": "#c8ddf150",
"notebook.cellBorderColor": "#dae3e9",
"notebook.outputContainerBackgroundColor": "#c8ddf150",
"notebook.focusedCellShadow": "#00315040"

View File

@@ -76,10 +76,10 @@ rimraf@^3.0.2:
dependencies:
glob "^7.1.3"
typescript@3.9.5:
version "3.9.5"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36"
integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==
typescript@3.9.6:
version "3.9.6"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.6.tgz#8f3e0198a34c3ae17091b35571d3afd31999365a"
integrity sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==
wrappy@1:
version "1.0.2"