mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-29 21:11:38 +01:00
Merge branch 'master' of https://github.com/microsoft/vscode
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Model } from '../model';
|
||||
import { Repository as BaseRepository, Resource } from '../repository';
|
||||
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions } from './git';
|
||||
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState } from './git';
|
||||
import { Event, SourceControlInputBox, Uri, SourceControl } from 'vscode';
|
||||
import { mapEvent } from '../util';
|
||||
|
||||
@@ -214,6 +214,14 @@ export class ApiImpl implements API {
|
||||
|
||||
readonly git = new ApiGit(this._model);
|
||||
|
||||
get state(): APIState {
|
||||
return this._model.state;
|
||||
}
|
||||
|
||||
get onDidChangeState(): Event<APIState> {
|
||||
return this._model.onDidChangeState;
|
||||
}
|
||||
|
||||
get onDidOpenRepository(): Event<Repository> {
|
||||
return mapEvent(this._model.onDidOpenRepository, r => new ApiRepository(r));
|
||||
}
|
||||
|
||||
4
extensions/git/src/api/git.d.ts
vendored
4
extensions/git/src/api/git.d.ts
vendored
@@ -176,7 +176,11 @@ export interface Repository {
|
||||
log(options?: LogOptions): Promise<Commit[]>;
|
||||
}
|
||||
|
||||
export type APIState = 'uninitialized' | 'initialized';
|
||||
|
||||
export interface API {
|
||||
readonly state: APIState;
|
||||
readonly onDidChangeState: Event<APIState>;
|
||||
readonly git: Git;
|
||||
readonly repositories: Repository[];
|
||||
readonly onDidOpenRepository: Event<Repository>;
|
||||
|
||||
@@ -450,6 +450,8 @@ export class CommandCenter {
|
||||
return;
|
||||
}
|
||||
|
||||
url = url.trim().replace(/^git\s+clone\s+/, '');
|
||||
|
||||
const config = workspace.getConfiguration('git');
|
||||
let defaultCloneDirectory = config.get<string>('defaultCloneDirectory') || os.homedir();
|
||||
defaultCloneDirectory = defaultCloneDirectory.replace(/^~/, os.homedir());
|
||||
@@ -673,7 +675,6 @@ export class CommandCenter {
|
||||
|
||||
if (!(resource instanceof Resource)) {
|
||||
// can happen when called from a keybinding
|
||||
console.log('WHAT');
|
||||
resource = this.getSCMResource();
|
||||
}
|
||||
|
||||
@@ -699,7 +700,13 @@ export class CommandCenter {
|
||||
viewColumn: ViewColumn.Active
|
||||
};
|
||||
|
||||
const document = await workspace.openTextDocument(uri);
|
||||
let document;
|
||||
try {
|
||||
document = await workspace.openTextDocument(uri);
|
||||
} catch (error) {
|
||||
await commands.executeCommand<void>('vscode.open', uri, opts);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if active text editor has same path as other editor. we cannot compare via
|
||||
// URI.toString() here because the schemas can be different. Instead we just go by path.
|
||||
@@ -738,6 +745,8 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
const HEAD = await this.getLeftResource(resource);
|
||||
const basename = path.basename(resource.resourceUri.fsPath);
|
||||
const title = `${basename} (HEAD)`;
|
||||
|
||||
if (!HEAD) {
|
||||
window.showWarningMessage(localize('HEAD not available', "HEAD version of '{0}' is not available.", path.basename(resource.resourceUri.fsPath)));
|
||||
@@ -748,7 +757,7 @@ export class CommandCenter {
|
||||
preview
|
||||
};
|
||||
|
||||
return await commands.executeCommand<void>('vscode.open', HEAD, opts);
|
||||
return await commands.executeCommand<void>('vscode.open', HEAD, opts, title);
|
||||
}
|
||||
|
||||
@command('git.openChange')
|
||||
@@ -1114,7 +1123,7 @@ export class CommandCenter {
|
||||
|
||||
if (scmResources.length === 1) {
|
||||
if (untrackedCount > 0) {
|
||||
message = localize('confirm delete', "Are you sure you want to DELETE {0}?", path.basename(scmResources[0].resourceUri.fsPath));
|
||||
message = localize('confirm delete', "Are you sure you want to DELETE {0}?\nThis is IRREVERSIBLE!\nThis file will be FOREVER LOST.", path.basename(scmResources[0].resourceUri.fsPath));
|
||||
yes = localize('delete file', "Delete file");
|
||||
} else {
|
||||
if (scmResources[0].type === Status.DELETED) {
|
||||
@@ -1133,7 +1142,7 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
if (untrackedCount > 0) {
|
||||
message = `${message}\n\n${localize('warn untracked', "This will DELETE {0} untracked files!", untrackedCount)}`;
|
||||
message = `${message}\n\n${localize('warn untracked', "This will DELETE {0} untracked files!\nThis is IRREVERSIBLE!\nThese files will be FOREVER LOST.", untrackedCount)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1174,7 +1183,7 @@ export class CommandCenter {
|
||||
await repository.clean(resources.map(r => r.resourceUri));
|
||||
return;
|
||||
} else if (resources.length === 1) {
|
||||
const message = localize('confirm delete', "Are you sure you want to DELETE {0}?", path.basename(resources[0].resourceUri.fsPath));
|
||||
const message = localize('confirm delete', "Are you sure you want to DELETE {0}?\nThis is IRREVERSIBLE!\nThis file will be FOREVER LOST.", path.basename(resources[0].resourceUri.fsPath));
|
||||
const yes = localize('delete file', "Delete file");
|
||||
const pick = await window.showWarningMessage(message, { modal: true }, yes);
|
||||
|
||||
@@ -1224,23 +1233,35 @@ export class CommandCenter {
|
||||
opts?: CommitOptions
|
||||
): Promise<boolean> {
|
||||
const config = workspace.getConfiguration('git', Uri.file(repository.root));
|
||||
const promptToSaveFilesBeforeCommit = config.get<boolean>('promptToSaveFilesBeforeCommit') === true;
|
||||
let promptToSaveFilesBeforeCommit = config.get<'always' | 'staged' | 'never'>('promptToSaveFilesBeforeCommit');
|
||||
|
||||
if (promptToSaveFilesBeforeCommit) {
|
||||
const unsavedTextDocuments = workspace.textDocuments
|
||||
// migration
|
||||
if (promptToSaveFilesBeforeCommit as any === true) {
|
||||
promptToSaveFilesBeforeCommit = 'always';
|
||||
} else if (promptToSaveFilesBeforeCommit as any === false) {
|
||||
promptToSaveFilesBeforeCommit = 'never';
|
||||
}
|
||||
|
||||
if (promptToSaveFilesBeforeCommit !== 'never') {
|
||||
let documents = workspace.textDocuments
|
||||
.filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath));
|
||||
|
||||
if (unsavedTextDocuments.length > 0) {
|
||||
const message = unsavedTextDocuments.length === 1
|
||||
? localize('unsaved files single', "The following file is unsaved: {0}.\n\nWould you like to save it before committing?", path.basename(unsavedTextDocuments[0].uri.fsPath))
|
||||
: localize('unsaved files', "There are {0} unsaved files.\n\nWould you like to save them before committing?", unsavedTextDocuments.length);
|
||||
if (promptToSaveFilesBeforeCommit === 'staged') {
|
||||
documents = documents
|
||||
.filter(d => repository.indexGroup.resourceStates.some(s => s.resourceUri.path === d.uri.fsPath));
|
||||
}
|
||||
|
||||
if (documents.length > 0) {
|
||||
const message = documents.length === 1
|
||||
? localize('unsaved files single', "The following file is unsaved and will not be included in the commit if you proceed: {0}.\n\nWould you like to save it before committing?", path.basename(documents[0].uri.fsPath))
|
||||
: localize('unsaved files', "There are {0} unsaved files.\n\nWould you like to save them before committing?", documents.length);
|
||||
const saveAndCommit = localize('save and commit', "Save All & Commit");
|
||||
const commit = localize('commit', "Commit Anyway");
|
||||
const pick = await window.showWarningMessage(message, { modal: true }, saveAndCommit, commit);
|
||||
|
||||
if (pick === saveAndCommit) {
|
||||
await Promise.all(unsavedTextDocuments.map(d => d.save()));
|
||||
await repository.status();
|
||||
await Promise.all(documents.map(d => d.save()));
|
||||
await repository.add(documents.map(d => d.uri));
|
||||
} else if (pick !== commit) {
|
||||
return false; // do not commit on cancel
|
||||
}
|
||||
@@ -1254,15 +1275,24 @@ export class CommandCenter {
|
||||
|
||||
// no changes, and the user has not configured to commit all in this case
|
||||
if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit) {
|
||||
const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;
|
||||
|
||||
if (!suggestSmartCommit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// prompt the user if we want to commit all or not
|
||||
const message = localize('no staged changes', "There are no staged changes to commit.\n\nWould you like to automatically stage all your changes and commit them directly?");
|
||||
const yes = localize('yes', "Yes");
|
||||
const always = localize('always', "Always");
|
||||
const pick = await window.showWarningMessage(message, { modal: true }, yes, always);
|
||||
const never = localize('never', "Never");
|
||||
const pick = await window.showWarningMessage(message, { modal: true }, yes, always, never);
|
||||
|
||||
if (pick === always) {
|
||||
config.update('enableSmartCommit', true, true);
|
||||
} else if (pick === never) {
|
||||
config.update('suggestSmartCommit', false, true);
|
||||
return false;
|
||||
} else if (pick !== yes) {
|
||||
return false; // do not commit on cancel
|
||||
}
|
||||
@@ -1300,6 +1330,10 @@ export class CommandCenter {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (opts.all && config.get<'all' | 'tracked'>('smartCommitChanges') === 'tracked') {
|
||||
opts.all = 'tracked';
|
||||
}
|
||||
|
||||
await repository.commit(message, opts);
|
||||
|
||||
const postCommitCommand = config.get<'none' | 'push' | 'sync'>('postCommitCommand');
|
||||
@@ -1349,19 +1383,6 @@ export class CommandCenter {
|
||||
await this.commitWithAnyInput(repository);
|
||||
}
|
||||
|
||||
@command('git.commitWithInput', { repository: true })
|
||||
async commitWithInput(repository: Repository): Promise<void> {
|
||||
if (!repository.inputBox.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const didCommit = await this.smartCommit(repository, async () => repository.inputBox.value);
|
||||
|
||||
if (didCommit) {
|
||||
repository.inputBox.value = await repository.getCommitTemplate();
|
||||
}
|
||||
}
|
||||
|
||||
@command('git.commitStaged', { repository: true })
|
||||
async commitStaged(repository: Repository): Promise<void> {
|
||||
await this.commitWithAnyInput(repository, { all: false });
|
||||
@@ -1486,12 +1507,12 @@ export class CommandCenter {
|
||||
await this._branch(repository, undefined, true);
|
||||
}
|
||||
|
||||
private async _branch(repository: Repository, defaultName?: string, from = false): Promise<void> {
|
||||
private async promptForBranchName(defaultName?: string): Promise<string> {
|
||||
const config = workspace.getConfiguration('git');
|
||||
const branchWhitespaceChar = config.get<string>('branchWhitespaceChar')!;
|
||||
const branchValidationRegex = config.get<string>('branchValidationRegex')!;
|
||||
const sanitize = (name: string) => name ?
|
||||
name.trim().replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, branchWhitespaceChar)
|
||||
name.trim().replace(/^-+/, '').replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, branchWhitespaceChar)
|
||||
: name;
|
||||
|
||||
const rawBranchName = defaultName || await window.showInputBox({
|
||||
@@ -1508,7 +1529,11 @@ export class CommandCenter {
|
||||
}
|
||||
});
|
||||
|
||||
const branchName = sanitize(rawBranchName || '');
|
||||
return sanitize(rawBranchName || '');
|
||||
}
|
||||
|
||||
private async _branch(repository: Repository, defaultName?: string, from = false): Promise<void> {
|
||||
const branchName = await this.promptForBranchName(defaultName);
|
||||
|
||||
if (!branchName) {
|
||||
return;
|
||||
@@ -1570,22 +1595,21 @@ 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 });
|
||||
const branchName = await this.promptForBranchName();
|
||||
|
||||
if (!name || name.trim().length === 0) {
|
||||
if (!branchName) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await repository.renameBranch(name);
|
||||
await repository.renameBranch(branchName);
|
||||
} 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));
|
||||
window.showErrorMessage(localize('branch already exists', "A branch named '{0}' already exists", branchName));
|
||||
return;
|
||||
default:
|
||||
throw err;
|
||||
@@ -1925,7 +1949,17 @@ export class CommandCenter {
|
||||
private async _sync(repository: Repository, rebase: boolean): Promise<void> {
|
||||
const HEAD = repository.HEAD;
|
||||
|
||||
if (!HEAD || !HEAD.upstream) {
|
||||
if (!HEAD) {
|
||||
return;
|
||||
} else if (!HEAD.upstream) {
|
||||
const branchName = HEAD.name;
|
||||
const message = localize('confirm publish branch', "The branch '{0}' has no upstream branch. Would you like to publish this branch?", branchName);
|
||||
const yes = localize('ok', "OK");
|
||||
const pick = await window.showWarningMessage(message, { modal: true }, yes);
|
||||
|
||||
if (pick === yes) {
|
||||
await this.publish(repository);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1957,8 +1991,16 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
@command('git.sync', { repository: true })
|
||||
sync(repository: Repository): Promise<void> {
|
||||
return this._sync(repository, false);
|
||||
async sync(repository: Repository): Promise<void> {
|
||||
try {
|
||||
await this._sync(repository, false);
|
||||
} catch (err) {
|
||||
if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@command('git._syncAll')
|
||||
@@ -1975,8 +2017,16 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
@command('git.syncRebase', { repository: true })
|
||||
syncRebase(repository: Repository): Promise<void> {
|
||||
return this._sync(repository, true);
|
||||
async syncRebase(repository: Repository): Promise<void> {
|
||||
try {
|
||||
await this._sync(repository, true);
|
||||
} catch (err) {
|
||||
if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@command('git.publish', { repository: true })
|
||||
|
||||
@@ -11,12 +11,15 @@ import * as which from 'which';
|
||||
import { EventEmitter } from 'events';
|
||||
import iconv = require('iconv-lite');
|
||||
import * as filetype from 'file-type';
|
||||
import { assign, groupBy, denodeify, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent } from './util';
|
||||
import { assign, groupBy, denodeify, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter } from './util';
|
||||
import { CancellationToken } from 'vscode';
|
||||
import { URI } from 'vscode-uri';
|
||||
import { detectEncoding } from './encoding';
|
||||
import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status } from './api/git';
|
||||
|
||||
// https://github.com/microsoft/vscode/issues/65693
|
||||
const MAX_CLI_LENGTH = 30000;
|
||||
|
||||
const readfile = denodeify<string, string | null, string>(fs.readFile);
|
||||
|
||||
export interface IGit {
|
||||
@@ -339,7 +342,7 @@ export class Git {
|
||||
}
|
||||
|
||||
async clone(url: string, parentPath: string, cancellationToken?: CancellationToken): Promise<string> {
|
||||
let baseFolderName = decodeURI(url).replace(/[\/]+$/, '').replace(/^.*\//, '').replace(/\.git$/, '') || 'repository';
|
||||
let baseFolderName = decodeURI(url).replace(/[\/]+$/, '').replace(/^.*[\/\\]/, '').replace(/\.git$/, '') || 'repository';
|
||||
let folderName = baseFolderName;
|
||||
let folderPath = path.join(parentPath, folderName);
|
||||
let count = 1;
|
||||
@@ -598,13 +601,13 @@ export function parseGitmodules(raw: string): Submodule[] {
|
||||
}
|
||||
|
||||
export function parseGitCommit(raw: string): Commit | null {
|
||||
const match = /^([0-9a-f]{40})\n(.*)\n(.*)\n([^]*)$/m.exec(raw.trim());
|
||||
const match = /^([0-9a-f]{40})\n(.*)\n(.*)(\n([^]*))?$/m.exec(raw.trim());
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parents = match[3] ? match[3].split(' ') : [];
|
||||
return { hash: match[1], message: match[4], parents, authorEmail: match[2] };
|
||||
return { hash: match[1], message: match[5], parents, authorEmail: match[2] };
|
||||
}
|
||||
|
||||
interface LsTreeElement {
|
||||
@@ -639,7 +642,7 @@ export function parseLsFiles(raw: string): LsFilesElement[] {
|
||||
}
|
||||
|
||||
export interface CommitOptions {
|
||||
all?: boolean;
|
||||
all?: boolean | 'tracked';
|
||||
amend?: boolean;
|
||||
signoff?: boolean;
|
||||
signCommit?: boolean;
|
||||
@@ -649,6 +652,7 @@ export interface CommitOptions {
|
||||
export interface PullOptions {
|
||||
unshallow?: boolean;
|
||||
tags?: boolean;
|
||||
readonly cancellationToken?: CancellationToken;
|
||||
}
|
||||
|
||||
export enum ForcePushMode {
|
||||
@@ -1077,8 +1081,16 @@ export class Repository {
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
async add(paths: string[]): Promise<void> {
|
||||
const args = ['add', '-A', '--'];
|
||||
async add(paths: string[], opts?: { update?: boolean }): Promise<void> {
|
||||
const args = ['add'];
|
||||
|
||||
if (opts && opts.update) {
|
||||
args.push('-u');
|
||||
} else {
|
||||
args.push('-A');
|
||||
}
|
||||
|
||||
args.push('--');
|
||||
|
||||
if (paths && paths.length) {
|
||||
args.push.apply(args, paths);
|
||||
@@ -1138,13 +1150,14 @@ export class Repository {
|
||||
args.push(treeish);
|
||||
}
|
||||
|
||||
if (paths && paths.length) {
|
||||
args.push('--');
|
||||
args.push.apply(args, paths);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.run(args);
|
||||
if (paths && paths.length > 0) {
|
||||
for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
|
||||
await this.run([...args, '--', ...chunk]);
|
||||
}
|
||||
} else {
|
||||
await this.run(args);
|
||||
}
|
||||
} catch (err) {
|
||||
if (/Please,? commit your changes or stash them/.test(err.stderr || '')) {
|
||||
err.gitErrorCode = GitErrorCodes.DirtyWorkTree;
|
||||
@@ -1280,11 +1293,17 @@ export class Repository {
|
||||
async clean(paths: string[]): Promise<void> {
|
||||
const pathsByGroup = groupBy(paths, p => path.dirname(p));
|
||||
const groups = Object.keys(pathsByGroup).map(k => pathsByGroup[k]);
|
||||
const tasks = groups.map(paths => () => this.run(['clean', '-f', '-q', '--'].concat(paths)));
|
||||
|
||||
for (let task of tasks) {
|
||||
await task();
|
||||
const limiter = new Limiter(5);
|
||||
const promises: Promise<any>[] = [];
|
||||
|
||||
for (const paths of groups) {
|
||||
for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
|
||||
promises.push(limiter.queue(() => this.run(['clean', '-f', '-q', '--', ...chunk])));
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
async undo(): Promise<void> {
|
||||
@@ -1401,7 +1420,7 @@ export class Repository {
|
||||
}
|
||||
|
||||
try {
|
||||
await this.run(args);
|
||||
await this.run(args, options);
|
||||
} catch (err) {
|
||||
if (/^CONFLICT \([^)]+\): \b/m.test(err.stdout || '')) {
|
||||
err.gitErrorCode = GitErrorCodes.Conflict;
|
||||
@@ -1674,13 +1693,16 @@ export class Repository {
|
||||
async getBranch(name: string): Promise<Branch> {
|
||||
if (name === 'HEAD') {
|
||||
return this.getHEAD();
|
||||
} else if (/^@/.test(name)) {
|
||||
const symbolicFullNameResult = await this.run(['rev-parse', '--symbolic-full-name', name]);
|
||||
const symbolicFullName = symbolicFullNameResult.stdout.trim();
|
||||
name = symbolicFullName || name;
|
||||
}
|
||||
|
||||
const result = await this.run(['rev-parse', name]);
|
||||
let result = await this.run(['rev-parse', name]);
|
||||
|
||||
if (!result.stdout && /^@/.test(name)) {
|
||||
const symbolicFullNameResult = await this.run(['rev-parse', '--symbolic-full-name', name]);
|
||||
name = symbolicFullNameResult.stdout.trim();
|
||||
|
||||
result = await this.run(['rev-parse', name]);
|
||||
}
|
||||
|
||||
if (!result.stdout) {
|
||||
return Promise.reject<Branch>(new Error('No such branch'));
|
||||
@@ -1737,7 +1759,7 @@ export class Repository {
|
||||
}
|
||||
|
||||
const raw = await readfile(templatePath, 'utf8');
|
||||
return raw.replace(/^\s*#.*$\n?/gm, '').trim();
|
||||
return raw.replace(/\n?#.*/g, '');
|
||||
|
||||
} catch (err) {
|
||||
return '';
|
||||
@@ -1750,8 +1772,11 @@ export class Repository {
|
||||
}
|
||||
|
||||
async updateSubmodules(paths: string[]): Promise<void> {
|
||||
const args = ['submodule', 'update', '--', ...paths];
|
||||
await this.run(args);
|
||||
const args = ['submodule', 'update', '--'];
|
||||
|
||||
for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
|
||||
await this.run([...args, ...chunk]);
|
||||
}
|
||||
}
|
||||
|
||||
async getSubmodules(): Promise<Submodule[]> {
|
||||
|
||||
@@ -12,7 +12,7 @@ import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { fromGitUri } from './uri';
|
||||
import { GitErrorCodes } from './api/git';
|
||||
import { GitErrorCodes, APIState as State } from './api/git';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -63,15 +63,22 @@ export class Model {
|
||||
|
||||
private possibleGitRepositoryPaths = new Set<string>();
|
||||
|
||||
private _onDidChangeState = new EventEmitter<State>();
|
||||
readonly onDidChangeState = this._onDidChangeState.event;
|
||||
|
||||
private _state: State = 'uninitialized';
|
||||
get state(): State { return this._state; }
|
||||
|
||||
setState(state: State): void {
|
||||
this._state = state;
|
||||
this._onDidChangeState.fire(state);
|
||||
}
|
||||
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(readonly git: Git, private globalState: Memento, private outputChannel: OutputChannel) {
|
||||
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('**');
|
||||
@@ -82,7 +89,15 @@ export class Model {
|
||||
const onPossibleGitRepositoryChange = filterEvent(onGitRepositoryChange, uri => !this.getRepository(uri));
|
||||
onPossibleGitRepositoryChange(this.onPossibleGitRepositoryChange, this, this.disposables);
|
||||
|
||||
this.scanWorkspaceFolders();
|
||||
this.doInitialScan().finally(() => this.setState('initialized'));
|
||||
}
|
||||
|
||||
private async doInitialScan(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] }),
|
||||
this.onDidChangeVisibleTextEditors(window.visibleTextEditors),
|
||||
this.scanWorkspaceFolders()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,8 +172,8 @@ export class Model {
|
||||
.filter(r => !activeRepositories.has(r!.repository))
|
||||
.filter(r => !(workspace.workspaceFolders || []).some(f => isDescendant(f.uri.fsPath, r!.repository.root))) as OpenRepository[];
|
||||
|
||||
possibleRepositoryFolders.forEach(p => this.openRepository(p.uri.fsPath));
|
||||
openRepositoriesToDispose.forEach(r => r.dispose());
|
||||
await Promise.all(possibleRepositoryFolders.map(p => this.openRepository(p.uri.fsPath)));
|
||||
}
|
||||
|
||||
private onDidChangeConfiguration(): void {
|
||||
@@ -175,7 +190,7 @@ export class Model {
|
||||
openRepositoriesToDispose.forEach(r => r.dispose());
|
||||
}
|
||||
|
||||
private onDidChangeVisibleTextEditors(editors: TextEditor[]): void {
|
||||
private async onDidChangeVisibleTextEditors(editors: TextEditor[]): Promise<void> {
|
||||
const config = workspace.getConfiguration('git');
|
||||
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
|
||||
|
||||
@@ -183,7 +198,7 @@ export class Model {
|
||||
return;
|
||||
}
|
||||
|
||||
editors.forEach(editor => {
|
||||
await Promise.all(editors.map(async editor => {
|
||||
const uri = editor.document.uri;
|
||||
|
||||
if (uri.scheme !== 'file') {
|
||||
@@ -196,8 +211,8 @@ export class Model {
|
||||
return;
|
||||
}
|
||||
|
||||
this.openRepository(path.dirname(uri.fsPath));
|
||||
});
|
||||
await this.openRepository(path.dirname(uri.fsPath));
|
||||
}));
|
||||
}
|
||||
|
||||
@sequentialize
|
||||
@@ -236,6 +251,7 @@ export class Model {
|
||||
const repository = new Repository(this.git.open(repositoryRoot, dotGit), this.globalState, this.outputChannel);
|
||||
|
||||
this.open(repository);
|
||||
await repository.status();
|
||||
} catch (err) {
|
||||
if (err.gitErrorCode === GitErrorCodes.NotAGitRepository) {
|
||||
return;
|
||||
@@ -421,4 +437,4 @@ export class Model {
|
||||
this.possibleGitRepositoryPaths.clear();
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { commands, Uri, Command, EventEmitter, Event, scm, SourceControl, SourceControlInputBox, SourceControlResourceGroup, SourceControlResourceState, SourceControlResourceDecorations, SourceControlInputBoxValidation, Disposable, ProgressLocation, window, workspace, WorkspaceEdit, ThemeColor, DecorationData, Memento, SourceControlInputBoxValidationType, OutputChannel, LogLevel, env } from 'vscode';
|
||||
import { commands, Uri, Command, EventEmitter, Event, scm, SourceControl, SourceControlInputBox, SourceControlResourceGroup, SourceControlResourceState, SourceControlResourceDecorations, SourceControlInputBoxValidation, Disposable, ProgressLocation, window, workspace, WorkspaceEdit, ThemeColor, DecorationData, Memento, SourceControlInputBoxValidationType, OutputChannel, LogLevel, env, ProgressOptions, CancellationToken } from 'vscode';
|
||||
import { Repository as BaseRepository, Commit, Stash, GitError, Submodule, CommitOptions, ForcePushMode } from './git';
|
||||
import { anyEvent, filterEvent, eventToPromise, dispose, find, isDescendant, IDisposable, onceEvent, EmptyDisposable, debounceEvent, combinedDisposable, watch, IFileWatcher } from './util';
|
||||
import { anyEvent, filterEvent, eventToPromise, dispose, find, isDescendant, IDisposable, onceEvent, EmptyDisposable, debounceEvent, combinedDisposable } from './util';
|
||||
import { memoize, throttle, debounce } from './decorators';
|
||||
import { toGitUri } from './uri';
|
||||
import { AutoFetcher } from './autofetch';
|
||||
@@ -14,6 +14,7 @@ import * as nls from 'vscode-nls';
|
||||
import * as fs from 'fs';
|
||||
import { StatusBarCommands } from './statusbar';
|
||||
import { Branch, Ref, Remote, RefType, GitErrorCodes, Status, LogOptions, Change } from './api/git';
|
||||
import { IFileWatcher, watch } from './watch';
|
||||
|
||||
const timeout = (millis: number) => new Promise(c => setTimeout(c, millis));
|
||||
|
||||
@@ -520,8 +521,8 @@ class DotGitWatcher implements IFileWatcher {
|
||||
this.transientDisposables.push(upstreamWatcher);
|
||||
upstreamWatcher.event(this.emitter.fire, this.emitter, this.transientDisposables);
|
||||
} catch (err) {
|
||||
if (env.logLevel <= LogLevel.Info) {
|
||||
this.outputChannel.appendLine(`Failed to watch ref '${upstreamPath}'. Ref is most likely packed.`);
|
||||
if (env.logLevel <= LogLevel.Error) {
|
||||
this.outputChannel.appendLine(`Failed to watch ref '${upstreamPath}', is most likely packed.\n${err.stack || err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -652,28 +653,42 @@ export class Repository implements Disposable {
|
||||
const onWorkspaceRepositoryFileChange = filterEvent(onWorkspaceFileChange, uri => isDescendant(repository.root, uri.fsPath));
|
||||
const onWorkspaceWorkingTreeFileChange = filterEvent(onWorkspaceRepositoryFileChange, uri => !/\/\.git($|\/)/.test(uri.path));
|
||||
|
||||
const dotGitFileWatcher = new DotGitWatcher(this, outputChannel);
|
||||
this.disposables.push(dotGitFileWatcher);
|
||||
let onDotGitFileChange: Event<Uri>;
|
||||
|
||||
try {
|
||||
const dotGitFileWatcher = new DotGitWatcher(this, outputChannel);
|
||||
onDotGitFileChange = dotGitFileWatcher.event;
|
||||
this.disposables.push(dotGitFileWatcher);
|
||||
} catch (err) {
|
||||
if (env.logLevel <= LogLevel.Error) {
|
||||
outputChannel.appendLine(`Failed to watch '${this.dotGit}', reverting to legacy API file watched. Some events might be lost.\n${err.stack || err}`);
|
||||
}
|
||||
|
||||
onDotGitFileChange = filterEvent(onWorkspaceRepositoryFileChange, uri => /\/\.git($|\/)/.test(uri.path));
|
||||
}
|
||||
|
||||
// FS changes should trigger `git status`:
|
||||
// - any change inside the repository working tree
|
||||
// - any change whithin the first level of the `.git` folder, except the folder itself and `index.lock`
|
||||
const onFileChange = anyEvent(onWorkspaceWorkingTreeFileChange, dotGitFileWatcher.event);
|
||||
const onFileChange = anyEvent(onWorkspaceWorkingTreeFileChange, onDotGitFileChange);
|
||||
onFileChange(this.onFileChange, this, this.disposables);
|
||||
|
||||
// Relevate repository changes should trigger virtual document change events
|
||||
dotGitFileWatcher.event(this._onDidChangeRepository.fire, this._onDidChangeRepository, this.disposables);
|
||||
onDotGitFileChange(this._onDidChangeRepository.fire, this._onDidChangeRepository, this.disposables);
|
||||
|
||||
this.disposables.push(new FileEventLogger(onWorkspaceWorkingTreeFileChange, dotGitFileWatcher.event, outputChannel));
|
||||
this.disposables.push(new FileEventLogger(onWorkspaceWorkingTreeFileChange, onDotGitFileChange, outputChannel));
|
||||
|
||||
const root = Uri.file(repository.root);
|
||||
this._sourceControl = scm.createSourceControl('git', 'Git', 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.acceptInputCommand = { command: 'git.commit', title: localize('commit', "Commit"), arguments: [this._sourceControl] };
|
||||
this._sourceControl.quickDiffProvider = this;
|
||||
this._sourceControl.inputBox.validateInput = this.validateInput.bind(this);
|
||||
this.disposables.push(this._sourceControl);
|
||||
|
||||
this.updateInputBoxPlaceholder();
|
||||
this.disposables.push(this.onDidRunGitStatus(() => this.updateInputBoxPlaceholder()));
|
||||
|
||||
this._mergeGroup = this._sourceControl.createResourceGroup('merge', localize('merge changes', "MERGE CHANGES"));
|
||||
this._indexGroup = this._sourceControl.createResourceGroup('index', localize('staged changes', "STAGED CHANGES"));
|
||||
this._workingTreeGroup = this._sourceControl.createResourceGroup('workingTree', localize('changes', "CHANGES"));
|
||||
@@ -713,8 +728,11 @@ export class Repository implements Disposable {
|
||||
const progressManager = new ProgressManager(this);
|
||||
this.disposables.push(progressManager);
|
||||
|
||||
const onDidChangeCountBadge = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.countBadge', root));
|
||||
onDidChangeCountBadge(this.setCountBadge, this, this.disposables);
|
||||
this.setCountBadge();
|
||||
|
||||
this.updateCommitTemplate();
|
||||
this.status();
|
||||
}
|
||||
|
||||
validateInput(text: string, position: number): SourceControlInputBoxValidation | undefined {
|
||||
@@ -896,7 +914,8 @@ export class Repository implements Disposable {
|
||||
if (this.rebaseCommit) {
|
||||
await this.run(Operation.RebaseContinue, async () => {
|
||||
if (opts.all) {
|
||||
await this.repository.add([]);
|
||||
const addOpts = opts.all === 'tracked' ? { update: true } : {};
|
||||
await this.repository.add([], addOpts);
|
||||
}
|
||||
|
||||
await this.repository.rebaseContinue();
|
||||
@@ -904,9 +923,11 @@ export class Repository implements Disposable {
|
||||
} else {
|
||||
await this.run(Operation.Commit, async () => {
|
||||
if (opts.all) {
|
||||
await this.repository.add([]);
|
||||
const addOpts = opts.all === 'tracked' ? { update: true } : {};
|
||||
await this.repository.add([], addOpts);
|
||||
}
|
||||
|
||||
delete opts.all;
|
||||
await this.repository.commit(message, opts);
|
||||
});
|
||||
}
|
||||
@@ -947,21 +968,9 @@ export class Repository implements Disposable {
|
||||
}
|
||||
});
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
if (toClean.length > 0) {
|
||||
promises.push(this.repository.clean(toClean));
|
||||
}
|
||||
|
||||
if (toCheckout.length > 0) {
|
||||
promises.push(this.repository.checkout('', toCheckout));
|
||||
}
|
||||
|
||||
if (submodulesToUpdate.length > 0) {
|
||||
promises.push(this.repository.updateSubmodules(submodulesToUpdate));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
await this.repository.clean(toClean);
|
||||
await this.repository.checkout('', toCheckout);
|
||||
await this.repository.updateSubmodules(submodulesToUpdate);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1141,11 +1150,22 @@ export class Repository implements Disposable {
|
||||
const config = workspace.getConfiguration('git', Uri.file(this.root));
|
||||
const fetchOnPull = config.get<boolean>('fetchOnPull');
|
||||
const tags = config.get<boolean>('pullTags');
|
||||
const supportCancellation = config.get<boolean>('supportCancellation');
|
||||
|
||||
if (fetchOnPull) {
|
||||
await this.repository.pull(rebase, undefined, undefined, { tags });
|
||||
const fn = fetchOnPull
|
||||
? async (cancellationToken?: CancellationToken) => await this.repository.pull(rebase, undefined, undefined, { tags, cancellationToken })
|
||||
: async (cancellationToken?: CancellationToken) => await this.repository.pull(rebase, remoteName, pullBranch, { tags, cancellationToken });
|
||||
|
||||
if (supportCancellation) {
|
||||
const opts: ProgressOptions = {
|
||||
location: ProgressLocation.Notification,
|
||||
title: localize('sync is unpredictable', "Syncing. Cancelling may cause serious damages to the repository"),
|
||||
cancellable: true
|
||||
};
|
||||
|
||||
await window.withProgress(opts, (_, token) => fn(token));
|
||||
} else {
|
||||
await this.repository.pull(rebase, remoteName, pullBranch, { tags });
|
||||
await fn();
|
||||
}
|
||||
|
||||
const remote = this.remotes.find(r => r.name === remoteName);
|
||||
@@ -1490,15 +1510,7 @@ export class Repository implements Disposable {
|
||||
this.workingTreeGroup.resourceStates = workingTree;
|
||||
|
||||
// set count badge
|
||||
const countBadge = workspace.getConfiguration('git').get<string>('countBadge');
|
||||
let count = merge.length + index.length + workingTree.length;
|
||||
|
||||
switch (countBadge) {
|
||||
case 'off': count = 0; break;
|
||||
case 'tracked': count = count - workingTree.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED).length; break;
|
||||
}
|
||||
|
||||
this._sourceControl.count = count;
|
||||
this.setCountBadge();
|
||||
|
||||
// Disable `Discard All Changes` for "fresh" repositories
|
||||
// https://github.com/Microsoft/vscode/issues/43066
|
||||
@@ -1512,6 +1524,18 @@ export class Repository implements Disposable {
|
||||
this._onDidChangeStatus.fire();
|
||||
}
|
||||
|
||||
private setCountBadge(): void {
|
||||
const countBadge = workspace.getConfiguration('git').get<string>('countBadge');
|
||||
let count = this.mergeGroup.resourceStates.length + this.indexGroup.resourceStates.length + this.workingTreeGroup.resourceStates.length;
|
||||
|
||||
switch (countBadge) {
|
||||
case 'off': count = 0; break;
|
||||
case 'tracked': count = count - this.workingTreeGroup.resourceStates.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED).length; break;
|
||||
}
|
||||
|
||||
this._sourceControl.count = count;
|
||||
}
|
||||
|
||||
private async getRebaseCommit(): Promise<Commit | undefined> {
|
||||
const rebaseHeadPath = path.join(this.repository.root, '.git', 'REBASE_HEAD');
|
||||
const rebaseApplyPath = path.join(this.repository.root, '.git', 'rebase-apply');
|
||||
@@ -1633,6 +1657,21 @@ export class Repository implements Disposable {
|
||||
return `${this.HEAD.behind}↓ ${this.HEAD.ahead}↑`;
|
||||
}
|
||||
|
||||
private updateInputBoxPlaceholder(): void {
|
||||
const HEAD = this.HEAD;
|
||||
|
||||
if (HEAD) {
|
||||
const tag = this.refs.filter(iref => iref.type === RefType.Tag && iref.commit === HEAD.commit)[0];
|
||||
const tagName = tag && tag.name;
|
||||
const head = HEAD.name || tagName || (HEAD.commit || '').substr(0, 8);
|
||||
|
||||
// '{0}' will be replaced by the corresponding key-command later in the process, which is why it needs to stay.
|
||||
this._sourceControl.inputBox.placeholder = localize('commitMessageWithHeadLabel', "Message ({0} to commit on '{1}')", "{0}", head);
|
||||
} else {
|
||||
this._sourceControl.inputBox.placeholder = localize('commitMessage', "Message ({0} to commit)");
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Disposable, Command, EventEmitter, Event, workspace, Uri } from 'vscode';
|
||||
import { Repository, Operation } from './repository';
|
||||
import { anyEvent, dispose } from './util';
|
||||
import { anyEvent, dispose, filterEvent } from './util';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Branch } from './api/git';
|
||||
|
||||
@@ -27,7 +27,7 @@ class CheckoutStatusBar {
|
||||
|
||||
return {
|
||||
command: 'git.checkout',
|
||||
tooltip: localize('checkout', 'Checkout...'),
|
||||
tooltip: `${this.repository.headLabel}`,
|
||||
title,
|
||||
arguments: [this.repository.sourceControl]
|
||||
};
|
||||
@@ -39,6 +39,7 @@ class CheckoutStatusBar {
|
||||
}
|
||||
|
||||
interface SyncStatusBarState {
|
||||
enabled: boolean;
|
||||
isSyncRunning: boolean;
|
||||
hasRemotes: boolean;
|
||||
HEAD: Branch | undefined;
|
||||
@@ -47,6 +48,7 @@ interface SyncStatusBarState {
|
||||
class SyncStatusBar {
|
||||
|
||||
private static StartState: SyncStatusBarState = {
|
||||
enabled: true,
|
||||
isSyncRunning: false,
|
||||
hasRemotes: false,
|
||||
HEAD: undefined
|
||||
@@ -66,9 +68,20 @@ class SyncStatusBar {
|
||||
constructor(private repository: Repository) {
|
||||
repository.onDidRunGitStatus(this.onModelChange, this, this.disposables);
|
||||
repository.onDidChangeOperations(this.onOperationsChange, this, this.disposables);
|
||||
|
||||
const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.enableStatusBarSync'));
|
||||
onEnablementChange(this.updateEnablement, this, this.disposables);
|
||||
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
|
||||
private updateEnablement(): void {
|
||||
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
|
||||
const enabled = config.get<boolean>('enableStatusBarSync', true);
|
||||
|
||||
this.state = { ... this.state, enabled };
|
||||
}
|
||||
|
||||
private onOperationsChange(): void {
|
||||
const isSyncRunning = this.repository.operations.isRunning(Operation.Sync) ||
|
||||
this.repository.operations.isRunning(Operation.Push) ||
|
||||
@@ -86,7 +99,7 @@ class SyncStatusBar {
|
||||
}
|
||||
|
||||
get command(): Command | undefined {
|
||||
if (!this.state.hasRemotes) {
|
||||
if (!this.state.enabled || !this.state.hasRemotes) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import 'mocha';
|
||||
import { GitStatusParser, parseGitCommit, parseGitmodules, parseLsTree, parseLsFiles } from '../git';
|
||||
import * as assert from 'assert';
|
||||
import { splitInChunks } from '../util';
|
||||
|
||||
suite('git', () => {
|
||||
suite('GitStatusParser', () => {
|
||||
@@ -292,4 +293,78 @@ This is a commit message.`;
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('splitInChunks', () => {
|
||||
test('unit tests', function () {
|
||||
assert.deepEqual(
|
||||
[...splitInChunks(['hello', 'there', 'cool', 'stuff'], 6)],
|
||||
[['hello'], ['there'], ['cool'], ['stuff']]
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[...splitInChunks(['hello', 'there', 'cool', 'stuff'], 10)],
|
||||
[['hello', 'there'], ['cool', 'stuff']]
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[...splitInChunks(['hello', 'there', 'cool', 'stuff'], 12)],
|
||||
[['hello', 'there'], ['cool', 'stuff']]
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[...splitInChunks(['hello', 'there', 'cool', 'stuff'], 14)],
|
||||
[['hello', 'there', 'cool'], ['stuff']]
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[...splitInChunks(['hello', 'there', 'cool', 'stuff'], 2000)],
|
||||
[['hello', 'there', 'cool', 'stuff']]
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 1)],
|
||||
[['0'], ['01'], ['012'], ['0'], ['01'], ['012'], ['0'], ['01'], ['012']]
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 2)],
|
||||
[['0'], ['01'], ['012'], ['0'], ['01'], ['012'], ['0'], ['01'], ['012']]
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 3)],
|
||||
[['0', '01'], ['012'], ['0', '01'], ['012'], ['0', '01'], ['012']]
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 4)],
|
||||
[['0', '01'], ['012', '0'], ['01'], ['012', '0'], ['01'], ['012']]
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 5)],
|
||||
[['0', '01'], ['012', '0'], ['01', '012'], ['0', '01'], ['012']]
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 6)],
|
||||
[['0', '01', '012'], ['0', '01', '012'], ['0', '01', '012']]
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 7)],
|
||||
[['0', '01', '012', '0'], ['01', '012', '0'], ['01', '012']]
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 8)],
|
||||
[['0', '01', '012', '0'], ['01', '012', '0', '01'], ['012']]
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 9)],
|
||||
[['0', '01', '012', '0', '01'], ['012', '0', '01', '012']]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, EventEmitter, Uri } from 'vscode';
|
||||
import { dirname, sep, join } from 'path';
|
||||
import { Event } from 'vscode';
|
||||
import { dirname, sep } from 'path';
|
||||
import { Readable } from 'stream';
|
||||
import * as fs from 'fs';
|
||||
import * as byline from 'byline';
|
||||
@@ -345,18 +345,69 @@ export function pathEquals(a: string, b: string): boolean {
|
||||
return a === b;
|
||||
}
|
||||
|
||||
export interface IFileWatcher extends IDisposable {
|
||||
readonly event: Event<Uri>;
|
||||
export function* splitInChunks(array: string[], maxChunkLength: number): IterableIterator<string[]> {
|
||||
let current: string[] = [];
|
||||
let length = 0;
|
||||
|
||||
for (const value of array) {
|
||||
let newLength = length + value.length;
|
||||
|
||||
if (newLength > maxChunkLength && current.length > 0) {
|
||||
yield current;
|
||||
current = [];
|
||||
newLength = value.length;
|
||||
}
|
||||
|
||||
current.push(value);
|
||||
length = newLength;
|
||||
}
|
||||
|
||||
if (current.length > 0) {
|
||||
yield current;
|
||||
}
|
||||
}
|
||||
|
||||
export function watch(location: string): IFileWatcher {
|
||||
const dotGitWatcher = fs.watch(location);
|
||||
const onDotGitFileChangeEmitter = new EventEmitter<Uri>();
|
||||
dotGitWatcher.on('change', (_, e) => onDotGitFileChangeEmitter.fire(Uri.file(join(location, e as string))));
|
||||
dotGitWatcher.on('error', err => console.error(err));
|
||||
|
||||
return new class implements IFileWatcher {
|
||||
event = onDotGitFileChangeEmitter.event;
|
||||
dispose() { dotGitWatcher.close(); }
|
||||
};
|
||||
interface ILimitedTaskFactory<T> {
|
||||
factory: () => Promise<T>;
|
||||
c: (value?: T | Promise<T>) => void;
|
||||
e: (error?: any) => void;
|
||||
}
|
||||
|
||||
export class Limiter<T> {
|
||||
|
||||
private runningPromises: number;
|
||||
private maxDegreeOfParalellism: number;
|
||||
private outstandingPromises: ILimitedTaskFactory<T>[];
|
||||
|
||||
constructor(maxDegreeOfParalellism: number) {
|
||||
this.maxDegreeOfParalellism = maxDegreeOfParalellism;
|
||||
this.outstandingPromises = [];
|
||||
this.runningPromises = 0;
|
||||
}
|
||||
|
||||
queue(factory: () => Promise<T>): Promise<T> {
|
||||
return new Promise<T>((c, e) => {
|
||||
this.outstandingPromises.push({ factory, c, e });
|
||||
this.consume();
|
||||
});
|
||||
}
|
||||
|
||||
private consume(): void {
|
||||
while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) {
|
||||
const iLimitedTask = this.outstandingPromises.shift()!;
|
||||
this.runningPromises++;
|
||||
|
||||
const promise = iLimitedTask.factory();
|
||||
promise.then(iLimitedTask.c, iLimitedTask.e);
|
||||
promise.then(() => this.consumed(), () => this.consumed());
|
||||
}
|
||||
}
|
||||
|
||||
private consumed(): void {
|
||||
this.runningPromises--;
|
||||
|
||||
if (this.outstandingPromises.length > 0) {
|
||||
this.consume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
extensions/git/src/watch.ts
Normal file
25
extensions/git/src/watch.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, EventEmitter, Uri } from 'vscode';
|
||||
import { join } from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { IDisposable } from './util';
|
||||
|
||||
export interface IFileWatcher extends IDisposable {
|
||||
readonly event: Event<Uri>;
|
||||
}
|
||||
|
||||
export function watch(location: string): IFileWatcher {
|
||||
const dotGitWatcher = fs.watch(location);
|
||||
const onDotGitFileChangeEmitter = new EventEmitter<Uri>();
|
||||
dotGitWatcher.on('change', (_, e) => onDotGitFileChangeEmitter.fire(Uri.file(join(location, e as string))));
|
||||
dotGitWatcher.on('error', err => console.error(err));
|
||||
|
||||
return new class implements IFileWatcher {
|
||||
event = onDotGitFileChangeEmitter.event;
|
||||
dispose() { dotGitWatcher.close(); }
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user