Merge branch 'master' into rievans/gitpushbranchwithsymbolfail

This commit is contained in:
João Moreno
2019-08-07 11:51:55 +02:00
committed by GitHub
3708 changed files with 246555 additions and 116781 deletions

View File

@@ -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 } 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';
@@ -76,6 +76,10 @@ export class ApiRepository implements Repository {
return this._repository.setConfig(key, value);
}
getGlobalConfig(key: string): Promise<string> {
return this._repository.getGlobalConfig(key);
}
getObjectDetails(treeish: string, path: string): Promise<{ mode: string; object: string; size: number; }> {
return this._repository.getObjectDetails(treeish, path);
}
@@ -104,19 +108,27 @@ export class ApiRepository implements Repository {
return this._repository.diff(cached);
}
diffWithHEAD(path: string): Promise<string> {
diffWithHEAD(): Promise<Change[]>;
diffWithHEAD(path: string): Promise<string>;
diffWithHEAD(path?: string): Promise<string | Change[]> {
return this._repository.diffWithHEAD(path);
}
diffWith(ref: string, path: string): Promise<string> {
diffWith(ref: string): Promise<Change[]>;
diffWith(ref: string, path: string): Promise<string>;
diffWith(ref: string, path?: string): Promise<string | Change[]> {
return this._repository.diffWith(ref, path);
}
diffIndexWithHEAD(path: string): Promise<string> {
diffIndexWithHEAD(): Promise<Change[]>;
diffIndexWithHEAD(path: string): Promise<string>;
diffIndexWithHEAD(path?: string): Promise<string | Change[]> {
return this._repository.diffIndexWithHEAD(path);
}
diffIndexWith(ref: string, path: string): Promise<string> {
diffIndexWith(ref: string): Promise<Change[]>;
diffIndexWith(ref: string, path: string): Promise<string>;
diffIndexWith(ref: string, path?: string): Promise<string | Change[]> {
return this._repository.diffIndexWith(ref, path);
}
@@ -124,7 +136,9 @@ export class ApiRepository implements Repository {
return this._repository.diffBlobs(object1, object2);
}
diffBetween(ref1: string, ref2: string, path: string): Promise<string> {
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
diffBetween(ref1: string, ref2: string, path?: string): Promise<string | Change[]> {
return this._repository.diffBetween(ref1, ref2, path);
}
@@ -168,17 +182,25 @@ export class ApiRepository implements Repository {
return this._repository.removeRemote(name);
}
fetch(remote?: string | undefined, ref?: string | undefined): Promise<void> {
return this._repository.fetch(remote, ref);
fetch(remote?: string | undefined, ref?: string | undefined, depth?: number | undefined): Promise<void> {
return this._repository.fetch(remote, ref, depth);
}
pull(): Promise<void> {
return this._repository.pull();
pull(unshallow?: boolean): Promise<void> {
return this._repository.pull(undefined, unshallow);
}
push(remoteName?: string, branchName?: string, setUpstream: boolean = false): Promise<void> {
return this._repository.pushTo(remoteName, branchName, setUpstream);
}
blame(path: string): Promise<string> {
return this._repository.blame(path);
}
log(options?: LogOptions): Promise<Commit[]> {
return this._repository.log(options);
}
}
export class ApiGit implements Git {
@@ -192,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));
}

View File

@@ -41,6 +41,7 @@ export interface Commit {
readonly hash: string;
readonly message: string;
readonly parents: string[];
readonly authorEmail?: string | undefined;
}
export interface Submodule {
@@ -110,6 +111,14 @@ export interface RepositoryUIState {
readonly onDidChange: Event<void>;
}
/**
* Log options.
*/
export interface LogOptions {
/** Max number of log entries to retrieve. If not specified, the default is 32. */
readonly maxEntries?: number;
}
export interface Repository {
readonly rootUri: Uri;
@@ -120,6 +129,7 @@ export interface Repository {
getConfigs(): Promise<{ key: string; value: string; }[]>;
getConfig(key: string): Promise<string>;
setConfig(key: string, value: string): Promise<string>;
getGlobalConfig(key: string): Promise<string>;
getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>;
detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }>;
@@ -131,11 +141,16 @@ export interface Repository {
apply(patch: string, reverse?: boolean): Promise<void>;
diff(cached?: boolean): Promise<string>;
diffWithHEAD(): Promise<Change[]>;
diffWithHEAD(path: string): Promise<string>;
diffWith(ref: string): Promise<Change[]>;
diffWith(ref: string, path: string): Promise<string>;
diffIndexWithHEAD(): Promise<Change[]>;
diffIndexWithHEAD(path: string): Promise<string>;
diffIndexWith(ref: string): Promise<Change[]>;
diffIndexWith(ref: string, path: string): Promise<string>;
diffBlobs(object1: string, object2: string): Promise<string>;
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
hashObject(data: string): Promise<string>;
@@ -153,12 +168,19 @@ export interface Repository {
addRemote(name: string, url: string): Promise<void>;
removeRemote(name: string): Promise<void>;
fetch(remote?: string, ref?: string): Promise<void>;
pull(): Promise<void>;
fetch(remote?: string, ref?: string, depth?: number): Promise<void>;
pull(unshallow?: boolean): Promise<void>;
push(remoteName?: string, branchName?: string, setUpstream?: boolean): Promise<void>;
blame(path: string): Promise<string>;
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>;
@@ -215,5 +237,6 @@ export const enum GitErrorCodes {
WrongCase = 'WrongCase',
CantLockRef = 'CantLockRef',
CantRebaseMultipleBranches = 'CantRebaseMultipleBranches',
PatchDoesNotApply = 'PatchDoesNotApply'
}
PatchDoesNotApply = 'PatchDoesNotApply',
NoPathFound = 'NoPathFound'
}

View File

@@ -65,7 +65,7 @@ export class Askpass implements Disposable {
return ipcHandlePath;
}
private onRequest(req: http.ServerRequest, res: http.ServerResponse): void {
private onRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
const chunks: string[] = [];
req.setEncoding('utf8');
req.on('data', (d: string) => chunks.push(d));

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workspace, Disposable, EventEmitter, Memento, window, MessageItem, ConfigurationTarget } from 'vscode';
import { workspace, Disposable, EventEmitter, Memento, window, MessageItem, ConfigurationTarget, Uri } from 'vscode';
import { Repository, Operation } from './repository';
import { eventToPromise, filterEvent, onceEvent } from './util';
import * as nls from 'vscode-nls';
@@ -60,7 +60,7 @@ export class AutoFetcher {
}
if (result === yes) {
const gitConfig = workspace.getConfiguration('git');
const gitConfig = workspace.getConfiguration('git', Uri.file(this.repository.root));
gitConfig.update('autofetch', true, ConfigurationTarget.Global);
}
@@ -68,7 +68,7 @@ export class AutoFetcher {
}
private onConfiguration(): void {
const gitConfig = workspace.getConfiguration('git');
const gitConfig = workspace.getConfiguration('git', Uri.file(this.repository.root));
if (gitConfig.get<boolean>('autofetch') === false) {
this.disable();
@@ -110,7 +110,7 @@ export class AutoFetcher {
return;
}
const period = workspace.getConfiguration('git').get<number>('autofetchPeriod', 180) * 1000;
const period = workspace.getConfiguration('git', Uri.file(this.repository.root)).get<number>('autofetchPeriod', 180) * 1000;
const timeout = new Promise(c => setTimeout(c, period));
const whenDisabled = eventToPromise(filterEvent(this.onDidChange, enabled => !enabled));

View File

@@ -56,7 +56,13 @@ class CheckoutRemoteHeadItem extends CheckoutItem {
return;
}
await repository.checkoutTracking(this.ref.name);
const branches = await repository.findTrackingBranches(this.ref.name);
if (branches.length > 0) {
await repository.checkout(branches[0].name!);
} else {
await repository.checkoutTracking(this.ref.name);
}
}
}
@@ -93,7 +99,21 @@ class CreateBranchItem implements QuickPickItem {
constructor(private cc: CommandCenter) { }
get label(): string { return localize('create branch', '$(plus) Create new branch'); }
get label(): string { return localize('create branch', '$(plus) Create new branch...'); }
get description(): string { return ''; }
get alwaysShow(): boolean { return true; }
async run(repository: Repository): Promise<void> {
await this.cc.branch(repository);
}
}
class CreateBranchFromItem implements QuickPickItem {
constructor(private cc: CommandCenter) { }
get label(): string { return localize('create branch from', '$(plus) Create new branch from...'); }
get description(): string { return ''; }
get alwaysShow(): boolean { return true; }
@@ -182,7 +202,7 @@ function createCheckoutItems(repository: Repository): CheckoutItem[] {
enum PushType {
Push,
PushTo,
PushTags,
PushFollowTags,
}
interface PushOptions {
@@ -430,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());
@@ -467,10 +489,10 @@ export class CommandCenter {
(_, token) => this.git.clone(url!, parentPath, token)
);
const choices = [];
let message = localize('proposeopen', "Would you like to open the cloned repository?");
const open = localize('openrepo', "Open Repository");
choices.push(open);
const open = localize('openrepo', "Open");
const openNewWindow = localize('openreponew', "Open in New Window");
const choices = [open, openNewWindow];
const addToWorkspace = localize('add', "Add to Workspace");
if (workspace.workspaceFolders) {
@@ -495,6 +517,8 @@ export class CommandCenter {
commands.executeCommand('vscode.openFolder', uri);
} else if (result === addToWorkspace) {
workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
} else if (result === openNewWindow) {
commands.executeCommand('vscode.openFolder', uri, true);
}
} catch (err) {
if (/already exists and is not an empty directory/.test(err && err.stderr || '')) {
@@ -579,10 +603,10 @@ export class CommandCenter {
await this.git.init(repositoryPath);
const choices = [];
let message = localize('proposeopen init', "Would you like to open the initialized repository?");
const open = localize('openrepo', "Open Repository");
choices.push(open);
const open = localize('openrepo', "Open");
const openNewWindow = localize('openreponew', "Open in New Window");
const choices = [open, openNewWindow];
if (!askToOpen) {
return;
@@ -601,6 +625,8 @@ export class CommandCenter {
commands.executeCommand('vscode.openFolder', uri);
} else if (result === addToWorkspace) {
workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
} else if (result === openNewWindow) {
commands.executeCommand('vscode.openFolder', uri, true);
} else {
await this.model.openRepository(repositoryPath);
}
@@ -653,10 +679,11 @@ export class CommandCenter {
}
if (resource) {
const resources = ([resource, ...resourceStates] as Resource[])
.filter(r => r.type !== Status.DELETED && r.type !== Status.INDEX_DELETED);
uris = resources.map(r => r.resourceUri);
uris = ([resource, ...resourceStates] as Resource[])
.filter(r => r.type !== Status.DELETED && r.type !== Status.INDEX_DELETED)
.map(r => r.resourceUri);
} else if (window.activeTextEditor) {
uris = [window.activeTextEditor.document.uri];
}
}
@@ -665,6 +692,7 @@ export class CommandCenter {
}
const activeTextEditor = window.activeTextEditor;
for (const uri of uris) {
const opts: TextDocumentShowOptions = {
preserveFocus,
@@ -711,6 +739,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)));
@@ -721,7 +751,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')
@@ -1087,7 +1117,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) {
@@ -1106,7 +1136,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)}`;
}
}
@@ -1147,7 +1177,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);
@@ -1228,14 +1258,23 @@ 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
}
@@ -1419,14 +1458,16 @@ export class CommandCenter {
await repository.checkout(treeish);
return true;
}
const createBranch = new CreateBranchItem(this);
const picks = [createBranch, ...createCheckoutItems(repository)];
const createBranch = new CreateBranchItem(this);
const createBranchFrom = new CreateBranchFromItem(this);
const picks = [createBranch, createBranchFrom, ...createCheckoutItems(repository)];
const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout');
const quickpick = window.createQuickPick();
quickpick.items = picks;
quickpick.placeholder = placeHolder;
quickpick.ignoreFocusOut = true;
quickpick.show();
const choice = await new Promise<QuickPickItem | undefined>(c => quickpick.onDidAccept(() => c(quickpick.activeItems[0])));
@@ -1438,6 +1479,8 @@ export class CommandCenter {
if (choice === createBranch) {
await this._branch(repository, quickpick.value);
} else if (choice === createBranchFrom) {
await this._branch(repository, quickpick.value, true);
} else {
await (choice as CheckoutItem).run(repository);
}
@@ -1450,16 +1493,20 @@ export class CommandCenter {
await this._branch(repository);
}
private async _branch(repository: Repository, defaultName?: string): Promise<void> {
@command('git.branchFrom', { repository: true })
async branchFrom(repository: Repository): Promise<void> {
await this._branch(repository, undefined, true);
}
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 = await window.showInputBox({
value: defaultName,
const rawBranchName = defaultName || await window.showInputBox({
placeHolder: localize('branch name', "Branch name"),
prompt: localize('provide branch name', "Please provide a branch name"),
ignoreFocusOut: true,
@@ -1473,21 +1520,31 @@ 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;
}
const picks = [new HEADItem(repository), ...createCheckoutItems(repository)];
const placeHolder = localize('select a ref to create a new branch from', 'Select a ref to create a new branch from');
const target = await window.showQuickPick(picks, { placeHolder });
let target = 'HEAD';
if (!target) {
return;
if (from) {
const picks = [new HEADItem(repository), ...createCheckoutItems(repository)];
const placeHolder = localize('select a ref to create a new branch from', 'Select a ref to create the \'{0}\' branch from', branchName);
const choice = await window.showQuickPick(picks, { placeHolder });
if (!choice) {
return;
}
target = choice.label;
}
await repository.branch(branchName, true, target.label);
await repository.branch(branchName, true, target);
}
@command('git.deleteBranch', { repository: true })
@@ -1529,22 +1586,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;
@@ -1722,10 +1778,8 @@ export class CommandCenter {
}
}
if (pushOptions.pushType === PushType.PushTags) {
await repository.pushTags(undefined, forcePushMode);
window.showInformationMessage(localize('push with tags success', "Successfully pushed with tags."));
if (pushOptions.pushType === PushType.PushFollowTags) {
await repository.pushFollowTags(undefined, forcePushMode);
return;
}
@@ -1782,13 +1836,13 @@ export class CommandCenter {
}
@command('git.pushWithTags', { repository: true })
async pushWithTags(repository: Repository): Promise<void> {
await this._push(repository, { pushType: PushType.PushTags });
async pushFollowTags(repository: Repository): Promise<void> {
await this._push(repository, { pushType: PushType.PushFollowTags });
}
@command('git.pushWithTagsForce', { repository: true })
async pushWithTagsForce(repository: Repository): Promise<void> {
await this._push(repository, { pushType: PushType.PushTags, forcePush: true });
async pushFollowTagsForce(repository: Repository): Promise<void> {
await this._push(repository, { pushType: PushType.PushFollowTags, forcePush: true });
}
@command('git.pushTo', { repository: true })
@@ -1801,10 +1855,86 @@ export class CommandCenter {
await this._push(repository, { pushType: PushType.PushTo, forcePush: true });
}
@command('git.addRemote', { repository: true })
async addRemote(repository: Repository): Promise<void> {
const remotes = repository.remotes;
const sanitize = (name: string) => {
name = name.trim();
return name && name.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, '-');
};
const resultName = await window.showInputBox({
placeHolder: localize('remote name', "Remote name"),
prompt: localize('provide remote name', "Please provide a remote name"),
ignoreFocusOut: true,
validateInput: (name: string) => {
if (sanitize(name)) {
return null;
}
return localize('remote name format invalid', "Remote name format invalid");
}
});
const name = sanitize(resultName || '');
if (!name) {
return;
}
if (remotes.find(r => r.name === name)) {
window.showErrorMessage(localize('remote already exists', "Remote '{0}' already exists.", name));
return;
}
const url = await window.showInputBox({
placeHolder: localize('remote url', "Remote URL"),
prompt: localize('provide remote URL', "Enter URL for remote \"{0}\"", name),
ignoreFocusOut: true
});
if (!url) {
return;
}
await repository.addRemote(name, url);
}
@command('git.removeRemote', { repository: true })
async removeRemote(repository: Repository): Promise<void> {
const remotes = repository.remotes;
if (remotes.length === 0) {
window.showErrorMessage(localize('no remotes added', "Your repository has no remotes."));
return;
}
const picks = remotes.map(r => r.name);
const placeHolder = localize('remove remote', "Pick a remote to remove");
const remoteName = await window.showQuickPick(picks, { placeHolder });
if (!remoteName) {
return;
}
await repository.removeRemote(remoteName);
}
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;
}
@@ -1836,8 +1966,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')
@@ -1854,8 +1992,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 })
@@ -2118,6 +2264,7 @@ export class CommandCenter {
uri = uri ? uri : (window.activeTextEditor && window.activeTextEditor.document.uri);
this.outputChannel.appendLine(`git.getSCMResource.uri ${uri && uri.toString()}`);
for (const r of this.model.repositories.map(r => r.root)) {
this.outputChannel.appendLine(`repo root ${r}`);
}

View File

@@ -20,7 +20,6 @@ class GitIgnoreDecorationProvider implements DecorationProvider {
private disposables: Disposable[] = [];
constructor(private model: Model) {
//todo@joh -> events when the ignore status actually changes, not only when the file changes
this.onDidChangeDecorations = fireEvent(anyEvent<any>(
filterEvent(workspace.onDidSaveTextDocument, e => e.fileName.endsWith('.gitignore')),
model.onDidOpenRepository,
@@ -119,7 +118,7 @@ class GitDecorationProvider implements DecorationProvider {
const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()]));
this.decorations = newDecorations;
this._onDidChangeDecorations.fire([...uris.values()].map(Uri.parse));
this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true)));
}
private collectDecorationData(group: GitResourceGroup, bucket: Map<string, DecorationData>): void {

View File

@@ -11,10 +11,14 @@ 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 } from './api/git';
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);
@@ -306,11 +310,15 @@ function getGitErrorCode(stderr: string): string | undefined {
return GitErrorCodes.BranchAlreadyExists;
} else if (/'.+' is not a valid branch name/.test(stderr)) {
return GitErrorCodes.InvalidBranchName;
} else if (/Please,? commit your changes or stash them/.test(stderr)) {
return GitErrorCodes.DirtyWorkTree;
}
return undefined;
}
const COMMIT_FORMAT = '%H\n%ae\n%P\n%B';
export class Git {
readonly path: string;
@@ -324,8 +332,8 @@ export class Git {
this.env = options.env || {};
}
open(repository: string): Repository {
return new Repository(this, repository);
open(repository: string, dotGit: string): Repository {
return new Repository(this, repository, dotGit);
}
async init(repository: string): Promise<void> {
@@ -334,7 +342,7 @@ export class Git {
}
async clone(url: string, parentPath: string, cancellationToken?: CancellationToken): Promise<string> {
let baseFolderName = decodeURI(url).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;
@@ -347,7 +355,7 @@ export class Git {
await mkdirp(parentPath);
try {
await this.exec(parentPath, ['clone', url, folderPath], { cancellationToken });
await this.exec(parentPath, ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath], { cancellationToken });
} catch (err) {
if (err.stderr) {
err.stderr = err.stderr.replace(/^Cloning.+$/m, '').trim();
@@ -365,6 +373,17 @@ export class Git {
return path.normalize(result.stdout.trim());
}
async getRepositoryDotGit(repositoryPath: string): Promise<string> {
const result = await this.exec(repositoryPath, ['rev-parse', '--git-dir']);
let dotGitPath = result.stdout.trim();
if (!path.isAbsolute(dotGitPath)) {
dotGitPath = path.join(repositoryPath, dotGitPath);
}
return path.normalize(dotGitPath);
}
async exec(cwd: string, args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
options = assign({ cwd }, options || {});
return await this._exec(args, options);
@@ -450,6 +469,7 @@ export interface Commit {
hash: string;
message: string;
parents: string[];
authorEmail?: string | undefined;
}
export class GitStatusParser {
@@ -552,7 +572,7 @@ export function parseGitmodules(raw: string): Submodule[] {
return;
}
const propertyMatch = /^\s*(\w+) = (.*)$/.exec(line);
const propertyMatch = /^\s*(\w+)\s+=\s+(.*)$/.exec(line);
if (!propertyMatch) {
return;
@@ -581,13 +601,13 @@ export function parseGitmodules(raw: string): Submodule[] {
}
export function parseGitCommit(raw: string): Commit | null {
const match = /^([0-9a-f]{40})\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[2] ? match[2].split(' ') : [];
return { hash: match[1], message: match[3], parents };
const parents = match[3] ? match[3].split(' ') : [];
return { hash: match[1], message: match[4], parents, authorEmail: match[2] };
}
interface LsTreeElement {
@@ -629,6 +649,12 @@ export interface CommitOptions {
empty?: boolean;
}
export interface PullOptions {
unshallow?: boolean;
tags?: boolean;
readonly cancellationToken?: CancellationToken;
}
export enum ForcePushMode {
Force,
ForceWithLease
@@ -638,7 +664,8 @@ export class Repository {
constructor(
private _git: Git,
private repositoryRoot: string
private repositoryRoot: string,
readonly dotGit: string
) { }
get git(): Git {
@@ -697,6 +724,41 @@ export class Repository {
});
}
async log(options?: LogOptions): Promise<Commit[]> {
const maxEntries = options && typeof options.maxEntries === 'number' && options.maxEntries > 0 ? options.maxEntries : 32;
const args = ['log', '-' + maxEntries, `--pretty=format:${COMMIT_FORMAT}%x00%x00`];
const gitResult = await this.run(args);
if (gitResult.exitCode) {
// An empty repo.
return [];
}
const s = gitResult.stdout;
const result: Commit[] = [];
let index = 0;
while (index < s.length) {
let nextIndex = s.indexOf('\x00\x00', index);
if (nextIndex === -1) {
nextIndex = s.length;
}
let entry = s.substr(index, nextIndex - index);
if (entry.startsWith('\n')) {
entry = entry.substring(1);
}
const commit = parseGitCommit(entry);
if (!commit) {
break;
}
result.push(commit);
index = nextIndex + 2;
}
return result;
}
async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false): Promise<string> {
const stdout = await this.buffer(object);
@@ -851,25 +913,53 @@ export class Repository {
return result.stdout;
}
async diffWithHEAD(path: string): Promise<string> {
diffWithHEAD(): Promise<Change[]>;
diffWithHEAD(path: string): Promise<string>;
diffWithHEAD(path?: string | undefined): Promise<string | Change[]>;
async diffWithHEAD(path?: string | undefined): Promise<string | Change[]> {
if (!path) {
return await this.diffFiles(false);
}
const args = ['diff', '--', path];
const result = await this.run(args);
return result.stdout;
}
async diffWith(ref: string, path: string): Promise<string> {
diffWith(ref: string): Promise<Change[]>;
diffWith(ref: string, path: string): Promise<string>;
diffWith(ref: string, path?: string | undefined): Promise<string | Change[]>;
async diffWith(ref: string, path?: string): Promise<string | Change[]> {
if (!path) {
return await this.diffFiles(false, ref);
}
const args = ['diff', ref, '--', path];
const result = await this.run(args);
return result.stdout;
}
async diffIndexWithHEAD(path: string): Promise<string> {
diffIndexWithHEAD(): Promise<Change[]>;
diffIndexWithHEAD(path: string): Promise<string>;
diffIndexWithHEAD(path?: string | undefined): Promise<string | Change[]>;
async diffIndexWithHEAD(path?: string): Promise<string | Change[]> {
if (!path) {
return await this.diffFiles(true);
}
const args = ['diff', '--cached', '--', path];
const result = await this.run(args);
return result.stdout;
}
async diffIndexWith(ref: string, path: string): Promise<string> {
diffIndexWith(ref: string): Promise<Change[]>;
diffIndexWith(ref: string, path: string): Promise<string>;
diffIndexWith(ref: string, path?: string | undefined): Promise<string | Change[]>;
async diffIndexWith(ref: string, path?: string): Promise<string | Change[]> {
if (!path) {
return await this.diffFiles(true, ref);
}
const args = ['diff', '--cached', ref, '--', path];
const result = await this.run(args);
return result.stdout;
@@ -881,13 +971,102 @@ export class Repository {
return result.stdout;
}
async diffBetween(ref1: string, ref2: string, path: string): Promise<string> {
const args = ['diff', `${ref1}...${ref2}`, '--', path];
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
diffBetween(ref1: string, ref2: string, path?: string | undefined): Promise<string | Change[]>;
async diffBetween(ref1: string, ref2: string, path?: string): Promise<string | Change[]> {
const range = `${ref1}...${ref2}`;
if (!path) {
return await this.diffFiles(false, range);
}
const args = ['diff', range, '--', path];
const result = await this.run(args);
return result.stdout.trim();
}
private async diffFiles(cached: boolean, ref?: string): Promise<Change[]> {
const args = ['diff', '--name-status', '-z', '--diff-filter=ADMR'];
if (cached) {
args.push('--cached');
}
if (ref) {
args.push(ref);
}
const gitResult = await this.run(args);
if (gitResult.exitCode) {
return [];
}
const entries = gitResult.stdout.split('\x00');
let index = 0;
const result: Change[] = [];
entriesLoop:
while (index < entries.length - 1) {
const change = entries[index++];
const resourcePath = entries[index++];
if (!change || !resourcePath) {
break;
}
const originalUri = URI.file(path.isAbsolute(resourcePath) ? resourcePath : path.join(this.repositoryRoot, resourcePath));
let status: Status = Status.UNTRACKED;
// Copy or Rename status comes with a number, e.g. 'R100'. We don't need the number, so we use only first character of the status.
switch (change[0]) {
case 'M':
status = Status.MODIFIED;
break;
case 'A':
status = Status.INDEX_ADDED;
break;
case 'D':
status = Status.DELETED;
break;
// Rename contains two paths, the second one is what the file is renamed/copied to.
case 'R':
if (index >= entries.length) {
break;
}
const newPath = entries[index++];
if (!newPath) {
break;
}
const uri = URI.file(path.isAbsolute(newPath) ? newPath : path.join(this.repositoryRoot, newPath));
result.push({
uri,
renameUri: uri,
originalUri,
status: Status.INDEX_RENAMED
});
continue;
default:
// Unknown status
break entriesLoop;
}
result.push({
status,
originalUri,
uri: originalUri,
renameUri: originalUri,
});
}
return result;
}
async getMergeBase(ref1: string, ref2: string): Promise<string> {
const args = ['merge-base', ref1, ref2];
const result = await this.run(args);
@@ -963,13 +1142,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;
@@ -1042,7 +1222,7 @@ export class Repository {
}
async branch(name: string, checkout: boolean, ref?: string): Promise<void> {
const args = checkout ? ['checkout', '-q', '-b', name] : ['branch', '-q', name];
const args = checkout ? ['checkout', '-q', '-b', name, '--no-track'] : ['branch', '-q', name];
if (ref) {
args.push(ref);
@@ -1100,11 +1280,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> {
@@ -1166,7 +1352,7 @@ export class Repository {
await this.run(args);
}
async fetch(options: { remote?: string, ref?: string, all?: boolean, prune?: boolean } = {}): Promise<void> {
async fetch(options: { remote?: string, ref?: string, all?: boolean, prune?: boolean, depth?: number } = {}): Promise<void> {
const args = ['fetch'];
if (options.remote) {
@@ -1183,6 +1369,9 @@ export class Repository {
args.push('--prune');
}
if (typeof options.depth === 'number') {
args.push(`--depth=${options.depth}`);
}
try {
await this.run(args);
@@ -1197,8 +1386,16 @@ export class Repository {
}
}
async pull(rebase?: boolean, remote?: string, branch?: string): Promise<void> {
const args = ['pull', '--tags'];
async pull(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise<void> {
const args = ['pull'];
if (options.tags) {
args.push('--tags');
}
if (options.unshallow) {
args.push('--unshallow');
}
if (rebase) {
args.push('-r');
@@ -1210,7 +1407,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;
@@ -1245,7 +1442,7 @@ export class Repository {
}
if (tags) {
args.push('--tags');
args.push('--follow-tags');
}
if (remote) {
@@ -1271,16 +1468,33 @@ export class Repository {
}
}
async blame(path: string): Promise<string> {
try {
const args = ['blame'];
args.push(path);
let result = await this.run(args);
return result.stdout.trim();
} catch (err) {
if (/^fatal: no such path/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.NoPathFound;
}
throw err;
}
}
async createStash(message?: string, includeUntracked?: boolean): Promise<void> {
try {
const args = ['stash', 'save'];
const args = ['stash', 'push'];
if (includeUntracked) {
args.push('-u');
}
if (message) {
args.push('--', message);
args.push('-m', message);
}
await this.run(args);
@@ -1388,6 +1602,14 @@ export class Repository {
}
}
async findTrackingBranches(upstreamBranch: string): Promise<Branch[]> {
const result = await this.run(['for-each-ref', '--format', '%(refname:short)%00%(upstream:short)', 'refs/heads']);
return result.stdout.trim().split('\n')
.map(line => line.trim().split('\0'))
.filter(([_, upstream]) => upstream === upstreamBranch)
.map(([ref]) => ({ name: ref, type: RefType.Head } as Branch));
}
async getRefs(): Promise<Ref[]> {
const result = await this.run(['for-each-ref', '--format', '%(refname) %(objectname)', '--sort', '-committerdate']);
@@ -1532,13 +1754,16 @@ export class Repository {
}
async getCommit(ref: string): Promise<Commit> {
const result = await this.run(['show', '-s', '--format=%H\n%P\n%B', ref]);
const result = await this.run(['show', '-s', `--format=${COMMIT_FORMAT}`, ref]);
return parseGitCommit(result.stdout) || Promise.reject<Commit>('bad commit format');
}
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[]> {

View File

@@ -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,26 +63,41 @@ 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('**');
this.disposables.push(fsWatcher);
const onWorkspaceChange = anyEvent(fsWatcher.onDidChange, fsWatcher.onDidCreate, fsWatcher.onDidDelete);
const onGitRepositoryChange = filterEvent(onWorkspaceChange, uri => /\/\.git\//.test(uri.path));
const onGitRepositoryChange = filterEvent(onWorkspaceChange, uri => /\/\.git/.test(uri.path));
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
@@ -232,9 +247,11 @@ export class Model {
return;
}
const repository = new Repository(this.git.open(repositoryRoot), this.globalState);
const dotGit = await this.git.getRepositoryDotGit(repositoryRoot);
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;
@@ -420,4 +437,4 @@ export class Model {
this.possibleGitRepositoryPaths.clear();
this.disposables = dispose(this.disposables);
}
}
}

View File

@@ -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 } 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 } 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';
@@ -13,7 +13,8 @@ import * as path from 'path';
import * as nls from 'vscode-nls';
import * as fs from 'fs';
import { StatusBarCommands } from './statusbar';
import { Branch, Ref, Remote, RefType, GitErrorCodes, Status } from './api/git';
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));
@@ -206,7 +207,7 @@ export class Resource implements SourceControlResourceState {
case Status.INDEX_ADDED:
case Status.INTENT_TO_ADD:
return new ThemeColor('gitDecoration.addedResourceForeground');
case Status.INDEX_RENAMED: // todo@joh - special color?
case Status.INDEX_RENAMED:
case Status.UNTRACKED:
return new ThemeColor('gitDecoration.untrackedResourceForeground');
case Status.IGNORED:
@@ -299,7 +300,10 @@ export const enum Operation {
GetObjectDetails = 'GetObjectDetails',
SubmoduleUpdate = 'SubmoduleUpdate',
RebaseContinue = 'RebaseContinue',
Apply = 'Apply'
FindTrackingBranches = 'GetTracking',
Apply = 'Apply',
Blame = 'Blame',
Log = 'Log',
}
function isReadOnly(operation: Operation): boolean {
@@ -444,6 +448,91 @@ class ProgressManager {
}
}
class FileEventLogger {
private eventDisposable: IDisposable = EmptyDisposable;
private logLevelDisposable: IDisposable = EmptyDisposable;
constructor(
private onWorkspaceWorkingTreeFileChange: Event<Uri>,
private onDotGitFileChange: Event<Uri>,
private outputChannel: OutputChannel
) {
this.logLevelDisposable = env.onDidChangeLogLevel(this.onDidChangeLogLevel, this);
this.onDidChangeLogLevel(env.logLevel);
}
private onDidChangeLogLevel(level: LogLevel): void {
this.eventDisposable.dispose();
if (level > LogLevel.Debug) {
return;
}
this.eventDisposable = combinedDisposable([
this.onWorkspaceWorkingTreeFileChange(uri => this.outputChannel.appendLine(`[debug] [wt] Change: ${uri.fsPath}`)),
this.onDotGitFileChange(uri => this.outputChannel.appendLine(`[debug] [.git] Change: ${uri.fsPath}`))
]);
}
dispose(): void {
this.eventDisposable.dispose();
this.logLevelDisposable.dispose();
}
}
class DotGitWatcher implements IFileWatcher {
readonly event: Event<Uri>;
private emitter = new EventEmitter<Uri>();
private transientDisposables: IDisposable[] = [];
private disposables: IDisposable[] = [];
constructor(
private repository: Repository,
private outputChannel: OutputChannel
) {
const rootWatcher = watch(repository.dotGit);
this.disposables.push(rootWatcher);
const filteredRootWatcher = filterEvent(rootWatcher.event, uri => !/\/\.git(\/index\.lock)?$/.test(uri.path));
this.event = anyEvent(filteredRootWatcher, this.emitter.event);
repository.onDidRunGitStatus(this.updateTransientWatchers, this, this.disposables);
this.updateTransientWatchers();
}
private updateTransientWatchers() {
this.transientDisposables = dispose(this.transientDisposables);
if (!this.repository.HEAD || !this.repository.HEAD.upstream) {
return;
}
this.transientDisposables = dispose(this.transientDisposables);
const { name, remote } = this.repository.HEAD.upstream;
const upstreamPath = path.join(this.repository.dotGit, 'refs', 'remotes', remote, name);
try {
const upstreamWatcher = watch(upstreamPath);
this.transientDisposables.push(upstreamWatcher);
upstreamWatcher.event(this.emitter.fire, this.emitter, this.transientDisposables);
} catch (err) {
if (env.logLevel <= LogLevel.Error) {
this.outputChannel.appendLine(`Failed to watch ref '${upstreamPath}', is most likely packed.\n${err.stack || err}`);
}
}
}
dispose() {
this.emitter.dispose();
this.transientDisposables = dispose(this.transientDisposables);
this.disposables = dispose(this.disposables);
}
}
export class Repository implements Disposable {
private _onDidChangeRepository = new EventEmitter<Uri>();
@@ -541,36 +630,52 @@ export class Repository implements Disposable {
return this.repository.root;
}
get dotGit(): string {
return this.repository.dotGit;
}
private isRepositoryHuge = false;
private didWarnAboutLimit = false;
private isFreshRepository: boolean | undefined = undefined;
private disposables: Disposable[] = [];
constructor(
private readonly repository: BaseRepository,
globalState: Memento
globalState: Memento,
outputChannel: OutputChannel
) {
const fsWatcher = workspace.createFileSystemWatcher('**');
this.disposables.push(fsWatcher);
const workspaceWatcher = workspace.createFileSystemWatcher('**');
this.disposables.push(workspaceWatcher);
const workspaceFilter = (uri: Uri) => isDescendant(repository.root, uri.fsPath);
const onWorkspaceDelete = filterEvent(fsWatcher.onDidDelete, workspaceFilter);
const onWorkspaceChange = filterEvent(anyEvent(fsWatcher.onDidChange, fsWatcher.onDidCreate), workspaceFilter);
const onRepositoryDotGitDelete = filterEvent(onWorkspaceDelete, uri => /\/\.git$/.test(uri.path));
const onRepositoryChange = anyEvent(onWorkspaceDelete, onWorkspaceChange);
const onWorkspaceFileChange = anyEvent(workspaceWatcher.onDidChange, workspaceWatcher.onDidCreate, workspaceWatcher.onDidDelete);
const onWorkspaceRepositoryFileChange = filterEvent(onWorkspaceFileChange, uri => isDescendant(repository.root, uri.fsPath));
const onWorkspaceWorkingTreeFileChange = filterEvent(onWorkspaceRepositoryFileChange, uri => !/\/\.git($|\/)/.test(uri.path));
// relevant repository changes are:
// - DELETE .git folder
// - ANY CHANGE within .git folder except .git itself and .git/index.lock
const onRelevantRepositoryChange = anyEvent(
onRepositoryDotGitDelete,
filterEvent(onRepositoryChange, uri => !/\/\.git(\/index\.lock)?$/.test(uri.path))
);
let onDotGitFileChange: Event<Uri>;
onRelevantRepositoryChange(this.onFSChange, this, this.disposables);
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}`);
}
const onRelevantGitChange = filterEvent(onRelevantRepositoryChange, uri => /\/\.git\//.test(uri.path));
onRelevantGitChange(this._onDidChangeRepository.fire, this._onDidChangeRepository, this.disposables);
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, onDotGitFileChange);
onFileChange(this.onFileChange, this, this.disposables);
// Relevate repository changes should trigger virtual document change events
onDotGitFileChange(this._onDidChangeRepository.fire, this._onDidChangeRepository, this.disposables);
this.disposables.push(new FileEventLogger(onWorkspaceWorkingTreeFileChange, onDotGitFileChange, outputChannel));
const root = Uri.file(repository.root);
this._sourceControl = scm.createSourceControl('git', 'Git', root);
@@ -580,9 +685,9 @@ export class Repository implements Disposable {
this._sourceControl.inputBox.validateInput = this.validateInput.bind(this);
this.disposables.push(this._sourceControl);
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"));
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"));
const updateIndexGroupVisibility = () => {
const config = workspace.getConfiguration('git', root);
@@ -620,7 +725,6 @@ export class Repository implements Disposable {
this.disposables.push(progressManager);
this.updateCommitTemplate();
this.status();
}
validateInput(text: string, position: number): SourceControlInputBoxValidation | undefined {
@@ -671,20 +775,6 @@ export class Repository implements Disposable {
}
}
// const subjectThreshold =
// Math.max(config.get<number>('inputValidationLength') || 50, config.get<number>('subjectValidationLength') || 50, 0) || 50;
if (line.length <= threshold) {
if (setting !== 'always') {
return;
@@ -726,10 +816,18 @@ export class Repository implements Disposable {
return this.run(Operation.Config, () => this.repository.config('local', key));
}
getGlobalConfig(key: string): Promise<string> {
return this.run(Operation.Config, () => this.repository.config('global', key));
}
setConfig(key: string, value: string): Promise<string> {
return this.run(Operation.Config, () => this.repository.config('local', key, value));
}
log(options?: LogOptions): Promise<Commit[]> {
return this.run(Operation.Log, () => this.repository.log(options));
}
@throttle
async status(): Promise<void> {
await this.run(Operation.Status);
@@ -739,19 +837,31 @@ export class Repository implements Disposable {
return this.run(Operation.Diff, () => this.repository.diff(cached));
}
diffWithHEAD(path: string): Promise<string> {
diffWithHEAD(): Promise<Change[]>;
diffWithHEAD(path: string): Promise<string>;
diffWithHEAD(path?: string | undefined): Promise<string | Change[]>;
diffWithHEAD(path?: string | undefined): Promise<string | Change[]> {
return this.run(Operation.Diff, () => this.repository.diffWithHEAD(path));
}
diffWith(ref: string, path: string): Promise<string> {
diffWith(ref: string): Promise<Change[]>;
diffWith(ref: string, path: string): Promise<string>;
diffWith(ref: string, path?: string | undefined): Promise<string | Change[]>;
diffWith(ref: string, path?: string): Promise<string | Change[]> {
return this.run(Operation.Diff, () => this.repository.diffWith(ref, path));
}
diffIndexWithHEAD(path: string): Promise<string> {
diffIndexWithHEAD(): Promise<Change[]>;
diffIndexWithHEAD(path: string): Promise<string>;
diffIndexWithHEAD(path?: string | undefined): Promise<string | Change[]>;
diffIndexWithHEAD(path?: string): Promise<string | Change[]> {
return this.run(Operation.Diff, () => this.repository.diffIndexWithHEAD(path));
}
diffIndexWith(ref: string, path: string): Promise<string> {
diffIndexWith(ref: string): Promise<Change[]>;
diffIndexWith(ref: string, path: string): Promise<string>;
diffIndexWith(ref: string, path?: string | undefined): Promise<string | Change[]>;
diffIndexWith(ref: string, path?: string): Promise<string | Change[]> {
return this.run(Operation.Diff, () => this.repository.diffIndexWith(ref, path));
}
@@ -759,7 +869,10 @@ export class Repository implements Disposable {
return this.run(Operation.Diff, () => this.repository.diffBlobs(object1, object2));
}
diffBetween(ref1: string, ref2: string, path: string): Promise<string> {
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
diffBetween(ref1: string, ref2: string, path?: string | undefined): Promise<string | Change[]>;
diffBetween(ref1: string, ref2: string, path?: string): Promise<string | Change[]> {
return this.run(Operation.Diff, () => this.repository.diffBetween(ref1, ref2, path));
}
@@ -844,21 +957,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);
});
}
@@ -898,6 +999,10 @@ export class Repository implements Disposable {
await this.run(Operation.CheckoutTracking, () => this.repository.checkout(treeish, [], { track: true }));
}
async findTrackingBranches(upstreamRef: string): Promise<Branch[]> {
return await this.run(Operation.FindTrackingBranches, () => this.repository.findTrackingBranches(upstreamRef));
}
async getCommit(ref: string): Promise<Commit> {
return await this.repository.getCommit(ref);
}
@@ -933,8 +1038,8 @@ export class Repository implements Disposable {
await this.run(Operation.Fetch, () => this.repository.fetch({ all: true }));
}
async fetch(remote?: string, ref?: string): Promise<void> {
await this.run(Operation.Fetch, () => this.repository.fetch({ remote, ref }));
async fetch(remote?: string, ref?: string, depth?: number): Promise<void> {
await this.run(Operation.Fetch, () => this.repository.fetch({ remote, ref, depth }));
}
@throttle
@@ -951,7 +1056,7 @@ export class Repository implements Disposable {
}
@throttle
async pull(head?: Branch): Promise<void> {
async pull(head?: Branch, unshallow?: boolean): Promise<void> {
let remote: string | undefined;
let branch: string | undefined;
@@ -960,19 +1065,20 @@ export class Repository implements Disposable {
branch = `${head.upstream.name}`;
}
return this.pullFrom(false, remote, branch);
return this.pullFrom(false, remote, branch, unshallow);
}
async pullFrom(rebase?: boolean, remote?: string, branch?: string): Promise<void> {
async pullFrom(rebase?: boolean, remote?: string, branch?: string, unshallow?: boolean): Promise<void> {
await this.run(Operation.Pull, async () => {
await this.maybeAutoStash(async () => {
const config = workspace.getConfiguration('git', Uri.file(this.root));
const fetchOnPull = config.get<boolean>('fetchOnPull');
const tags = config.get<boolean>('pullTags');
if (fetchOnPull) {
await this.repository.pull(rebase);
await this.repository.pull(rebase, undefined, undefined, { unshallow, tags });
} else {
await this.repository.pull(rebase, remote, branch);
await this.repository.pull(rebase, remote, branch, { unshallow, tags });
}
});
});
@@ -995,10 +1101,14 @@ export class Repository implements Disposable {
await this.run(Operation.Push, () => this.repository.push(remote, name, setUpstream, undefined, forcePushMode));
}
async pushTags(remote?: string, forcePushMode?: ForcePushMode): Promise<void> {
async pushFollowTags(remote?: string, forcePushMode?: ForcePushMode): Promise<void> {
await this.run(Operation.Push, () => this.repository.push(remote, undefined, false, true, forcePushMode));
}
async blame(path: string): Promise<string> {
return await this.run(Operation.Blame, () => this.repository.blame(path));
}
@throttle
sync(head: Branch): Promise<void> {
return this._sync(head, false);
@@ -1024,11 +1134,23 @@ export class Repository implements Disposable {
await this.maybeAutoStash(async () => {
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);
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);
await fn();
}
const remote = this.remotes.find(r => r.name === remoteName);
@@ -1141,7 +1263,7 @@ export class Repository implements Disposable {
}
// https://git-scm.com/docs/git-check-ignore#git-check-ignore--z
const child = this.repository.stream(['check-ignore', '-z', '--stdin'], { stdio: [null, null, null] });
const child = this.repository.stream(['check-ignore', '-v', '-z', '--stdin'], { stdio: [null, null, null] });
child.stdin.end(filePaths.join('\0'), 'utf8');
const onExit = (exitCode: number) => {
@@ -1149,8 +1271,7 @@ export class Repository implements Disposable {
// nothing ignored
resolve(new Set<string>());
} else if (exitCode === 0) {
// paths are separated by the null-character
resolve(new Set<string>(data.split('\0')));
resolve(new Set<string>(this.parseIgnoreCheck(data)));
} else {
if (/ is in submodule /.test(stderr)) {
reject(new GitError({ stdout: data, stderr, exitCode, gitErrorCode: GitErrorCodes.IsInSubmodule }));
@@ -1178,6 +1299,23 @@ export class Repository implements Disposable {
});
}
// Parses output of `git check-ignore -v -z` and returns only those paths
// that are actually ignored by git.
// Matches to a negative pattern (starting with '!') are filtered out.
// See also https://git-scm.com/docs/git-check-ignore#_output.
private parseIgnoreCheck(raw: string): string[] {
const ignored = [];
const elements = raw.split('\0');
for (let i = 0; i < elements.length; i += 4) {
const pattern = elements[i + 2];
const path = elements[i + 3];
if (pattern && !pattern.startsWith('!')) {
ignored.push(path);
}
}
return ignored;
}
private async run<T>(operation: Operation, runOperation: () => Promise<T> = () => Promise.resolve<any>(null)): Promise<T> {
if (this.state !== RepositoryState.Idle) {
throw new Error('Repository not initialized');
@@ -1415,7 +1553,7 @@ export class Repository implements Disposable {
return result;
}
private onFSChange(_uri: Uri): void {
private onFileChange(_uri: Uri): void {
const config = workspace.getConfiguration('git');
const autorefresh = config.get<boolean>('autorefresh');

View File

@@ -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', () => {
@@ -172,42 +173,59 @@ suite('git', () => {
{ name: 'deps/spdlog4', path: 'deps/spdlog4', url: 'https://github.com/gabime/spdlog4.git' }
]);
});
test('whitespace #74844', () => {
const sample = `[submodule "deps/spdlog"]
path = deps/spdlog
url = https://github.com/gabime/spdlog.git
`;
assert.deepEqual(parseGitmodules(sample), [
{ name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' }
]);
});
});
suite('parseGitCommit', () => {
test('single parent commit', function () {
const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1
john.doe@mail.com
8e5a374372b8393906c7e380dbb09349c5385554
This is a commit message.`;
assert.deepEqual(parseGitCommit(GIT_OUTPUT_SINGLE_PARENT), {
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: ['8e5a374372b8393906c7e380dbb09349c5385554']
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
authorEmail: 'john.doe@mail.com',
});
});
test('multiple parent commits', function () {
const GIT_OUTPUT_MULTIPLE_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1
john.doe@mail.com
8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217
This is a commit message.`;
assert.deepEqual(parseGitCommit(GIT_OUTPUT_MULTIPLE_PARENTS), {
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: ['8e5a374372b8393906c7e380dbb09349c5385554', 'df27d8c75b129ab9b178b386077da2822101b217']
parents: ['8e5a374372b8393906c7e380dbb09349c5385554', 'df27d8c75b129ab9b178b386077da2822101b217'],
authorEmail: 'john.doe@mail.com',
});
});
test('no parent commits', function () {
const GIT_OUTPUT_NO_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1
john.doe@mail.com
This is a commit message.`;
assert.deepEqual(parseGitCommit(GIT_OUTPUT_NO_PARENTS), {
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: []
parents: [],
authorEmail: 'john.doe@mail.com',
});
});
});
@@ -275,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']]
);
});
});
});

View File

@@ -343,4 +343,71 @@ export function pathEquals(a: string, b: string): boolean {
}
return a === b;
}
}
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;
}
}
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();
}
}
}

View 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(); }
};
}