Merge from vscode 52dcb723a39ae75bee1bd56b3312d7fcdc87aeed (#6719)

This commit is contained in:
Anthony Dresser
2019-08-12 21:31:51 -07:00
committed by GitHub
parent 00250839fc
commit 7eba8c4c03
616 changed files with 9472 additions and 7087 deletions

View File

@@ -238,5 +238,6 @@ export const enum GitErrorCodes {
CantLockRef = 'CantLockRef',
CantRebaseMultipleBranches = 'CantRebaseMultipleBranches',
PatchDoesNotApply = 'PatchDoesNotApply',
NoPathFound = 'NoPathFound'
NoPathFound = 'NoPathFound',
UnknownPath = 'UnknownPath',
}

View File

@@ -353,9 +353,11 @@ export class CommandCenter {
switch (resource.type) {
case Status.INDEX_MODIFIED:
case Status.INDEX_RENAMED:
case Status.INDEX_ADDED:
return this.getURI(resource.original, 'HEAD');
case Status.MODIFIED:
case Status.UNTRACKED:
return this.getURI(resource.resourceUri, '~');
case Status.DELETED_BY_THEM:
@@ -414,6 +416,7 @@ export class CommandCenter {
switch (resource.type) {
case Status.INDEX_MODIFIED:
case Status.INDEX_RENAMED:
case Status.INDEX_ADDED:
return `${basename} (Index)`;
case Status.MODIFIED:
@@ -426,6 +429,10 @@ export class CommandCenter {
case Status.DELETED_BY_THEM:
return `${basename} (Ours)`;
case Status.UNTRACKED:
return `${basename} (Untracked)`;
}
return '';
@@ -450,6 +457,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());
@@ -698,7 +707,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.
@@ -1225,23 +1240,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
}
@@ -1255,15 +1282,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
}
@@ -1301,6 +1337,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');
@@ -1350,19 +1390,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 });
@@ -1487,12 +1514,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({
@@ -1509,7 +1536,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;
@@ -1571,25 +1602,21 @@ export class CommandCenter {
@command('git.renameBranch', { repository: true })
async renameBranch(repository: Repository): Promise<void> {
const name = await window.showInputBox({
placeHolder: localize('branch name', "Branch name"),
prompt: localize('provide branch name', "Please provide a branch name"),
value: repository.HEAD && repository.HEAD.name
});
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;
@@ -1913,7 +1940,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;
}
@@ -1945,8 +1982,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')
@@ -1963,8 +2008,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 })

View File

@@ -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 {
@@ -797,7 +801,7 @@ export class Repository {
const elements = await this.lsfiles(path);
if (elements.length === 0) {
throw new GitError({ message: 'Error running ls-files' });
throw new GitError({ message: 'Path not known by git', gitErrorCode: GitErrorCodes.UnknownPath });
}
const { mode, object } = elements[0];
@@ -810,7 +814,7 @@ export class Repository {
const elements = await this.lstree(treeish, path);
if (elements.length === 0) {
throw new GitError({ message: 'Error running ls-files' });
throw new GitError({ message: 'Path not known by git', gitErrorCode: GitErrorCodes.UnknownPath });
}
const { mode, object, size } = elements[0];
@@ -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);
@@ -1116,15 +1128,21 @@ export class Repository {
}
let mode: string;
let add: string = '';
try {
const details = await this.getObjectDetails('HEAD', path);
mode = details.mode;
} catch (err) {
if (err.gitErrorCode !== GitErrorCodes.UnknownPath) {
throw err;
}
mode = '100644';
add = '--add';
}
await this.run(['update-index', '--cacheinfo', mode, hash, path]);
await this.run(['update-index', add, '--cacheinfo', mode, hash, path]);
}
async checkout(treeish: string, paths: string[], opts: { track?: boolean } = Object.create(null)): Promise<void> {
@@ -1138,13 +1156,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;
@@ -1275,11 +1294,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> {
@@ -1396,7 +1421,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;
@@ -1669,13 +1694,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'));
@@ -1732,7 +1760,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 '';
@@ -1745,8 +1773,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[]> {

View File

@@ -3,9 +3,9 @@
* Licensed under the Source EULA. 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));
@@ -678,12 +679,15 @@ export class Repository implements Disposable {
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"));
@@ -723,6 +727,10 @@ 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();
}
@@ -905,7 +913,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();
@@ -913,9 +922,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);
});
}
@@ -956,21 +967,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);
});
}
@@ -1146,11 +1145,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);
@@ -1495,15 +1505,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
@@ -1517,6 +1519,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');
@@ -1638,6 +1652,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);
}

View File

@@ -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;
}

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', () => {
@@ -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']]
);
});
});
});

View File

@@ -3,8 +3,8 @@
* Licensed under the Source EULA. 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();
}
}
}

View File

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