mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-31 17:23:31 -05:00
Initial VS Code 1.19 source merge (#571)
* Initial 1.19 xcopy * Fix yarn build * Fix numerous build breaks * Next batch of build break fixes * More build break fixes * Runtime breaks * Additional post merge fixes * Fix windows setup file * Fix test failures. * Update license header blocks to refer to source eula
This commit is contained in:
42
extensions/git/src/api.ts
Normal file
42
extensions/git/src/api.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Model } from './model';
|
||||
import { SourceControlInputBox, Uri } from 'vscode';
|
||||
|
||||
export interface InputBox {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface Repository {
|
||||
readonly rootUri: Uri;
|
||||
readonly inputBox: InputBox;
|
||||
}
|
||||
|
||||
export interface API {
|
||||
getRepositories(): Promise<Repository[]>;
|
||||
}
|
||||
|
||||
export function createApi(modelPromise: Promise<Model>) {
|
||||
return {
|
||||
async getRepositories(): Promise<Repository[]> {
|
||||
const model = await modelPromise;
|
||||
|
||||
return model.repositories.map(repository => ({
|
||||
rootUri: Uri.file(repository.root),
|
||||
inputBox: {
|
||||
set value(value: string) {
|
||||
repository.inputBox.value = value;
|
||||
},
|
||||
get value(): string {
|
||||
return repository.inputBox.value;
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -34,8 +34,8 @@ function main(argv: string[]): void {
|
||||
return fatal('Skip fetch commands');
|
||||
}
|
||||
|
||||
const output = process.env['VSCODE_GIT_ASKPASS_PIPE'];
|
||||
const socketPath = process.env['VSCODE_GIT_ASKPASS_HANDLE'];
|
||||
const output = process.env['VSCODE_GIT_ASKPASS_PIPE'] as string;
|
||||
const socketPath = process.env['VSCODE_GIT_ASKPASS_HANDLE'] as string;
|
||||
const request = argv[2];
|
||||
const host = argv[4].substring(1, argv[4].length - 2);
|
||||
const opts: http.RequestOptions = {
|
||||
|
||||
@@ -28,7 +28,7 @@ function getIPCHandlePath(nonce: string): string {
|
||||
}
|
||||
|
||||
if (process.env['XDG_RUNTIME_DIR']) {
|
||||
return path.join(process.env['XDG_RUNTIME_DIR'], `vscode-git-askpass-${nonce}.sock`);
|
||||
return path.join(process.env['XDG_RUNTIME_DIR'] as string, `vscode-git-askpass-${nonce}.sock`);
|
||||
}
|
||||
|
||||
return path.join(os.tmpdir(), `vscode-git-askpass-${nonce}.sock`);
|
||||
|
||||
@@ -5,14 +5,22 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { workspace, Disposable, EventEmitter } from 'vscode';
|
||||
import { workspace, Disposable, EventEmitter, Memento, window, MessageItem, ConfigurationTarget, commands, Uri } from 'vscode';
|
||||
import { GitErrorCodes } from './git';
|
||||
import { Repository } from './repository';
|
||||
import { eventToPromise, filterEvent } from './util';
|
||||
import { Repository, Operation } from './repository';
|
||||
import { eventToPromise, filterEvent, onceEvent } from './util';
|
||||
import * as nls from 'vscode-nls';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
function isRemoteOperation(operation: Operation): boolean {
|
||||
return operation === Operation.Pull || operation === Operation.Push || operation === Operation.Sync || operation === Operation.Fetch;
|
||||
}
|
||||
|
||||
export class AutoFetcher {
|
||||
|
||||
private static Period = 3 * 60 * 1000 /* three minutes */;
|
||||
private static readonly Period = 3 * 60 * 1000 /* three minutes */;
|
||||
private static DidInformUser = 'autofetch.didInformUser';
|
||||
|
||||
private _onDidChange = new EventEmitter<boolean>();
|
||||
private onDidChange = this._onDidChange.event;
|
||||
@@ -23,9 +31,49 @@ export class AutoFetcher {
|
||||
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(private repository: Repository) {
|
||||
constructor(private repository: Repository, private globalState: Memento) {
|
||||
workspace.onDidChangeConfiguration(this.onConfiguration, this, this.disposables);
|
||||
this.onConfiguration();
|
||||
|
||||
const onGoodRemoteOperation = filterEvent(repository.onDidRunOperation, ({ operation, error }) => !error && isRemoteOperation(operation));
|
||||
const onFirstGoodRemoteOperation = onceEvent(onGoodRemoteOperation);
|
||||
onFirstGoodRemoteOperation(this.onFirstGoodRemoteOperation, this, this.disposables);
|
||||
}
|
||||
|
||||
private async onFirstGoodRemoteOperation(): Promise<void> {
|
||||
const didInformUser = !this.globalState.get<boolean>(AutoFetcher.DidInformUser);
|
||||
|
||||
if (this.enabled && !didInformUser) {
|
||||
this.globalState.update(AutoFetcher.DidInformUser, true);
|
||||
}
|
||||
|
||||
const shouldInformUser = !this.enabled && didInformUser;
|
||||
|
||||
if (!shouldInformUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const yes: MessageItem = { title: localize('yes', "Yes") };
|
||||
const readMore: MessageItem = { title: localize('read more', "Read More") };
|
||||
const no: MessageItem = { isCloseAffordance: true, title: localize('no', "No") };
|
||||
const askLater: MessageItem = { title: localize('not now', "Ask Me Later") };
|
||||
const result = await window.showInformationMessage(localize('suggest auto fetch', "Would you like Code to periodically run `git fetch`?"), yes, readMore, no, askLater);
|
||||
|
||||
if (result === askLater) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (result === readMore) {
|
||||
commands.executeCommand('vscode.open', Uri.parse('https://go.microsoft.com/fwlink/?linkid=865294'));
|
||||
return this.onFirstGoodRemoteOperation();
|
||||
}
|
||||
|
||||
if (result === yes) {
|
||||
const gitConfig = workspace.getConfiguration('git');
|
||||
gitConfig.update('autofetch', true, ConfigurationTarget.Global);
|
||||
}
|
||||
|
||||
this.globalState.update(AutoFetcher.DidInformUser, true);
|
||||
}
|
||||
|
||||
private onConfiguration(): void {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { Uri, commands, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation, TextEditor } from 'vscode';
|
||||
import { Uri, commands, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation, TextEditor, CancellationTokenSource, StatusBarAlignment } from 'vscode';
|
||||
import { Ref, RefType, Git, GitErrorCodes, Branch } from './git';
|
||||
import { Repository, Resource, Status, CommitOptions, ResourceGroupType } from './repository';
|
||||
import { Model } from './model';
|
||||
@@ -127,6 +127,15 @@ function command(commandId: string, options: CommandOptions = {}): Function {
|
||||
};
|
||||
}
|
||||
|
||||
const ImageMimetypes = [
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
'image/tiff',
|
||||
'image/bmp'
|
||||
];
|
||||
|
||||
export class CommandCenter {
|
||||
|
||||
private disposables: Disposable[];
|
||||
@@ -159,8 +168,8 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
private async _openResource(resource: Resource, preview?: boolean, preserveFocus?: boolean, preserveSelection?: boolean): Promise<void> {
|
||||
const left = this.getLeftResource(resource);
|
||||
const right = this.getRightResource(resource);
|
||||
const left = await this.getLeftResource(resource);
|
||||
const right = await this.getRightResource(resource);
|
||||
const title = this.getTitle(resource);
|
||||
|
||||
if (!right) {
|
||||
@@ -184,40 +193,77 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
if (!left) {
|
||||
const document = await workspace.openTextDocument(right);
|
||||
await window.showTextDocument(document, opts);
|
||||
return;
|
||||
await commands.executeCommand<void>('vscode.open', right, opts);
|
||||
} else {
|
||||
await commands.executeCommand<void>('vscode.diff', left, right, title, opts);
|
||||
}
|
||||
|
||||
return await commands.executeCommand<void>('vscode.diff', left, right, title, opts);
|
||||
}
|
||||
|
||||
private getLeftResource(resource: Resource): Uri | undefined {
|
||||
private async getURI(uri: Uri, ref: string): Promise<Uri | undefined> {
|
||||
const repository = this.model.getRepository(uri);
|
||||
|
||||
if (!repository) {
|
||||
return toGitUri(uri, ref);
|
||||
}
|
||||
|
||||
try {
|
||||
let gitRef = ref;
|
||||
|
||||
if (gitRef === '~') {
|
||||
const uriString = uri.toString();
|
||||
const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString);
|
||||
gitRef = indexStatus ? '' : 'HEAD';
|
||||
}
|
||||
|
||||
const { size, object } = await repository.lstree(gitRef, uri.fsPath);
|
||||
const { mimetype, encoding } = await repository.detectObjectType(object);
|
||||
|
||||
if (mimetype === 'text/plain') {
|
||||
return toGitUri(uri, ref);
|
||||
}
|
||||
|
||||
if (size > 1000000) { // 1 MB
|
||||
return Uri.parse(`data:;label:${path.basename(uri.fsPath)};description:${gitRef},`);
|
||||
}
|
||||
|
||||
if (ImageMimetypes.indexOf(mimetype) > -1) {
|
||||
const contents = await repository.buffer(gitRef, uri.fsPath);
|
||||
return Uri.parse(`data:${mimetype};label:${path.basename(uri.fsPath)};description:${gitRef};size:${size};base64,${contents.toString('base64')}`);
|
||||
}
|
||||
|
||||
return Uri.parse(`data:;label:${path.basename(uri.fsPath)};description:${gitRef},`);
|
||||
|
||||
} catch (err) {
|
||||
return toGitUri(uri, ref);
|
||||
}
|
||||
}
|
||||
|
||||
private async getLeftResource(resource: Resource): Promise<Uri | undefined> {
|
||||
switch (resource.type) {
|
||||
case Status.INDEX_MODIFIED:
|
||||
case Status.INDEX_RENAMED:
|
||||
return toGitUri(resource.original, 'HEAD');
|
||||
return this.getURI(resource.original, 'HEAD');
|
||||
|
||||
case Status.MODIFIED:
|
||||
return toGitUri(resource.resourceUri, '~');
|
||||
return this.getURI(resource.resourceUri, '~');
|
||||
|
||||
case Status.DELETED_BY_THEM:
|
||||
return toGitUri(resource.resourceUri, '');
|
||||
return this.getURI(resource.resourceUri, '');
|
||||
}
|
||||
}
|
||||
|
||||
private getRightResource(resource: Resource): Uri | undefined {
|
||||
private async getRightResource(resource: Resource): Promise<Uri | undefined> {
|
||||
switch (resource.type) {
|
||||
case Status.INDEX_MODIFIED:
|
||||
case Status.INDEX_ADDED:
|
||||
case Status.INDEX_COPIED:
|
||||
case Status.INDEX_RENAMED:
|
||||
return toGitUri(resource.resourceUri, '');
|
||||
return this.getURI(resource.resourceUri, '');
|
||||
|
||||
case Status.INDEX_DELETED:
|
||||
case Status.DELETED_BY_THEM:
|
||||
case Status.DELETED:
|
||||
return toGitUri(resource.resourceUri, 'HEAD');
|
||||
return this.getURI(resource.resourceUri, 'HEAD');
|
||||
|
||||
case Status.MODIFIED:
|
||||
case Status.UNTRACKED:
|
||||
@@ -261,6 +307,8 @@ export class CommandCenter {
|
||||
return '';
|
||||
}
|
||||
|
||||
private static cloneId = 0;
|
||||
|
||||
@command('git.clone')
|
||||
async clone(url?: string): Promise<void> {
|
||||
if (!url) {
|
||||
@@ -281,7 +329,8 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
const config = workspace.getConfiguration('git');
|
||||
const value = config.get<string>('defaultCloneDirectory') || os.homedir();
|
||||
let value = config.get<string>('defaultCloneDirectory') || os.homedir();
|
||||
value = value.replace(/^~/, os.homedir());
|
||||
|
||||
const parentPath = await window.showInputBox({
|
||||
prompt: localize('parent', "Parent Directory"),
|
||||
@@ -299,12 +348,21 @@ export class CommandCenter {
|
||||
return;
|
||||
}
|
||||
|
||||
const clonePromise = this.git.clone(url, parentPath);
|
||||
const tokenSource = new CancellationTokenSource();
|
||||
const cancelCommandId = `cancelClone${CommandCenter.cloneId++}`;
|
||||
const commandDisposable = commands.registerCommand(cancelCommandId, () => tokenSource.cancel());
|
||||
|
||||
const statusBarItem = window.createStatusBarItem(StatusBarAlignment.Left);
|
||||
statusBarItem.text = localize('cancel', "$(sync~spin) Cloning repository... Click to cancel");
|
||||
statusBarItem.tooltip = localize('cancel tooltip', "Cancel clone");
|
||||
statusBarItem.command = cancelCommandId;
|
||||
statusBarItem.show();
|
||||
|
||||
const clonePromise = this.git.clone(url, parentPath, tokenSource.token);
|
||||
|
||||
try {
|
||||
window.withProgress({ location: ProgressLocation.SourceControl, title: localize('cloning', "Cloning git repository...") }, () => clonePromise);
|
||||
window.withProgress({ location: ProgressLocation.Window, title: localize('cloning', "Cloning git repository...") }, () => clonePromise);
|
||||
// window.withProgress({ location: ProgressLocation.Window, title: localize('cloning', "Cloning git repository...") }, () => clonePromise);
|
||||
|
||||
const repositoryPath = await clonePromise;
|
||||
|
||||
@@ -330,6 +388,8 @@ export class CommandCenter {
|
||||
}
|
||||
*/
|
||||
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' });
|
||||
} else if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
|
||||
return;
|
||||
} else {
|
||||
/* __GDPR__
|
||||
"clone" : {
|
||||
@@ -338,41 +398,62 @@ export class CommandCenter {
|
||||
*/
|
||||
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' });
|
||||
}
|
||||
|
||||
throw err;
|
||||
} finally {
|
||||
commandDisposable.dispose();
|
||||
statusBarItem.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@command('git.init')
|
||||
async init(): Promise<void> {
|
||||
const homeUri = Uri.file(os.homedir());
|
||||
const defaultUri = workspace.workspaceFolders && workspace.workspaceFolders.length > 0
|
||||
? Uri.file(workspace.workspaceFolders[0].uri.fsPath)
|
||||
: homeUri;
|
||||
let path: string | undefined;
|
||||
|
||||
const result = await window.showOpenDialog({
|
||||
canSelectFiles: false,
|
||||
canSelectFolders: true,
|
||||
canSelectMany: false,
|
||||
defaultUri,
|
||||
openLabel: localize('init repo', "Initialize Repository")
|
||||
});
|
||||
if (workspace.workspaceFolders && workspace.workspaceFolders.length > 1) {
|
||||
const placeHolder = localize('init', "Pick workspace folder to initialize git repo in");
|
||||
const items = workspace.workspaceFolders.map(folder => ({ label: folder.name, description: folder.uri.fsPath, folder }));
|
||||
const item = await window.showQuickPick(items, { placeHolder, ignoreFocusOut: true });
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uri = result[0];
|
||||
|
||||
if (homeUri.toString().startsWith(uri.toString())) {
|
||||
const yes = localize('create repo', "Initialize Repository");
|
||||
const answer = await window.showWarningMessage(localize('are you sure', "This will create a Git repository in '{0}'. Are you sure you want to continue?", uri.fsPath), yes);
|
||||
|
||||
if (answer !== yes) {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
path = item.folder.uri.fsPath;
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
const homeUri = Uri.file(os.homedir());
|
||||
const defaultUri = workspace.workspaceFolders && workspace.workspaceFolders.length > 0
|
||||
? Uri.file(workspace.workspaceFolders[0].uri.fsPath)
|
||||
: homeUri;
|
||||
|
||||
const result = await window.showOpenDialog({
|
||||
canSelectFiles: false,
|
||||
canSelectFolders: true,
|
||||
canSelectMany: false,
|
||||
defaultUri,
|
||||
openLabel: localize('init repo', "Initialize Repository")
|
||||
});
|
||||
|
||||
if (!result || result.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uri = result[0];
|
||||
|
||||
if (homeUri.toString().startsWith(uri.toString())) {
|
||||
const yes = localize('create repo', "Initialize Repository");
|
||||
const answer = await window.showWarningMessage(localize('are you sure', "This will create a Git repository in '{0}'. Are you sure you want to continue?", uri.fsPath), yes);
|
||||
|
||||
if (answer !== yes) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
path = uri.fsPath;
|
||||
}
|
||||
|
||||
const path = uri.fsPath;
|
||||
await this.git.init(path);
|
||||
await this.model.tryOpenRepository(path);
|
||||
}
|
||||
@@ -426,8 +507,7 @@ export class CommandCenter {
|
||||
opts.selection = activeTextEditor.selection;
|
||||
}
|
||||
|
||||
const document = await workspace.openTextDocument(uri);
|
||||
await window.showTextDocument(document, opts);
|
||||
await commands.executeCommand<void>('vscode.open', uri, opts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,7 +527,7 @@ export class CommandCenter {
|
||||
return;
|
||||
}
|
||||
|
||||
const HEAD = this.getLeftResource(resource);
|
||||
const HEAD = await this.getLeftResource(resource);
|
||||
|
||||
if (!HEAD) {
|
||||
window.showWarningMessage(localize('HEAD not available', "HEAD version of '{0}' is not available.", path.basename(resource.resourceUri.fsPath)));
|
||||
@@ -494,7 +574,7 @@ export class CommandCenter {
|
||||
|
||||
@command('git.stage')
|
||||
async stage(...resourceStates: SourceControlResourceState[]): Promise<void> {
|
||||
if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
|
||||
if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
|
||||
const resource = this.getSCMResource();
|
||||
|
||||
if (!resource) {
|
||||
@@ -671,7 +751,7 @@ export class CommandCenter {
|
||||
|
||||
@command('git.unstage')
|
||||
async unstage(...resourceStates: SourceControlResourceState[]): Promise<void> {
|
||||
if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
|
||||
if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
|
||||
const resource = this.getSCMResource();
|
||||
|
||||
if (!resource) {
|
||||
@@ -737,7 +817,7 @@ export class CommandCenter {
|
||||
|
||||
@command('git.clean')
|
||||
async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
|
||||
if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
|
||||
if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
|
||||
const resource = this.getSCMResource();
|
||||
|
||||
if (!resource) {
|
||||
@@ -917,6 +997,7 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
return await window.showInputBox({
|
||||
value: opts && opts.defaultMsg,
|
||||
placeHolder: localize('commit message', "Commit message"),
|
||||
prompt: localize('provide commit message', "Please provide a commit message"),
|
||||
ignoreFocusOut: true
|
||||
@@ -960,7 +1041,15 @@ export class CommandCenter {
|
||||
|
||||
@command('git.commitStagedAmend', { repository: true })
|
||||
async commitStagedAmend(repository: Repository): Promise<void> {
|
||||
await this.commitWithAnyInput(repository, { all: false, amend: true });
|
||||
let msg;
|
||||
if (repository.HEAD) {
|
||||
if (repository.HEAD.commit) {
|
||||
let id = repository.HEAD.commit;
|
||||
let commit = await repository.getCommit(id);
|
||||
msg = commit.message;
|
||||
}
|
||||
}
|
||||
await this.commitWithAnyInput(repository, { all: false, amend: true, defaultMsg: msg });
|
||||
}
|
||||
|
||||
@command('git.commitAll', { repository: true })
|
||||
@@ -1077,6 +1166,31 @@ export class CommandCenter {
|
||||
}
|
||||
}
|
||||
|
||||
@command('git.renameBranch', { repository: true })
|
||||
async renameBranch(repository: Repository): Promise<void> {
|
||||
const placeHolder = localize('provide branch name', "Please provide a branch name");
|
||||
const name = await window.showInputBox({ placeHolder });
|
||||
|
||||
if (!name || name.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await repository.renameBranch(name);
|
||||
} catch (err) {
|
||||
switch (err.gitErrorCode) {
|
||||
case GitErrorCodes.InvalidBranchName:
|
||||
window.showErrorMessage(localize('invalid branch name', 'Invalid branch name'));
|
||||
return;
|
||||
case GitErrorCodes.BranchAlreadyExists:
|
||||
window.showErrorMessage(localize('branch already exists', "A branch named '{0}' already exists", name));
|
||||
return;
|
||||
default:
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@command('git.merge', { repository: true })
|
||||
async merge(repository: Repository): Promise<void> {
|
||||
const config = workspace.getConfiguration('git');
|
||||
@@ -1134,6 +1248,16 @@ export class CommandCenter {
|
||||
await repository.tag(name, message);
|
||||
}
|
||||
|
||||
@command('git.fetch', { repository: true })
|
||||
async fetch(repository: Repository): Promise<void> {
|
||||
if (repository.remotes.length === 0) {
|
||||
window.showWarningMessage(localize('no remotes to fetch', "This repository has no remotes configured to fetch from."));
|
||||
return;
|
||||
}
|
||||
|
||||
await repository.fetch();
|
||||
}
|
||||
|
||||
@command('git.pullFrom', { repository: true })
|
||||
async pullFrom(repository: Repository): Promise<void> {
|
||||
const remotes = repository.remotes;
|
||||
@@ -1240,8 +1364,7 @@ export class CommandCenter {
|
||||
repository.pushTo(pick.label, branchName);
|
||||
}
|
||||
|
||||
@command('git.sync', { repository: true })
|
||||
async sync(repository: Repository): Promise<void> {
|
||||
private async _sync(repository: Repository, rebase: boolean): Promise<void> {
|
||||
const HEAD = repository.HEAD;
|
||||
|
||||
if (!HEAD || !HEAD.upstream) {
|
||||
@@ -1264,7 +1387,16 @@ export class CommandCenter {
|
||||
}
|
||||
}
|
||||
|
||||
await repository.sync();
|
||||
if (rebase) {
|
||||
await repository.syncRebase();
|
||||
} else {
|
||||
await repository.sync();
|
||||
}
|
||||
}
|
||||
|
||||
@command('git.sync', { repository: true })
|
||||
sync(repository: Repository): Promise<void> {
|
||||
return this._sync(repository, false);
|
||||
}
|
||||
|
||||
@command('git._syncAll')
|
||||
@@ -1280,6 +1412,11 @@ export class CommandCenter {
|
||||
}));
|
||||
}
|
||||
|
||||
@command('git.syncRebase', { repository: true })
|
||||
syncRebase(repository: Repository): Promise<void> {
|
||||
return this._sync(repository, true);
|
||||
}
|
||||
|
||||
@command('git.publish', { repository: true })
|
||||
async publish(repository: Repository): Promise<void> {
|
||||
const remotes = repository.remotes;
|
||||
@@ -1304,14 +1441,9 @@ export class CommandCenter {
|
||||
await repository.pushTo(choice, branchName, true);
|
||||
}
|
||||
|
||||
@command('git.showOutput')
|
||||
showOutput(): void {
|
||||
this.outputChannel.show();
|
||||
}
|
||||
|
||||
@command('git.ignore')
|
||||
async ignore(...resourceStates: SourceControlResourceState[]): Promise<void> {
|
||||
if (resourceStates.length === 0 || !(resourceStates[0].resourceUri instanceof Uri)) {
|
||||
if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
|
||||
const resource = this.getSCMResource();
|
||||
|
||||
if (!resource) {
|
||||
@@ -1332,23 +1464,36 @@ export class CommandCenter {
|
||||
await this.runByRepository(resources, async (repository, resources) => repository.ignore(resources));
|
||||
}
|
||||
|
||||
@command('git.stash', { repository: true })
|
||||
async stash(repository: Repository): Promise<void> {
|
||||
private async _stash(repository: Repository, includeUntracked = false): Promise<void> {
|
||||
if (repository.workingTreeGroup.resourceStates.length === 0) {
|
||||
window.showInformationMessage(localize('no changes stash', "There are no changes to stash."));
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await window.showInputBox({
|
||||
prompt: localize('provide stash message', "Optionally provide a stash message"),
|
||||
placeHolder: localize('stash message', "Stash message")
|
||||
});
|
||||
const message = await this.getStashMessage();
|
||||
|
||||
if (typeof message === 'undefined') {
|
||||
return;
|
||||
}
|
||||
|
||||
await repository.createStash(message);
|
||||
await repository.createStash(message, includeUntracked);
|
||||
}
|
||||
|
||||
private async getStashMessage(): Promise<string | undefined> {
|
||||
return await window.showInputBox({
|
||||
prompt: localize('provide stash message', "Optionally provide a stash message"),
|
||||
placeHolder: localize('stash message', "Stash message")
|
||||
});
|
||||
}
|
||||
|
||||
@command('git.stash', { repository: true })
|
||||
stash(repository: Repository): Promise<void> {
|
||||
return this._stash(repository);
|
||||
}
|
||||
|
||||
@command('git.stashIncludeUntracked', { repository: true })
|
||||
stashIncludeUntracked(repository: Repository): Promise<void> {
|
||||
return this._stash(repository, true);
|
||||
}
|
||||
|
||||
@command('git.stashPop', { repository: true })
|
||||
@@ -1384,7 +1529,7 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any {
|
||||
const result = (...args) => {
|
||||
const result = (...args: any[]) => {
|
||||
let result: Promise<any>;
|
||||
|
||||
if (!options.repository) {
|
||||
@@ -1426,14 +1571,14 @@ export class CommandCenter {
|
||||
message = localize('clean repo', "Please clean your repository working tree before checkout.");
|
||||
break;
|
||||
case GitErrorCodes.PushRejected:
|
||||
message = localize('cant push', "Can't push refs to remote. Run 'Pull' first to integrate your changes.");
|
||||
message = localize('cant push', "Can't push refs to remote. Try running 'Pull' first to integrate your changes.");
|
||||
break;
|
||||
default:
|
||||
const hint = (err.stderr || err.message || String(err))
|
||||
.replace(/^error: /mi, '')
|
||||
.replace(/^> husky.*$/mi, '')
|
||||
.split(/[\r\n]/)
|
||||
.filter(line => !!line)
|
||||
.filter((line: string) => !!line)
|
||||
[0];
|
||||
|
||||
message = hint
|
||||
@@ -1459,7 +1604,7 @@ export class CommandCenter {
|
||||
};
|
||||
|
||||
// patch this object, so people can call methods directly
|
||||
this[key] = result;
|
||||
(this as any)[key] = result;
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -1523,4 +1668,4 @@ export class CommandCenter {
|
||||
dispose(): void {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { workspace, Uri, Disposable, Event, EventEmitter, window } from 'vscode'
|
||||
import { debounce, throttle } from './decorators';
|
||||
import { fromGitUri, toGitUri } from './uri';
|
||||
import { Model, ModelChangeEvent, OriginalResourceChangeEvent } from './model';
|
||||
import { filterEvent, eventToPromise } from './util';
|
||||
import { filterEvent, eventToPromise, isDescendant } from './util';
|
||||
|
||||
interface CacheRow {
|
||||
uri: Uri;
|
||||
@@ -72,7 +72,7 @@ export class GitContentProvider {
|
||||
const fsPath = uri.fsPath;
|
||||
|
||||
for (const root of this.changedRepositoryRoots) {
|
||||
if (fsPath.startsWith(root)) {
|
||||
if (isDescendant(root, fsPath)) {
|
||||
this._onDidChange.fire(uri);
|
||||
return;
|
||||
}
|
||||
@@ -100,7 +100,7 @@ export class GitContentProvider {
|
||||
if (ref === '~') {
|
||||
const fileUri = Uri.file(path);
|
||||
const uriString = fileUri.toString();
|
||||
const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.original.toString() === uriString);
|
||||
const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString);
|
||||
ref = indexStatus ? '' : 'HEAD';
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
'use strict';
|
||||
|
||||
import { window, workspace, Uri, Disposable, Event, EventEmitter, DecorationData, DecorationProvider, ThemeColor } from 'vscode';
|
||||
import { Repository, GitResourceGroup } from './repository';
|
||||
import { Repository, GitResourceGroup, Status } from './repository';
|
||||
import { Model } from './model';
|
||||
import { debounce } from './decorators';
|
||||
import { filterEvent } from './util';
|
||||
@@ -74,34 +74,28 @@ class GitDecorationProvider implements DecorationProvider {
|
||||
constructor(private repository: Repository) {
|
||||
this.disposables.push(
|
||||
window.registerDecorationProvider(this),
|
||||
repository.onDidRunOperation(this.onDidRunOperation, this)
|
||||
repository.onDidRunGitStatus(this.onDidRunGitStatus, this)
|
||||
);
|
||||
}
|
||||
|
||||
private onDidRunOperation(): void {
|
||||
private onDidRunGitStatus(): void {
|
||||
let newDecorations = new Map<string, DecorationData>();
|
||||
this.collectDecorationData(this.repository.indexGroup, newDecorations);
|
||||
this.collectDecorationData(this.repository.workingTreeGroup, newDecorations);
|
||||
this.collectDecorationData(this.repository.mergeGroup, newDecorations);
|
||||
|
||||
let uris: Uri[] = [];
|
||||
newDecorations.forEach((value, uriString) => {
|
||||
if (this.decorations.has(uriString)) {
|
||||
this.decorations.delete(uriString);
|
||||
} else {
|
||||
uris.push(Uri.parse(uriString));
|
||||
}
|
||||
});
|
||||
this.decorations.forEach((value, uriString) => {
|
||||
uris.push(Uri.parse(uriString));
|
||||
});
|
||||
const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()]));
|
||||
this.decorations = newDecorations;
|
||||
this._onDidChangeDecorations.fire(uris);
|
||||
this._onDidChangeDecorations.fire([...uris.values()].map(Uri.parse));
|
||||
}
|
||||
|
||||
private collectDecorationData(group: GitResourceGroup, bucket: Map<string, DecorationData>): void {
|
||||
group.resourceStates.forEach(r => {
|
||||
if (r.resourceDecoration) {
|
||||
if (r.resourceDecoration
|
||||
&& r.type !== Status.DELETED
|
||||
&& r.type !== Status.INDEX_DELETED
|
||||
) {
|
||||
// not deleted and has a decoration
|
||||
bucket.set(r.original.toString(), r.resourceDecoration);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -31,7 +31,7 @@ function decorate(decorator: (fn: Function, key: string) => Function): Function
|
||||
function _memoize(fn: Function, key: string): Function {
|
||||
const memoizeKey = `$memoize$${key}`;
|
||||
|
||||
return function (...args: any[]) {
|
||||
return function (this: any, ...args: any[]) {
|
||||
if (!this.hasOwnProperty(memoizeKey)) {
|
||||
Object.defineProperty(this, memoizeKey, {
|
||||
configurable: false,
|
||||
@@ -51,7 +51,7 @@ function _throttle<T>(fn: Function, key: string): Function {
|
||||
const currentKey = `$throttle$current$${key}`;
|
||||
const nextKey = `$throttle$next$${key}`;
|
||||
|
||||
const trigger = function (...args: any[]) {
|
||||
const trigger = function (this: any, ...args: any[]) {
|
||||
if (this[nextKey]) {
|
||||
return this[nextKey];
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export const throttle = decorate(_throttle);
|
||||
function _sequentialize<T>(fn: Function, key: string): Function {
|
||||
const currentKey = `__$sequence$${key}`;
|
||||
|
||||
return function (...args: any[]) {
|
||||
return function (this: any, ...args: any[]) {
|
||||
const currentPromise = this[currentKey] as Promise<any> || Promise.resolve(null);
|
||||
const run = async () => await fn.apply(this, args);
|
||||
this[currentKey] = currentPromise.then(run, run);
|
||||
@@ -95,7 +95,7 @@ export function debounce(delay: number): Function {
|
||||
return decorate((fn, key) => {
|
||||
const timerKey = `$debounce$${key}`;
|
||||
|
||||
return function (...args: any[]) {
|
||||
return function (this: any, ...args: any[]) {
|
||||
clearTimeout(this[timerKey]);
|
||||
this[timerKey] = setTimeout(() => fn.apply(this, args), delay);
|
||||
};
|
||||
|
||||
@@ -9,9 +9,12 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import * as cp from 'child_process';
|
||||
import * as which from 'which';
|
||||
import { EventEmitter } from 'events';
|
||||
import iconv = require('iconv-lite');
|
||||
import { assign, uniqBy, groupBy, denodeify, IDisposable, toDisposable, dispose, mkdirp } from './util';
|
||||
import * as filetype from 'file-type';
|
||||
import { assign, uniqBy, groupBy, denodeify, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent } from './util';
|
||||
import { CancellationToken } from 'vscode';
|
||||
|
||||
const readfile = denodeify<string>(fs.readFile);
|
||||
|
||||
@@ -60,8 +63,10 @@ function parseVersion(raw: string): string {
|
||||
return raw.replace(/^git version /, '');
|
||||
}
|
||||
|
||||
function findSpecificGit(path: string): Promise<IGit> {
|
||||
function findSpecificGit(path: string, onLookup: (path: string) => void): Promise<IGit> {
|
||||
return new Promise<IGit>((c, e) => {
|
||||
onLookup(path);
|
||||
|
||||
const buffers: Buffer[] = [];
|
||||
const child = cp.spawn(path, ['--version']);
|
||||
child.stdout.on('data', (b: Buffer) => buffers.push(b));
|
||||
@@ -70,7 +75,7 @@ function findSpecificGit(path: string): Promise<IGit> {
|
||||
});
|
||||
}
|
||||
|
||||
function findGitDarwin(): Promise<IGit> {
|
||||
function findGitDarwin(onLookup: (path: string) => void): Promise<IGit> {
|
||||
return new Promise<IGit>((c, e) => {
|
||||
cp.exec('which git', (err, gitPathBuffer) => {
|
||||
if (err) {
|
||||
@@ -80,8 +85,11 @@ function findGitDarwin(): Promise<IGit> {
|
||||
const path = gitPathBuffer.toString().replace(/^\s+|\s+$/g, '');
|
||||
|
||||
function getVersion(path: string) {
|
||||
onLookup(path);
|
||||
|
||||
// make sure git executes
|
||||
cp.exec('git --version', (err, stdout) => {
|
||||
|
||||
if (err) {
|
||||
return e('git not found');
|
||||
}
|
||||
@@ -109,38 +117,44 @@ function findGitDarwin(): Promise<IGit> {
|
||||
});
|
||||
}
|
||||
|
||||
function findSystemGitWin32(base: string): Promise<IGit> {
|
||||
function findSystemGitWin32(base: string, onLookup: (path: string) => void): Promise<IGit> {
|
||||
if (!base) {
|
||||
return Promise.reject<IGit>('Not found');
|
||||
}
|
||||
|
||||
return findSpecificGit(path.join(base, 'Git', 'cmd', 'git.exe'));
|
||||
return findSpecificGit(path.join(base, 'Git', 'cmd', 'git.exe'), onLookup);
|
||||
}
|
||||
|
||||
function findGitWin32(): Promise<IGit> {
|
||||
return findSystemGitWin32(process.env['ProgramW6432'])
|
||||
.then(void 0, () => findSystemGitWin32(process.env['ProgramFiles(x86)']))
|
||||
.then(void 0, () => findSystemGitWin32(process.env['ProgramFiles']))
|
||||
.then(void 0, () => findSpecificGit('git'));
|
||||
function findGitWin32InPath(onLookup: (path: string) => void): Promise<IGit> {
|
||||
const whichPromise = new Promise<string>((c, e) => which('git.exe', (err, path) => err ? e(err) : c(path)));
|
||||
return whichPromise.then(path => findSpecificGit(path, onLookup));
|
||||
}
|
||||
|
||||
export function findGit(hint: string | undefined): Promise<IGit> {
|
||||
var first = hint ? findSpecificGit(hint) : Promise.reject<IGit>(null);
|
||||
function findGitWin32(onLookup: (path: string) => void): Promise<IGit> {
|
||||
return findSystemGitWin32(process.env['ProgramW6432'] as string, onLookup)
|
||||
.then(void 0, () => findSystemGitWin32(process.env['ProgramFiles(x86)'] as string, onLookup))
|
||||
.then(void 0, () => findSystemGitWin32(process.env['ProgramFiles'] as string, onLookup))
|
||||
.then(void 0, () => findSystemGitWin32(path.join(process.env['LocalAppData'] as string, 'Programs'), onLookup))
|
||||
.then(void 0, () => findGitWin32InPath(onLookup));
|
||||
}
|
||||
|
||||
export function findGit(hint: string | undefined, onLookup: (path: string) => void): Promise<IGit> {
|
||||
const first = hint ? findSpecificGit(hint, onLookup) : Promise.reject<IGit>(null);
|
||||
|
||||
return first
|
||||
.then(void 0, () => {
|
||||
switch (process.platform) {
|
||||
case 'darwin': return findGitDarwin();
|
||||
case 'win32': return findGitWin32();
|
||||
default: return findSpecificGit('git');
|
||||
case 'darwin': return findGitDarwin(onLookup);
|
||||
case 'win32': return findGitWin32(onLookup);
|
||||
default: return findSpecificGit('git', onLookup);
|
||||
}
|
||||
})
|
||||
.then(null, () => Promise.reject(new Error('Git installation not found.')));
|
||||
}
|
||||
|
||||
export interface IExecutionResult {
|
||||
export interface IExecutionResult<T extends string | Buffer> {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
stdout: T;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
@@ -162,50 +176,69 @@ export interface SpawnOptions extends cp.SpawnOptions {
|
||||
input?: string;
|
||||
encoding?: string;
|
||||
log?: boolean;
|
||||
cancellationToken?: CancellationToken;
|
||||
}
|
||||
|
||||
async function exec(child: cp.ChildProcess, options: SpawnOptions = {}): Promise<IExecutionResult> {
|
||||
async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToken): Promise<IExecutionResult<Buffer>> {
|
||||
if (!child.stdout || !child.stderr) {
|
||||
throw new GitError({
|
||||
message: 'Failed to get stdout or stderr from git process.'
|
||||
});
|
||||
throw new GitError({ message: 'Failed to get stdout or stderr from git process.' });
|
||||
}
|
||||
|
||||
if (cancellationToken && cancellationToken.isCancellationRequested) {
|
||||
throw new GitError({ message: 'Cancelled' });
|
||||
}
|
||||
|
||||
const disposables: IDisposable[] = [];
|
||||
|
||||
const once = (ee: NodeJS.EventEmitter, name: string, fn: Function) => {
|
||||
const once = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void) => {
|
||||
ee.once(name, fn);
|
||||
disposables.push(toDisposable(() => ee.removeListener(name, fn)));
|
||||
};
|
||||
|
||||
const on = (ee: NodeJS.EventEmitter, name: string, fn: Function) => {
|
||||
const on = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void) => {
|
||||
ee.on(name, fn);
|
||||
disposables.push(toDisposable(() => ee.removeListener(name, fn)));
|
||||
};
|
||||
|
||||
let encoding = options.encoding || 'utf8';
|
||||
encoding = iconv.encodingExists(encoding) ? encoding : 'utf8';
|
||||
|
||||
const [exitCode, stdout, stderr] = await Promise.all<any>([
|
||||
let result = Promise.all<any>([
|
||||
new Promise<number>((c, e) => {
|
||||
once(child, 'error', cpErrorHandler(e));
|
||||
once(child, 'exit', c);
|
||||
}),
|
||||
new Promise<string>(c => {
|
||||
new Promise<Buffer>(c => {
|
||||
const buffers: Buffer[] = [];
|
||||
on(child.stdout, 'data', b => buffers.push(b));
|
||||
once(child.stdout, 'close', () => c(iconv.decode(Buffer.concat(buffers), encoding)));
|
||||
on(child.stdout, 'data', (b: Buffer) => buffers.push(b));
|
||||
once(child.stdout, 'close', () => c(Buffer.concat(buffers)));
|
||||
}),
|
||||
new Promise<string>(c => {
|
||||
const buffers: Buffer[] = [];
|
||||
on(child.stderr, 'data', b => buffers.push(b));
|
||||
on(child.stderr, 'data', (b: Buffer) => buffers.push(b));
|
||||
once(child.stderr, 'close', () => c(Buffer.concat(buffers).toString('utf8')));
|
||||
})
|
||||
]);
|
||||
]) as Promise<[number, Buffer, string]>;
|
||||
|
||||
dispose(disposables);
|
||||
if (cancellationToken) {
|
||||
const cancellationPromise = new Promise<[number, Buffer, string]>((_, e) => {
|
||||
onceEvent(cancellationToken.onCancellationRequested)(() => {
|
||||
try {
|
||||
child.kill();
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
|
||||
return { exitCode, stdout, stderr };
|
||||
e(new GitError({ message: 'Cancelled' }));
|
||||
});
|
||||
});
|
||||
|
||||
result = Promise.race([result, cancellationPromise]);
|
||||
}
|
||||
|
||||
try {
|
||||
const [exitCode, stdout, stderr] = await result;
|
||||
return { exitCode, stdout, stderr };
|
||||
} finally {
|
||||
dispose(disposables);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IGitErrorData {
|
||||
@@ -288,6 +321,8 @@ export const GitErrorCodes = {
|
||||
RepositoryIsLocked: 'RepositoryIsLocked',
|
||||
BranchNotFullyMerged: 'BranchNotFullyMerged',
|
||||
NoRemoteReference: 'NoRemoteReference',
|
||||
InvalidBranchName: 'InvalidBranchName',
|
||||
BranchAlreadyExists: 'BranchAlreadyExists',
|
||||
NoLocalChanges: 'NoLocalChanges',
|
||||
NoStashFound: 'NoStashFound',
|
||||
LocalChangesOverwritten: 'LocalChangesOverwritten'
|
||||
@@ -312,6 +347,10 @@ function getGitErrorCode(stderr: string): string | undefined {
|
||||
return GitErrorCodes.BranchNotFullyMerged;
|
||||
} else if (/Couldn\'t find remote ref/.test(stderr)) {
|
||||
return GitErrorCodes.NoRemoteReference;
|
||||
} else if (/A branch named '.+' already exists/.test(stderr)) {
|
||||
return GitErrorCodes.BranchAlreadyExists;
|
||||
} else if (/'.+' is not a valid branch name/.test(stderr)) {
|
||||
return GitErrorCodes.InvalidBranchName;
|
||||
}
|
||||
|
||||
return void 0;
|
||||
@@ -341,12 +380,12 @@ export class Git {
|
||||
return;
|
||||
}
|
||||
|
||||
async clone(url: string, parentPath: string): Promise<string> {
|
||||
async clone(url: string, parentPath: string, cancellationToken?: CancellationToken): Promise<string> {
|
||||
const folderName = decodeURI(url).replace(/^.*\//, '').replace(/\.git$/, '') || 'repository';
|
||||
const folderPath = path.join(parentPath, folderName);
|
||||
|
||||
await mkdirp(parentPath);
|
||||
await this.exec(parentPath, ['clone', url, folderPath]);
|
||||
await this.exec(parentPath, ['clone', url, folderPath], { cancellationToken });
|
||||
return folderPath;
|
||||
}
|
||||
|
||||
@@ -355,7 +394,7 @@ export class Git {
|
||||
return path.normalize(result.stdout.trim());
|
||||
}
|
||||
|
||||
async exec(cwd: string, args: string[], options: SpawnOptions = {}): Promise<IExecutionResult> {
|
||||
async exec(cwd: string, args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
|
||||
options = assign({ cwd }, options || {});
|
||||
return await this._exec(args, options);
|
||||
}
|
||||
@@ -365,21 +404,30 @@ export class Git {
|
||||
return this.spawn(args, options);
|
||||
}
|
||||
|
||||
private async _exec(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult> {
|
||||
private async _exec(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
|
||||
const child = this.spawn(args, options);
|
||||
|
||||
if (options.input) {
|
||||
child.stdin.end(options.input, 'utf8');
|
||||
}
|
||||
|
||||
const result = await exec(child, options);
|
||||
const bufferResult = await exec(child, options.cancellationToken);
|
||||
|
||||
if (options.log !== false && result.stderr.length > 0) {
|
||||
this.log(`${result.stderr}\n`);
|
||||
if (options.log !== false && bufferResult.stderr.length > 0) {
|
||||
this.log(`${bufferResult.stderr}\n`);
|
||||
}
|
||||
|
||||
if (result.exitCode) {
|
||||
return Promise.reject<IExecutionResult>(new GitError({
|
||||
let encoding = options.encoding || 'utf8';
|
||||
encoding = iconv.encodingExists(encoding) ? encoding : 'utf8';
|
||||
|
||||
const result: IExecutionResult<string> = {
|
||||
exitCode: bufferResult.exitCode,
|
||||
stdout: iconv.decode(bufferResult.stdout, encoding),
|
||||
stderr: bufferResult.stderr
|
||||
};
|
||||
|
||||
if (bufferResult.exitCode) {
|
||||
return Promise.reject<IExecutionResult<string>>(new GitError({
|
||||
message: 'Failed to execute git',
|
||||
stdout: result.stdout,
|
||||
stderr: result.stderr,
|
||||
@@ -510,7 +558,7 @@ export class Repository {
|
||||
}
|
||||
|
||||
// TODO@Joao: rename to exec
|
||||
async run(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult> {
|
||||
async run(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
|
||||
return await this.git.exec(this.repositoryRoot, args, options);
|
||||
}
|
||||
|
||||
@@ -539,39 +587,97 @@ export class Repository {
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
async buffer(object: string, encoding: string = 'utf8'): Promise<string> {
|
||||
async bufferString(object: string, encoding: string = 'utf8'): Promise<string> {
|
||||
const stdout = await this.buffer(object);
|
||||
return iconv.decode(stdout, iconv.encodingExists(encoding) ? encoding : 'utf8');
|
||||
}
|
||||
|
||||
async buffer(object: string): Promise<Buffer> {
|
||||
const child = this.stream(['show', object]);
|
||||
|
||||
if (!child.stdout) {
|
||||
return Promise.reject<string>('Can\'t open file from git');
|
||||
return Promise.reject<Buffer>('Can\'t open file from git');
|
||||
}
|
||||
|
||||
const { exitCode, stdout } = await exec(child, { encoding });
|
||||
const { exitCode, stdout } = await exec(child);
|
||||
|
||||
if (exitCode) {
|
||||
return Promise.reject<string>(new GitError({
|
||||
return Promise.reject<Buffer>(new GitError({
|
||||
message: 'Could not show object.',
|
||||
exitCode
|
||||
}));
|
||||
}
|
||||
|
||||
return stdout;
|
||||
}
|
||||
|
||||
// TODO@joao
|
||||
// return new Promise((c, e) => {
|
||||
// detectMimesFromStream(child.stdout, null, (err, result) => {
|
||||
// if (err) {
|
||||
// e(err);
|
||||
// } else if (isBinaryMime(result.mimes)) {
|
||||
// e(<IFileOperationResult>{
|
||||
// message: localize('fileBinaryError', "File seems to be binary and cannot be opened as text"),
|
||||
// fileOperationResult: FileOperationResult.FILE_IS_BINARY
|
||||
// });
|
||||
// } else {
|
||||
// c(this.doBuffer(object));
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
async lstree(treeish: string, path: string): Promise<{ mode: number, object: string, size: number }> {
|
||||
if (!treeish) { // index
|
||||
const { stdout } = await this.run(['ls-files', '--stage', '--', path]);
|
||||
|
||||
const match = /^(\d+)\s+([0-9a-f]{40})\s+(\d+)/.exec(stdout);
|
||||
|
||||
if (!match) {
|
||||
throw new GitError({ message: 'Error running ls-files' });
|
||||
}
|
||||
|
||||
const [, mode, object] = match;
|
||||
const catFile = await this.run(['cat-file', '-s', object]);
|
||||
const size = parseInt(catFile.stdout);
|
||||
|
||||
return { mode: parseInt(mode), object, size };
|
||||
}
|
||||
|
||||
const { stdout } = await this.run(['ls-tree', '-l', treeish, '--', path]);
|
||||
|
||||
const match = /^(\d+)\s+(\w+)\s+([0-9a-f]{40})\s+(\d+)/.exec(stdout);
|
||||
|
||||
if (!match) {
|
||||
throw new GitError({ message: 'Error running ls-tree' });
|
||||
}
|
||||
|
||||
const [, mode, , object, size] = match;
|
||||
return { mode: parseInt(mode), object, size: parseInt(size) };
|
||||
}
|
||||
|
||||
async detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }> {
|
||||
const child = await this.stream(['show', object]);
|
||||
const buffer = await readBytes(child.stdout, 4100);
|
||||
|
||||
try {
|
||||
child.kill();
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
|
||||
const encoding = detectUnicodeEncoding(buffer);
|
||||
let isText = true;
|
||||
|
||||
if (encoding !== Encoding.UTF16be && encoding !== Encoding.UTF16le) {
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
if (buffer.readInt8(i) === 0) {
|
||||
isText = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isText) {
|
||||
const result = filetype(buffer);
|
||||
|
||||
if (!result) {
|
||||
return { mimetype: 'application/octet-stream' };
|
||||
} else {
|
||||
return { mimetype: result.mime };
|
||||
}
|
||||
}
|
||||
|
||||
if (encoding) {
|
||||
return { mimetype: 'text/plain', encoding };
|
||||
} else {
|
||||
// TODO@JOAO: read the setting OUTSIDE!
|
||||
return { mimetype: 'text/plain' };
|
||||
}
|
||||
}
|
||||
|
||||
async add(paths: string[]): Promise<void> {
|
||||
@@ -591,6 +697,7 @@ export class Repository {
|
||||
child.stdin.end(data, 'utf8');
|
||||
|
||||
const { exitCode, stdout } = await exec(child);
|
||||
const hash = stdout.toString('utf8');
|
||||
|
||||
if (exitCode) {
|
||||
throw new GitError({
|
||||
@@ -599,7 +706,7 @@ export class Repository {
|
||||
});
|
||||
}
|
||||
|
||||
await this.run(['update-index', '--cacheinfo', '100644', stdout, path]);
|
||||
await this.run(['update-index', '--cacheinfo', '100644', hash, path]);
|
||||
}
|
||||
|
||||
async checkout(treeish: string, paths: string[]): Promise<void> {
|
||||
@@ -680,6 +787,11 @@ export class Repository {
|
||||
await this.run(args);
|
||||
}
|
||||
|
||||
async renameBranch(name: string): Promise<void> {
|
||||
const args = ['branch', '-m', name];
|
||||
await this.run(args);
|
||||
}
|
||||
|
||||
async merge(ref: string): Promise<void> {
|
||||
const args = ['merge', ref];
|
||||
|
||||
@@ -847,10 +959,14 @@ export class Repository {
|
||||
}
|
||||
}
|
||||
|
||||
async createStash(message?: string): Promise<void> {
|
||||
async createStash(message?: string, includeUntracked?: boolean): Promise<void> {
|
||||
try {
|
||||
const args = ['stash', 'save'];
|
||||
|
||||
if (includeUntracked) {
|
||||
args.push('-u');
|
||||
}
|
||||
|
||||
if (message) {
|
||||
args.push('--', message);
|
||||
}
|
||||
@@ -869,7 +985,7 @@ export class Repository {
|
||||
try {
|
||||
const args = ['stash', 'pop'];
|
||||
|
||||
if (typeof index === 'string') {
|
||||
if (typeof index === 'number') {
|
||||
args.push(`stash@{${index}}`);
|
||||
}
|
||||
|
||||
@@ -891,7 +1007,7 @@ export class Repository {
|
||||
const env = { GIT_OPTIONAL_LOCKS: '0' };
|
||||
const child = this.stream(['status', '-z', '-u'], { env });
|
||||
|
||||
const onExit = exitCode => {
|
||||
const onExit = (exitCode: number) => {
|
||||
if (exitCode !== 0) {
|
||||
const stderr = stderrData.join('');
|
||||
return e(new GitError({
|
||||
@@ -909,12 +1025,12 @@ export class Repository {
|
||||
const onStdoutData = (raw: string) => {
|
||||
parser.update(raw);
|
||||
|
||||
if (parser.status.length > 5000) {
|
||||
if (parser.status.length > limit) {
|
||||
child.removeListener('exit', onExit);
|
||||
child.stdout.removeListener('data', onStdoutData);
|
||||
child.kill();
|
||||
|
||||
c({ status: parser.status.slice(0, 5000), didHitLimit: true });
|
||||
c({ status: parser.status.slice(0, limit), didHitLimit: true });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -953,7 +1069,7 @@ export class Repository {
|
||||
async getRefs(): Promise<Ref[]> {
|
||||
const result = await this.run(['for-each-ref', '--format', '%(refname) %(objectname)']);
|
||||
|
||||
const fn = (line): Ref | null => {
|
||||
const fn = (line: string): Ref | null => {
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
if (match = /^refs\/heads\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) {
|
||||
@@ -978,7 +1094,7 @@ export class Repository {
|
||||
const regex = /^stash@{(\d+)}:(.+)$/;
|
||||
const rawStashes = result.stdout.trim().split('\n')
|
||||
.filter(b => !!b)
|
||||
.map(line => regex.exec(line))
|
||||
.map(line => regex.exec(line) as RegExpExecArray)
|
||||
.filter(g => !!g)
|
||||
.map(([, index, description]: RegExpExecArray) => ({ index: parseInt(index), description }));
|
||||
|
||||
@@ -990,7 +1106,7 @@ export class Repository {
|
||||
const regex = /^([^\s]+)\s+([^\s]+)\s/;
|
||||
const rawRemotes = result.stdout.trim().split('\n')
|
||||
.filter(b => !!b)
|
||||
.map(line => regex.exec(line))
|
||||
.map(line => regex.exec(line) as RegExpExecArray)
|
||||
.filter(g => !!g)
|
||||
.map((groups: RegExpExecArray) => ({ name: groups[1], url: groups[2] }));
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.config(process.env.VSCODE_NLS_CONFIG)();
|
||||
import { ExtensionContext, workspace, window, Disposable, commands, Uri } from 'vscode';
|
||||
import { ExtensionContext, workspace, window, Disposable, commands, Uri, OutputChannel } from 'vscode';
|
||||
import { findGit, Git, IGit } from './git';
|
||||
import { Model } from './model';
|
||||
import { CommandCenter } from './commands';
|
||||
@@ -16,23 +16,19 @@ import { GitDecorations } from './decorationProvider';
|
||||
import { Askpass } from './askpass';
|
||||
import { toDisposable } from './util';
|
||||
import TelemetryReporter from 'vscode-extension-telemetry';
|
||||
import { API, createApi } from './api';
|
||||
|
||||
async function init(context: ExtensionContext, disposables: Disposable[]): Promise<void> {
|
||||
async function init(context: ExtensionContext, outputChannel: OutputChannel, disposables: Disposable[]): Promise<Model> {
|
||||
const { name, version, aiKey } = require(context.asAbsolutePath('./package.json')) as { name: string, version: string, aiKey: string };
|
||||
const telemetryReporter: TelemetryReporter = new TelemetryReporter(name, version, aiKey);
|
||||
disposables.push(telemetryReporter);
|
||||
|
||||
const outputChannel = window.createOutputChannel('Git');
|
||||
disposables.push(outputChannel);
|
||||
|
||||
const config = workspace.getConfiguration('git');
|
||||
const enabled = config.get<boolean>('enabled') === true;
|
||||
const pathHint = workspace.getConfiguration('git').get<string>('path');
|
||||
const info = await findGit(pathHint);
|
||||
const info = await findGit(pathHint, path => outputChannel.appendLine(localize('looking', "Looking for git in: {0}", path)));
|
||||
const askpass = new Askpass();
|
||||
const env = await askpass.getEnv();
|
||||
const git = new Git({ gitPath: info.path, version: info.version, env });
|
||||
const model = new Model(git);
|
||||
const model = new Model(git, context.globalState);
|
||||
disposables.push(model);
|
||||
|
||||
const onRepository = () => commands.executeCommand('setContext', 'gitOpenRepositoryCount', `${model.repositories.length}`);
|
||||
@@ -40,15 +36,9 @@ async function init(context: ExtensionContext, disposables: Disposable[]): Promi
|
||||
model.onDidCloseRepository(onRepository, null, disposables);
|
||||
onRepository();
|
||||
|
||||
if (!enabled) {
|
||||
const commandCenter = new CommandCenter(git, model, outputChannel, telemetryReporter);
|
||||
disposables.push(commandCenter);
|
||||
return;
|
||||
}
|
||||
|
||||
outputChannel.appendLine(localize('using git', "Using git {0} from {1}", info.version, info.path));
|
||||
|
||||
const onOutput = str => outputChannel.append(str);
|
||||
const onOutput = (str: string) => outputChannel.append(str);
|
||||
git.onOutput.addListener('log', onOutput);
|
||||
disposables.push(toDisposable(() => git.onOutput.removeListener('log', onOutput)));
|
||||
|
||||
@@ -59,14 +49,58 @@ async function init(context: ExtensionContext, disposables: Disposable[]): Promi
|
||||
);
|
||||
|
||||
await checkGitVersion(info);
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
export function activate(context: ExtensionContext): any {
|
||||
async function _activate(context: ExtensionContext, disposables: Disposable[]): Promise<Model | undefined> {
|
||||
const outputChannel = window.createOutputChannel('Git');
|
||||
commands.registerCommand('git.showOutput', () => outputChannel.show());
|
||||
disposables.push(outputChannel);
|
||||
|
||||
try {
|
||||
return await init(context, outputChannel, disposables);
|
||||
} catch (err) {
|
||||
if (!/Git installation not found/.test(err.message || '')) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
const config = workspace.getConfiguration('git');
|
||||
const shouldIgnore = config.get<boolean>('ignoreMissingGitWarning') === true;
|
||||
|
||||
if (shouldIgnore) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(err.message);
|
||||
outputChannel.appendLine(err.message);
|
||||
outputChannel.show();
|
||||
|
||||
const download = localize('downloadgit', "Download Git");
|
||||
const neverShowAgain = localize('neverShowAgain', "Don't show again");
|
||||
const choice = await window.showWarningMessage(
|
||||
localize('notfound', "Git not found. Install it or configure it using the 'git.path' setting."),
|
||||
download,
|
||||
neverShowAgain
|
||||
);
|
||||
|
||||
if (choice === download) {
|
||||
commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/'));
|
||||
} else if (choice === neverShowAgain) {
|
||||
await config.update('ignoreMissingGitWarning', true, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function activate(context: ExtensionContext): API {
|
||||
const disposables: Disposable[] = [];
|
||||
context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose()));
|
||||
|
||||
init(context, disposables)
|
||||
.catch(err => console.error(err));
|
||||
const activatePromise = _activate(context, disposables);
|
||||
const modelPromise = activatePromise.then(model => model || Promise.reject<Model>('Git model not found'));
|
||||
activatePromise.catch(err => console.error(err));
|
||||
|
||||
return createApi(modelPromise);
|
||||
}
|
||||
|
||||
async function checkGitVersion(info: IGit): Promise<void> {
|
||||
@@ -102,5 +136,6 @@ async function checkGitVersion(info: IGit): Promise<void> {
|
||||
} else if (choice === neverShowAgain) {
|
||||
await config.update('ignoreLegacyWarning', true, true);
|
||||
}
|
||||
// {{SQL CARBON EDIT}}
|
||||
*/
|
||||
}
|
||||
|
||||
@@ -5,14 +5,15 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor } from 'vscode';
|
||||
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, ConfigurationChangeEvent } from 'vscode';
|
||||
import { Repository, RepositoryState } from './repository';
|
||||
import { memoize, sequentialize, debounce } from './decorators';
|
||||
import { dispose, anyEvent, filterEvent } from './util';
|
||||
import { dispose, anyEvent, filterEvent, IDisposable, isDescendant } from './util';
|
||||
import { Git, GitErrorCodes } from './git';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { fromGitUri } from './uri';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -44,10 +45,6 @@ interface OpenRepository extends Disposable {
|
||||
repository: Repository;
|
||||
}
|
||||
|
||||
function isParent(parent: string, child: string): boolean {
|
||||
return child.startsWith(parent);
|
||||
}
|
||||
|
||||
export class Model {
|
||||
|
||||
private _onDidOpenRepository = new EventEmitter<Repository>();
|
||||
@@ -67,45 +64,17 @@ export class Model {
|
||||
|
||||
private possibleGitRepositoryPaths = new Set<string>();
|
||||
|
||||
private enabled = false;
|
||||
private configurationChangeDisposable: Disposable;
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(private git: Git) {
|
||||
const config = workspace.getConfiguration('git');
|
||||
this.enabled = config.get<boolean>('enabled') === true;
|
||||
|
||||
this.configurationChangeDisposable = workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this);
|
||||
|
||||
if (this.enabled) {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
private onDidChangeConfiguration(): void {
|
||||
const config = workspace.getConfiguration('git');
|
||||
const enabled = config.get<boolean>('enabled') === true;
|
||||
|
||||
if (enabled === this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.enabled = enabled;
|
||||
|
||||
if (enabled) {
|
||||
this.enable();
|
||||
} else {
|
||||
this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
private enable(): void {
|
||||
constructor(private git: Git, private globalState: Memento) {
|
||||
workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables);
|
||||
this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] });
|
||||
|
||||
window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
|
||||
this.onDidChangeVisibleTextEditors(window.visibleTextEditors);
|
||||
|
||||
workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables);
|
||||
|
||||
const fsWatcher = workspace.createFileSystemWatcher('**');
|
||||
this.disposables.push(fsWatcher);
|
||||
|
||||
@@ -117,15 +86,6 @@ export class Model {
|
||||
this.scanWorkspaceFolders();
|
||||
}
|
||||
|
||||
private disable(): void {
|
||||
const openRepositories = [...this.openRepositories];
|
||||
openRepositories.forEach(r => r.dispose());
|
||||
this.openRepositories = [];
|
||||
|
||||
this.possibleGitRepositoryPaths.clear();
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scans the first level of each workspace folder, looking
|
||||
* for git repositories.
|
||||
@@ -169,7 +129,21 @@ export class Model {
|
||||
.map(folder => this.getOpenRepository(folder.uri))
|
||||
.filter(r => !!r)
|
||||
.filter(r => !activeRepositories.has(r!.repository))
|
||||
.filter(r => !(workspace.workspaceFolders || []).some(f => isParent(f.uri.fsPath, r!.repository.root))) as OpenRepository[];
|
||||
.filter(r => !(workspace.workspaceFolders || []).some(f => isDescendant(f.uri.fsPath, r!.repository.root))) as OpenRepository[];
|
||||
|
||||
possibleRepositoryFolders.forEach(p => this.tryOpenRepository(p.uri.fsPath));
|
||||
openRepositoriesToDispose.forEach(r => r.dispose());
|
||||
}
|
||||
|
||||
private onDidChangeConfiguration(): void {
|
||||
const possibleRepositoryFolders = (workspace.workspaceFolders || [])
|
||||
.filter(folder => workspace.getConfiguration('git', folder.uri).get<boolean>('enabled') === true)
|
||||
.filter(folder => !this.getOpenRepository(folder.uri));
|
||||
|
||||
const openRepositoriesToDispose = this.openRepositories
|
||||
.map(repository => ({ repository, root: Uri.file(repository.repository.root) }))
|
||||
.filter(({ root }) => workspace.getConfiguration('git', root).get<boolean>('enabled') !== true)
|
||||
.map(({ repository }) => repository);
|
||||
|
||||
possibleRepositoryFolders.forEach(p => this.tryOpenRepository(p.uri.fsPath));
|
||||
openRepositoriesToDispose.forEach(r => r.dispose());
|
||||
@@ -199,6 +173,13 @@ export class Model {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = workspace.getConfiguration('git', Uri.file(path));
|
||||
const enabled = config.get<boolean>('enabled') === true;
|
||||
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const repositoryRoot = await this.git.getRepositoryRoot(path);
|
||||
|
||||
@@ -209,7 +190,7 @@ export class Model {
|
||||
return;
|
||||
}
|
||||
|
||||
const repository = new Repository(this.git.open(repositoryRoot));
|
||||
const repository = new Repository(this.git.open(repositoryRoot), this.globalState);
|
||||
|
||||
this.open(repository);
|
||||
} catch (err) {
|
||||
@@ -292,12 +273,18 @@ export class Model {
|
||||
}
|
||||
|
||||
if (hint instanceof Uri) {
|
||||
const resourcePath = hint.fsPath;
|
||||
let resourcePath: string;
|
||||
|
||||
if (hint.scheme === 'git') {
|
||||
resourcePath = fromGitUri(hint).path;
|
||||
} else {
|
||||
resourcePath = hint.fsPath;
|
||||
}
|
||||
|
||||
for (const liveRepository of this.openRepositories) {
|
||||
const relativePath = path.relative(liveRepository.repository.root, resourcePath);
|
||||
|
||||
if (!/^\.\./.test(relativePath)) {
|
||||
if (isDescendant(liveRepository.repository.root, resourcePath)) {
|
||||
return liveRepository;
|
||||
}
|
||||
}
|
||||
@@ -321,7 +308,11 @@ export class Model {
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disable();
|
||||
this.configurationChangeDisposable.dispose();
|
||||
const openRepositories = [...this.openRepositories];
|
||||
openRepositories.forEach(r => r.dispose());
|
||||
this.openRepositories = [];
|
||||
|
||||
this.possibleGitRepositoryPaths.clear();
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import { Uri, Command, EventEmitter, Event, scm, SourceControl, SourceControlInputBox, SourceControlResourceGroup, SourceControlResourceState, SourceControlResourceDecorations, Disposable, ProgressLocation, window, workspace, WorkspaceEdit, ThemeColor, DecorationData } from 'vscode';
|
||||
import { Uri, Command, EventEmitter, Event, scm, SourceControl, SourceControlInputBox, SourceControlResourceGroup, SourceControlResourceState, SourceControlResourceDecorations, Disposable, ProgressLocation, window, workspace, WorkspaceEdit, ThemeColor, DecorationData, Memento } from 'vscode';
|
||||
import { Repository as BaseRepository, Ref, Branch, Remote, Commit, GitErrorCodes, Stash, RefType, GitError } from './git';
|
||||
import { anyEvent, filterEvent, eventToPromise, dispose, find } from './util';
|
||||
import { anyEvent, filterEvent, eventToPromise, dispose, find, isDescendant } from './util';
|
||||
import { memoize, throttle, debounce } from './decorators';
|
||||
import { toGitUri } from './uri';
|
||||
import { AutoFetcher } from './autofetch';
|
||||
@@ -82,7 +82,7 @@ export class Resource implements SourceControlResourceState {
|
||||
get original(): Uri { return this._resourceUri; }
|
||||
get renameResourceUri(): Uri | undefined { return this._renameResourceUri; }
|
||||
|
||||
private static Icons = {
|
||||
private static Icons: any = {
|
||||
light: {
|
||||
Modified: getIconUri('status-modified', 'light'),
|
||||
Added: getIconUri('status-added', 'light'),
|
||||
@@ -171,10 +171,8 @@ export class Resource implements SourceControlResourceState {
|
||||
}
|
||||
|
||||
get decorations(): SourceControlResourceDecorations {
|
||||
// TODO@joh, still requires restart/redraw in the SCM viewlet
|
||||
const decorations = workspace.getConfiguration().get<boolean>('git.decorations.enabled');
|
||||
const light = !decorations ? { iconPath: this.getIconPath('light') } : undefined;
|
||||
const dark = !decorations ? { iconPath: this.getIconPath('dark') } : undefined;
|
||||
const light = this._useIcons ? { iconPath: this.getIconPath('light') } : undefined;
|
||||
const dark = this._useIcons ? { iconPath: this.getIconPath('dark') } : undefined;
|
||||
const tooltip = this.tooltip;
|
||||
const strikeThrough = this.strikeThrough;
|
||||
const faded = this.faded;
|
||||
@@ -275,6 +273,7 @@ export class Resource implements SourceControlResourceState {
|
||||
private _resourceGroupType: ResourceGroupType,
|
||||
private _resourceUri: Uri,
|
||||
private _type: Status,
|
||||
private _useIcons: boolean,
|
||||
private _renameResourceUri?: Uri
|
||||
) { }
|
||||
}
|
||||
@@ -296,11 +295,13 @@ export enum Operation {
|
||||
Stage = 'Stage',
|
||||
GetCommitTemplate = 'GetCommitTemplate',
|
||||
DeleteBranch = 'DeleteBranch',
|
||||
RenameBranch = 'RenameBranch',
|
||||
Merge = 'Merge',
|
||||
Ignore = 'Ignore',
|
||||
Tag = 'Tag',
|
||||
Stash = 'Stash',
|
||||
CheckIgnore = 'CheckIgnore'
|
||||
CheckIgnore = 'CheckIgnore',
|
||||
LSTree = 'LSTree'
|
||||
}
|
||||
|
||||
function isReadOnly(operation: Operation): boolean {
|
||||
@@ -308,6 +309,7 @@ function isReadOnly(operation: Operation): boolean {
|
||||
case Operation.Show:
|
||||
case Operation.GetCommitTemplate:
|
||||
case Operation.CheckIgnore:
|
||||
case Operation.LSTree:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
@@ -318,6 +320,8 @@ function shouldShowProgress(operation: Operation): boolean {
|
||||
switch (operation) {
|
||||
case Operation.Fetch:
|
||||
case Operation.CheckIgnore:
|
||||
case Operation.LSTree:
|
||||
case Operation.Show:
|
||||
return false;
|
||||
default:
|
||||
return true;
|
||||
@@ -369,12 +373,18 @@ export interface CommitOptions {
|
||||
amend?: boolean;
|
||||
signoff?: boolean;
|
||||
signCommit?: boolean;
|
||||
defaultMsg?: string;
|
||||
}
|
||||
|
||||
export interface GitResourceGroup extends SourceControlResourceGroup {
|
||||
resourceStates: Resource[];
|
||||
}
|
||||
|
||||
export interface OperationResult {
|
||||
operation: Operation;
|
||||
error: any;
|
||||
}
|
||||
|
||||
export class Repository implements Disposable {
|
||||
|
||||
private _onDidChangeRepository = new EventEmitter<Uri>();
|
||||
@@ -384,7 +394,7 @@ export class Repository implements Disposable {
|
||||
readonly onDidChangeState: Event<RepositoryState> = this._onDidChangeState.event;
|
||||
|
||||
private _onDidChangeStatus = new EventEmitter<void>();
|
||||
readonly onDidChangeStatus: Event<void> = this._onDidChangeStatus.event;
|
||||
readonly onDidRunGitStatus: Event<void> = this._onDidChangeStatus.event;
|
||||
|
||||
private _onDidChangeOriginalResource = new EventEmitter<Uri>();
|
||||
readonly onDidChangeOriginalResource: Event<Uri> = this._onDidChangeOriginalResource.event;
|
||||
@@ -392,8 +402,8 @@ export class Repository implements Disposable {
|
||||
private _onRunOperation = new EventEmitter<Operation>();
|
||||
readonly onRunOperation: Event<Operation> = this._onRunOperation.event;
|
||||
|
||||
private _onDidRunOperation = new EventEmitter<Operation>();
|
||||
readonly onDidRunOperation: Event<Operation> = this._onDidRunOperation.event;
|
||||
private _onDidRunOperation = new EventEmitter<OperationResult>();
|
||||
readonly onDidRunOperation: Event<OperationResult> = this._onDidRunOperation.event;
|
||||
|
||||
@memoize
|
||||
get onDidChangeOperations(): Event<void> {
|
||||
@@ -456,13 +466,14 @@ export class Repository implements Disposable {
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly repository: BaseRepository
|
||||
private readonly repository: BaseRepository,
|
||||
globalState: Memento
|
||||
) {
|
||||
const fsWatcher = workspace.createFileSystemWatcher('**');
|
||||
this.disposables.push(fsWatcher);
|
||||
|
||||
const onWorkspaceChange = anyEvent(fsWatcher.onDidChange, fsWatcher.onDidCreate, fsWatcher.onDidDelete);
|
||||
const onRepositoryChange = filterEvent(onWorkspaceChange, uri => !/^\.\./.test(path.relative(repository.root, uri.fsPath)));
|
||||
const onRepositoryChange = filterEvent(onWorkspaceChange, uri => isDescendant(repository.root, uri.fsPath));
|
||||
const onRelevantRepositoryChange = filterEvent(onRepositoryChange, uri => !/\/\.git\/index\.lock$/.test(uri.path));
|
||||
onRelevantRepositoryChange(this.onFSChange, this, this.disposables);
|
||||
|
||||
@@ -470,6 +481,7 @@ export class Repository implements Disposable {
|
||||
onRelevantGitChange(this._onDidChangeRepository.fire, this._onDidChangeRepository, this.disposables);
|
||||
|
||||
this._sourceControl = scm.createSourceControl('git', 'Git', Uri.file(repository.root));
|
||||
this._sourceControl.inputBox.placeholder = localize('commitMessage', "Message (press {0} to commit)");
|
||||
this._sourceControl.acceptInputCommand = { command: 'git.commitWithInput', title: localize('commit', "Commit"), arguments: [this._sourceControl] };
|
||||
this._sourceControl.quickDiffProvider = this;
|
||||
this.disposables.push(this._sourceControl);
|
||||
@@ -485,7 +497,7 @@ export class Repository implements Disposable {
|
||||
this.disposables.push(this.indexGroup);
|
||||
this.disposables.push(this.workingTreeGroup);
|
||||
|
||||
this.disposables.push(new AutoFetcher(this));
|
||||
this.disposables.push(new AutoFetcher(this, globalState));
|
||||
|
||||
const statusBar = new StatusBarCommands(this);
|
||||
this.disposables.push(statusBar);
|
||||
@@ -598,6 +610,10 @@ export class Repository implements Disposable {
|
||||
await this.run(Operation.DeleteBranch, () => this.repository.deleteBranch(name, force));
|
||||
}
|
||||
|
||||
async renameBranch(name: string): Promise<void> {
|
||||
await this.run(Operation.RenameBranch, () => this.repository.renameBranch(name));
|
||||
}
|
||||
|
||||
async merge(ref: string): Promise<void> {
|
||||
await this.run(Operation.Merge, () => this.repository.merge(ref));
|
||||
}
|
||||
@@ -650,10 +666,9 @@ export class Repository implements Disposable {
|
||||
await this.run(Operation.Push, () => this.repository.push(remote, undefined, false, true));
|
||||
}
|
||||
|
||||
@throttle
|
||||
async sync(): Promise<void> {
|
||||
private async _sync(rebase: boolean): Promise<void> {
|
||||
await this.run(Operation.Sync, async () => {
|
||||
await this.repository.pull();
|
||||
await this.repository.pull(rebase);
|
||||
|
||||
const shouldPush = this.HEAD && typeof this.HEAD.ahead === 'number' ? this.HEAD.ahead > 0 : true;
|
||||
|
||||
@@ -663,22 +678,52 @@ export class Repository implements Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
@throttle
|
||||
sync(): Promise<void> {
|
||||
return this._sync(false);
|
||||
}
|
||||
|
||||
@throttle
|
||||
async syncRebase(): Promise<void> {
|
||||
return this._sync(true);
|
||||
}
|
||||
|
||||
async show(ref: string, filePath: string): Promise<string> {
|
||||
return await this.run(Operation.Show, async () => {
|
||||
const relativePath = path.relative(this.repository.root, filePath).replace(/\\/g, '/');
|
||||
const configFiles = workspace.getConfiguration('files', Uri.file(filePath));
|
||||
const encoding = configFiles.get<string>('encoding');
|
||||
|
||||
return await this.repository.buffer(`${ref}:${relativePath}`, encoding);
|
||||
// TODO@joao: Resource config api
|
||||
return await this.repository.bufferString(`${ref}:${relativePath}`, encoding);
|
||||
});
|
||||
}
|
||||
|
||||
async buffer(ref: string, filePath: string): Promise<Buffer> {
|
||||
return await this.run(Operation.Show, async () => {
|
||||
const relativePath = path.relative(this.repository.root, filePath).replace(/\\/g, '/');
|
||||
const configFiles = workspace.getConfiguration('files', Uri.file(filePath));
|
||||
const encoding = configFiles.get<string>('encoding');
|
||||
|
||||
// TODO@joao: REsource config api
|
||||
return await this.repository.buffer(`${ref}:${relativePath}`);
|
||||
});
|
||||
}
|
||||
|
||||
lstree(ref: string, filePath: string): Promise<{ mode: number, object: string, size: number }> {
|
||||
return this.run(Operation.LSTree, () => this.repository.lstree(ref, filePath));
|
||||
}
|
||||
|
||||
detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }> {
|
||||
return this.run(Operation.Show, () => this.repository.detectObjectType(object));
|
||||
}
|
||||
|
||||
async getStashes(): Promise<Stash[]> {
|
||||
return await this.repository.getStashes();
|
||||
}
|
||||
|
||||
async createStash(message?: string): Promise<void> {
|
||||
return await this.run(Operation.Stash, () => this.repository.createStash(message));
|
||||
async createStash(message?: string, includeUntracked?: boolean): Promise<void> {
|
||||
return await this.run(Operation.Stash, () => this.repository.createStash(message, includeUntracked));
|
||||
}
|
||||
|
||||
async popStash(index?: number): Promise<void> {
|
||||
@@ -715,7 +760,8 @@ export class Repository implements Disposable {
|
||||
return this.run(Operation.CheckIgnore, () => {
|
||||
return new Promise<Set<string>>((resolve, reject) => {
|
||||
|
||||
filePaths = filePaths.filter(filePath => !path.relative(this.root, filePath).startsWith('..'));
|
||||
filePaths = filePaths
|
||||
.filter(filePath => isDescendant(this.root, filePath));
|
||||
|
||||
if (filePaths.length === 0) {
|
||||
// nothing left
|
||||
@@ -726,7 +772,7 @@ export class Repository implements Disposable {
|
||||
const child = this.repository.stream(['check-ignore', '-z', '--stdin'], { stdio: [null, null, null] });
|
||||
child.stdin.end(filePaths.join('\0'), 'utf8');
|
||||
|
||||
const onExit = exitCode => {
|
||||
const onExit = (exitCode: number) => {
|
||||
if (exitCode === 1) {
|
||||
// nothing ignored
|
||||
resolve(new Set<string>());
|
||||
@@ -762,6 +808,8 @@ export class Repository implements Disposable {
|
||||
}
|
||||
|
||||
const run = async () => {
|
||||
let error: any = null;
|
||||
|
||||
this._operations.start(operation);
|
||||
this._onRunOperation.fire(operation);
|
||||
|
||||
@@ -774,6 +822,8 @@ export class Repository implements Disposable {
|
||||
|
||||
return result;
|
||||
} catch (err) {
|
||||
error = err;
|
||||
|
||||
if (err.gitErrorCode === GitErrorCodes.NotAGitRepository) {
|
||||
this.state = RepositoryState.Disposed;
|
||||
}
|
||||
@@ -781,7 +831,7 @@ export class Repository implements Disposable {
|
||||
throw err;
|
||||
} finally {
|
||||
this._operations.end(operation);
|
||||
this._onDidRunOperation.fire(operation);
|
||||
this._onDidRunOperation.fire({ operation, error });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -813,6 +863,7 @@ export class Repository implements Disposable {
|
||||
const { status, didHitLimit } = await this.repository.getStatus();
|
||||
const config = workspace.getConfiguration('git');
|
||||
const shouldIgnore = config.get<boolean>('ignoreLimitWarning') === true;
|
||||
const useIcons = !config.get<boolean>('decorations.enabled', true);
|
||||
|
||||
this.isRepositoryHuge = didHitLimit;
|
||||
|
||||
@@ -860,30 +911,30 @@ export class Repository implements Disposable {
|
||||
const renameUri = raw.rename ? Uri.file(path.join(this.repository.root, raw.rename)) : undefined;
|
||||
|
||||
switch (raw.x + raw.y) {
|
||||
case '??': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.UNTRACKED));
|
||||
case '!!': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.IGNORED));
|
||||
case 'DD': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_DELETED));
|
||||
case 'AU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.ADDED_BY_US));
|
||||
case 'UD': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.DELETED_BY_THEM));
|
||||
case 'UA': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.ADDED_BY_THEM));
|
||||
case 'DU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.DELETED_BY_US));
|
||||
case 'AA': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_ADDED));
|
||||
case 'UU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_MODIFIED));
|
||||
case '??': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.UNTRACKED, useIcons));
|
||||
case '!!': return workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.IGNORED, useIcons));
|
||||
case 'DD': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_DELETED, useIcons));
|
||||
case 'AU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.ADDED_BY_US, useIcons));
|
||||
case 'UD': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.DELETED_BY_THEM, useIcons));
|
||||
case 'UA': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.ADDED_BY_THEM, useIcons));
|
||||
case 'DU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.DELETED_BY_US, useIcons));
|
||||
case 'AA': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_ADDED, useIcons));
|
||||
case 'UU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_MODIFIED, useIcons));
|
||||
}
|
||||
|
||||
let isModifiedInIndex = false;
|
||||
|
||||
switch (raw.x) {
|
||||
case 'M': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_MODIFIED)); isModifiedInIndex = true; break;
|
||||
case 'A': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_ADDED)); break;
|
||||
case 'D': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_DELETED)); break;
|
||||
case 'R': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_RENAMED, renameUri)); break;
|
||||
case 'C': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_COPIED, renameUri)); break;
|
||||
case 'M': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_MODIFIED, useIcons)); isModifiedInIndex = true; break;
|
||||
case 'A': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_ADDED, useIcons)); break;
|
||||
case 'D': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_DELETED, useIcons)); break;
|
||||
case 'R': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_RENAMED, useIcons, renameUri)); break;
|
||||
case 'C': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_COPIED, useIcons, renameUri)); break;
|
||||
}
|
||||
|
||||
switch (raw.y) {
|
||||
case 'M': workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.MODIFIED, renameUri)); break;
|
||||
case 'D': workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.DELETED, renameUri)); break;
|
||||
case 'M': workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.MODIFIED, useIcons, renameUri)); break;
|
||||
case 'D': workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.DELETED, useIcons, renameUri)); break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class CheckoutStatusBar {
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(private repository: Repository) {
|
||||
repository.onDidChangeStatus(this._onDidChange.fire, this._onDidChange, this.disposables);
|
||||
repository.onDidRunGitStatus(this._onDidChange.fire, this._onDidChange, this.disposables);
|
||||
}
|
||||
|
||||
get command(): Command | undefined {
|
||||
@@ -65,7 +65,7 @@ class SyncStatusBar {
|
||||
}
|
||||
|
||||
constructor(private repository: Repository) {
|
||||
repository.onDidChangeStatus(this.onModelChange, this, this.disposables);
|
||||
repository.onDidRunGitStatus(this.onModelChange, this, this.disposables);
|
||||
repository.onDidChangeOperations(this.onOperationsChange, this, this.disposables);
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
import 'mocha';
|
||||
import { GitStatusParser } from '../git';
|
||||
import * as assert from 'assert';
|
||||
|
||||
|
||||
4
extensions/git/src/typings/refs.d.ts
vendored
4
extensions/git/src/typings/refs.d.ts
vendored
@@ -4,6 +4,4 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/// <reference path='../../../../src/vs/vscode.d.ts'/>
|
||||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
||||
/// <reference types='@types/node'/>
|
||||
/// <reference types='@types/mocha'/>
|
||||
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
|
||||
@@ -6,7 +6,8 @@
|
||||
'use strict';
|
||||
|
||||
import { Event } from 'vscode';
|
||||
import { dirname } from 'path';
|
||||
import { dirname, sep } from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import * as fs from 'fs';
|
||||
import * as byline from 'byline';
|
||||
|
||||
@@ -86,7 +87,7 @@ export function once(fn: (...args: any[]) => any): (...args: any[]) => any {
|
||||
|
||||
export function assign<T>(destination: T, ...sources: any[]): T {
|
||||
for (const source of sources) {
|
||||
Object.keys(source).forEach(key => destination[key] = source[key]);
|
||||
Object.keys(source).forEach(key => (destination as any)[key] = source[key]);
|
||||
}
|
||||
|
||||
return destination;
|
||||
@@ -115,12 +116,12 @@ export function groupBy<T>(arr: T[], fn: (el: T) => string): { [key: string]: T[
|
||||
}, Object.create(null));
|
||||
}
|
||||
|
||||
export function denodeify<R>(fn: Function): (...args) => Promise<R> {
|
||||
return (...args) => new Promise<R>((c, e) => fn(...args, (err, r) => err ? e(err) : c(r)));
|
||||
export function denodeify<R>(fn: Function): (...args: any[]) => Promise<R> {
|
||||
return (...args) => new Promise<R>((c, e) => fn(...args, (err: any, r: any) => err ? e(err) : c(r)));
|
||||
}
|
||||
|
||||
export function nfcall<R>(fn: Function, ...args): Promise<R> {
|
||||
return new Promise<R>((c, e) => fn(...args, (err, r) => err ? e(err) : c(r)));
|
||||
export function nfcall<R>(fn: Function, ...args: any[]): Promise<R> {
|
||||
return new Promise<R>((c, e) => fn(...args, (err: any, r: any) => err ? e(err) : c(r)));
|
||||
}
|
||||
|
||||
export async function mkdirp(path: string, mode?: number): Promise<boolean> {
|
||||
@@ -205,4 +206,83 @@ export async function grep(filename: string, pattern: RegExp): Promise<boolean>
|
||||
stream.on('error', e);
|
||||
stream.on('end', () => c(false));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function readBytes(stream: Readable, bytes: number): Promise<Buffer> {
|
||||
return new Promise<Buffer>((complete, error) => {
|
||||
let done = false;
|
||||
let buffer = new Buffer(bytes);
|
||||
let bytesRead = 0;
|
||||
|
||||
stream.on('data', (data: Buffer) => {
|
||||
let bytesToRead = Math.min(bytes - bytesRead, data.length);
|
||||
data.copy(buffer, bytesRead, 0, bytesToRead);
|
||||
bytesRead += bytesToRead;
|
||||
|
||||
if (bytesRead === bytes) {
|
||||
(stream as any).destroy(); // Will trigger the close event eventually
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('error', (e: Error) => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on('close', () => {
|
||||
if (!done) {
|
||||
done = true;
|
||||
complete(buffer.slice(0, bytesRead));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export enum Encoding {
|
||||
UTF8 = 'utf8',
|
||||
UTF16be = 'utf16be',
|
||||
UTF16le = 'utf16le'
|
||||
}
|
||||
|
||||
export function detectUnicodeEncoding(buffer: Buffer): Encoding | null {
|
||||
if (buffer.length < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const b0 = buffer.readUInt8(0);
|
||||
const b1 = buffer.readUInt8(1);
|
||||
|
||||
if (b0 === 0xFE && b1 === 0xFF) {
|
||||
return Encoding.UTF16be;
|
||||
}
|
||||
|
||||
if (b0 === 0xFF && b1 === 0xFE) {
|
||||
return Encoding.UTF16le;
|
||||
}
|
||||
|
||||
if (buffer.length < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const b2 = buffer.readUInt8(2);
|
||||
|
||||
if (b0 === 0xEF && b1 === 0xBB && b2 === 0xBF) {
|
||||
return Encoding.UTF8;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function isDescendant(parent: string, descendant: string): boolean {
|
||||
if (parent === descendant) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (parent.charAt(parent.length - 1) !== sep) {
|
||||
parent += sep;
|
||||
}
|
||||
|
||||
return descendant.startsWith(parent);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user