Merge VS Code 1.21 source code (#1067)

* Initial VS Code 1.21 file copy with patches

* A few more merges

* Post npm install

* Fix batch of build breaks

* Fix more build breaks

* Fix more build errors

* Fix more build breaks

* Runtime fixes 1

* Get connection dialog working with some todos

* Fix a few packaging issues

* Copy several node_modules to package build to fix loader issues

* Fix breaks from master

* A few more fixes

* Make tests pass

* First pass of license header updates

* Second pass of license header updates

* Fix restore dialog issues

* Remove add additional themes menu items

* fix select box issues where the list doesn't show up

* formatting

* Fix editor dispose issue

* Copy over node modules to correct location on all platforms
This commit is contained in:
Karl Burtram
2018-04-04 15:27:51 -07:00
committed by GitHub
parent 5fba3e31b4
commit dafb780987
9412 changed files with 141255 additions and 98813 deletions

View File

@@ -6,7 +6,7 @@
'use strict';
import { Model } from './model';
import { SourceControlInputBox, Uri } from 'vscode';
import { Uri } from 'vscode';
export interface InputBox {
value: string;

View File

@@ -5,7 +5,7 @@
'use strict';
import { workspace, Disposable, EventEmitter, Memento, window, MessageItem, ConfigurationTarget, commands, Uri } from 'vscode';
import { workspace, Disposable, EventEmitter, Memento, window, MessageItem, ConfigurationTarget } from 'vscode';
import { GitErrorCodes } from './git';
import { Repository, Operation } from './repository';
import { eventToPromise, filterEvent, onceEvent } from './util';
@@ -54,20 +54,14 @@ export class AutoFetcher {
}
const yes: MessageItem = { title: localize('yes', "Yes") };
const readMore: MessageItem = { title: localize('read more', "Read More") };
const no: MessageItem = { isCloseAffordance: true, title: localize('no', "No") };
const askLater: MessageItem = { title: localize('not now', "Ask Me Later") };
const result = await window.showInformationMessage(localize('suggest auto fetch', "Would you like Code to periodically run `git fetch`?"), yes, readMore, no, askLater);
const result = await window.showInformationMessage(localize('suggest auto fetch', "Would you like Code to [periodically run 'git fetch']({0})?", 'https://go.microsoft.com/fwlink/?linkid=865294'), yes, no, askLater);
if (result === askLater) {
return;
}
if (result === readMore) {
commands.executeCommand('vscode.open', Uri.parse('https://go.microsoft.com/fwlink/?linkid=865294'));
return this.onFirstGoodRemoteOperation();
}
if (result === yes) {
const gitConfig = workspace.getConfiguration('git');
gitConfig.update('autofetch', true, ConfigurationTarget.Global);

View File

@@ -10,9 +10,10 @@ import { Ref, RefType, Git, GitErrorCodes, Branch } from './git';
import { Repository, Resource, Status, CommitOptions, ResourceGroupType } from './repository';
import { Model } from './model';
import { toGitUri, fromGitUri } from './uri';
import { grep } from './util';
import { applyLineChanges, intersectDiffWithRange, toLineRanges, invertLineChange } from './staging';
import { grep, isDescendant } from './util';
import { applyLineChanges, intersectDiffWithRange, toLineRanges, invertLineChange, getModifiedRange } from './staging';
import * as path from 'path';
import { lstat, Stats } from 'fs';
import * as os from 'os';
import TelemetryReporter from 'vscode-extension-telemetry';
import * as nls from 'vscode-nls';
@@ -168,8 +169,28 @@ export class CommandCenter {
}
private async _openResource(resource: Resource, preview?: boolean, preserveFocus?: boolean, preserveSelection?: boolean): Promise<void> {
const left = await this.getLeftResource(resource);
const right = await this.getRightResource(resource);
let stat: Stats | undefined;
try {
stat = await new Promise<Stats>((c, e) => lstat(resource.resourceUri.fsPath, (err, stat) => err ? e(err) : c(stat)));
} catch (err) {
// noop
}
let left: Uri | undefined;
let right: Uri | undefined;
if (stat && stat.isDirectory()) {
const repository = this.model.getRepositoryForSubmodule(resource.resourceUri);
if (repository) {
right = toGitUri(resource.resourceUri, resource.resourceGroupType === ResourceGroupType.Index ? 'index' : 'wt', { submoduleOf: repository.root });
}
} else {
left = await this.getLeftResource(resource);
right = await this.getRightResource(resource);
}
const title = this.getTitle(resource);
if (!right) {
@@ -216,7 +237,7 @@ export class CommandCenter {
}
const { size, object } = await repository.lstree(gitRef, uri.fsPath);
const { mimetype, encoding } = await repository.detectObjectType(object);
const { mimetype } = await repository.detectObjectType(object);
if (mimetype === 'text/plain') {
return toGitUri(uri, ref);
@@ -330,7 +351,6 @@ export class CommandCenter {
const config = workspace.getConfiguration('git');
let value = config.get<string>('defaultCloneDirectory') || os.homedir();
value = value.replace(/^~/, os.homedir());
const parentPath = await window.showInputBox({
prompt: localize('parent', "Parent Directory"),
@@ -358,11 +378,10 @@ export class CommandCenter {
statusBarItem.command = cancelCommandId;
statusBarItem.show();
const clonePromise = this.git.clone(url, parentPath, tokenSource.token);
const clonePromise = this.git.clone(url, parentPath.replace(/^~/, os.homedir()), tokenSource.token);
try {
window.withProgress({ location: ProgressLocation.SourceControl, title: localize('cloning', "Cloning git repository...") }, () => clonePromise);
// window.withProgress({ location: ProgressLocation.Window, title: localize('cloning', "Cloning git repository...") }, () => clonePromise);
const repositoryPath = await clonePromise;
@@ -484,7 +503,10 @@ export class CommandCenter {
}
if (resource) {
uris = [...resourceStates.map(r => r.resourceUri), resource.resourceUri];
const resources = ([resource, ...resourceStates] as Resource[])
.filter(r => r.type !== Status.DELETED && r.type !== Status.INDEX_DELETED);
uris = resources.map(r => r.resourceUri);
}
}
@@ -511,6 +533,11 @@ export class CommandCenter {
}
}
@command('git.openFile2')
async openFile2(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
this.openFile(arg, ...resourceStates);
}
@command('git.openHEADFile')
async openHEADFile(arg?: Resource | Uri): Promise<void> {
let resource: Resource | undefined = undefined;
@@ -709,10 +736,7 @@ export class CommandCenter {
const modifiedDocument = textEditor.document;
const selections = textEditor.selections;
const selectedChanges = changes.filter(change => {
const modifiedRange = change.modifiedEndLineNumber === 0
? new Range(modifiedDocument.lineAt(change.modifiedStartLineNumber - 1).range.end, modifiedDocument.lineAt(change.modifiedStartLineNumber).range.start)
: new Range(modifiedDocument.lineAt(change.modifiedStartLineNumber - 1).range.start, modifiedDocument.lineAt(change.modifiedEndLineNumber - 1).range.end);
const modifiedRange = getModifiedRange(modifiedDocument, change);
return selections.every(selection => !selection.intersection(modifiedRange));
});
@@ -940,6 +964,29 @@ export class CommandCenter {
opts?: CommitOptions
): Promise<boolean> {
const config = workspace.getConfiguration('git');
const promptToSaveFilesBeforeCommit = config.get<boolean>('promptToSaveFilesBeforeCommit') === true;
if (promptToSaveFilesBeforeCommit) {
const unsavedTextDocuments = 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 comitting?", path.basename(unsavedTextDocuments[0].uri.fsPath))
: localize('unsaved files', "There are {0} unsaved files.\n\nWould you like to save them before comitting?", unsavedTextDocuments.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();
} else if (pick !== commit) {
return false; // do not commit on cancel
}
}
}
const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
@@ -963,6 +1010,8 @@ export class CommandCenter {
if (!opts) {
opts = { all: noStagedChanges };
} else if (!opts.all && noStagedChanges) {
opts = { ...opts, all: true };
}
// enable signing of commits if configurated
@@ -996,8 +1045,14 @@ export class CommandCenter {
return message;
}
let value: string | undefined = undefined;
if (opts && opts.amend && repository.HEAD && repository.HEAD.commit) {
value = (await repository.getCommit(repository.HEAD.commit)).message;
}
return await window.showInputBox({
value: opts && opts.defaultMsg,
value,
placeHolder: localize('commit message', "Commit message"),
prompt: localize('provide commit message', "Please provide a commit message"),
ignoreFocusOut: true
@@ -1041,15 +1096,7 @@ export class CommandCenter {
@command('git.commitStagedAmend', { repository: true })
async commitStagedAmend(repository: Repository): Promise<void> {
let msg;
if (repository.HEAD) {
if (repository.HEAD.commit) {
let id = repository.HEAD.commit;
let commit = await repository.getCommit(id);
msg = commit.message;
}
}
await this.commitWithAnyInput(repository, { all: false, amend: true, defaultMsg: msg });
await this.commitWithAnyInput(repository, { all: false, amend: true });
}
@command('git.commitAll', { repository: true })
@@ -1267,25 +1314,26 @@ export class CommandCenter {
return;
}
const picks = remotes.map(r => ({ label: r.name, description: r.url }));
const remotePicks = remotes.map(r => ({ label: r.name, description: r.url }));
const placeHolder = localize('pick remote pull repo', "Pick a remote to pull the branch from");
const pick = await window.showQuickPick(picks, { placeHolder });
const remotePick = await window.showQuickPick(remotePicks, { placeHolder });
if (!pick) {
if (!remotePick) {
return;
}
const branchName = await window.showInputBox({
placeHolder: localize('branch name', "Branch name"),
prompt: localize('provide branch name', "Please provide a branch name"),
ignoreFocusOut: true
});
const remoteRefs = repository.refs;
const remoteRefsFiltered = remoteRefs.filter(r => (r.remote === remotePick.label));
const branchPicks = remoteRefsFiltered.map(r => ({ label: r.name })) as { label: string; description: string }[];
const branchPick = await window.showQuickPick(branchPicks, { placeHolder });
if (!branchName) {
if (!branchPick) {
return;
}
repository.pull(false, pick.label, branchName);
const remoteCharCnt = remotePick.label.length;
repository.pull(false, remotePick.label, branchPick.label.slice(remoteCharCnt + 1));
}
@command('git.pull', { repository: true })
@@ -1321,7 +1369,27 @@ export class CommandCenter {
return;
}
await repository.push();
if (!repository.HEAD || !repository.HEAD.name) {
window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
return;
}
try {
await repository.push();
} catch (err) {
if (err.gitErrorCode !== GitErrorCodes.NoUpstreamBranch) {
throw err;
}
const branchName = repository.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);
}
}
}
@command('git.pushWithTags', { repository: true })
@@ -1377,7 +1445,7 @@ export class CommandCenter {
if (shouldPrompt) {
const message = localize('sync is unpredictable', "This action will push and pull commits to and from '{0}'.", HEAD.upstream);
const yes = localize('ok', "OK");
const neverAgain = localize('never again', "OK, Never Show Again");
const neverAgain = localize('never again', "OK, Don't Show Again");
const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);
if (pick === neverAgain) {
@@ -1465,7 +1533,10 @@ export class CommandCenter {
}
private async _stash(repository: Repository, includeUntracked = false): Promise<void> {
if (repository.workingTreeGroup.resourceStates.length === 0) {
const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
if (noUnstagedChanges && noStagedChanges) {
window.showInformationMessage(localize('no changes stash', "There are no changes to stash."));
return;
}
@@ -1641,13 +1712,18 @@ export class CommandCenter {
const isSingleResource = arg instanceof Uri;
const groups = resources.reduce((result, resource) => {
const repository = this.model.getRepository(resource);
let repository = this.model.getRepository(resource);
if (!repository) {
console.warn('Could not find git repository for ', resource);
return result;
}
// Could it be a submodule?
if (resource.fsPath === repository.root) {
repository = this.model.getRepositoryForSubmodule(resource) || repository;
}
const tuple = result.filter(p => p.repository === repository)[0];
if (tuple) {

View File

@@ -52,7 +52,7 @@ export class GitContentProvider {
return;
}
this._onDidChange.fire(toGitUri(uri, '', true));
this._onDidChange.fire(toGitUri(uri, '', { replaceFileExtension: true }));
}
@debounce(1100)
@@ -83,6 +83,18 @@ export class GitContentProvider {
}
async provideTextDocumentContent(uri: Uri): Promise<string> {
let { path, ref, submoduleOf } = fromGitUri(uri);
if (submoduleOf) {
const repository = this.model.getRepository(submoduleOf);
if (!repository) {
return '';
}
return await repository.diff(path, { cached: ref === 'index' });
}
const repository = this.model.getRepository(uri);
if (!repository) {
@@ -95,8 +107,6 @@ export class GitContentProvider {
this.cache[cacheKey] = cacheValue;
let { path, ref } = fromGitUri(uri);
if (ref === '~') {
const fileUri = Uri.file(path);
const uriString = fileUri.toString();

View File

@@ -6,35 +6,48 @@
'use strict';
import { window, workspace, Uri, Disposable, Event, EventEmitter, DecorationData, DecorationProvider, ThemeColor } from 'vscode';
import * as path from 'path';
import { Repository, GitResourceGroup, Status } from './repository';
import { Model } from './model';
import { debounce } from './decorators';
import { filterEvent } from './util';
import { filterEvent, dispose, anyEvent, fireEvent } from './util';
import { GitErrorCodes } from './git';
type Callback = { resolve: (status: boolean) => void, reject: (err: any) => void };
class GitIgnoreDecorationProvider implements DecorationProvider {
private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
readonly onDidChangeDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;
private checkIgnoreQueue = new Map<string, { resolve: (status: boolean) => void, reject: (err: any) => void }>();
readonly onDidChangeDecorations: Event<Uri[]>;
private queue = new Map<string, { repository: Repository; queue: Map<string, Callback>; }>();
private disposables: Disposable[] = [];
constructor(private repository: Repository) {
this.disposables.push(
window.registerDecorationProvider(this),
filterEvent(workspace.onDidSaveTextDocument, e => e.fileName.endsWith('.gitignore'))(_ => this._onDidChangeDecorations.fire())
//todo@joh -> events when the ignore status actually changes, not only when the file changes
);
}
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,
model.onDidCloseRepository
));
dispose(): void {
this.disposables.forEach(d => d.dispose());
this.checkIgnoreQueue.clear();
this.disposables.push(window.registerDecorationProvider(this));
}
provideDecoration(uri: Uri): Promise<DecorationData | undefined> {
const repository = this.model.getRepository(uri);
if (!repository) {
return Promise.resolve(undefined);
}
let queueItem = this.queue.get(repository.root);
if (!queueItem) {
queueItem = { repository, queue: new Map<string, Callback>() };
this.queue.set(repository.root, queueItem);
}
return new Promise<boolean>((resolve, reject) => {
this.checkIgnoreQueue.set(uri.fsPath, { resolve, reject });
queueItem!.queue.set(uri.fsPath, { resolve, reject });
this.checkIgnoreSoon();
}).then(ignored => {
if (ignored) {
@@ -48,23 +61,42 @@ class GitIgnoreDecorationProvider implements DecorationProvider {
@debounce(500)
private checkIgnoreSoon(): void {
const queue = new Map(this.checkIgnoreQueue.entries());
this.checkIgnoreQueue.clear();
this.repository.checkIgnore([...queue.keys()]).then(ignoreSet => {
for (const [key, value] of queue.entries()) {
value.resolve(ignoreSet.has(key));
}
}, err => {
console.error(err);
for (const [, value] of queue.entries()) {
value.reject(err);
}
});
const queue = new Map(this.queue.entries());
this.queue.clear();
for (const [, item] of queue) {
const paths = [...item.queue.keys()];
item.repository.checkIgnore(paths).then(ignoreSet => {
for (const [key, value] of item.queue.entries()) {
value.resolve(ignoreSet.has(key));
}
}, err => {
if (err.gitErrorCode !== GitErrorCodes.IsInSubmodule) {
console.error(err);
}
for (const [, value] of item.queue.entries()) {
value.reject(err);
}
});
}
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
this.queue.clear();
}
}
class GitDecorationProvider implements DecorationProvider {
private static SubmoduleDecorationData: DecorationData = {
title: 'Submodule',
abbreviation: 'S',
color: new ThemeColor('gitDecoration.submoduleResourceForeground')
};
private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
readonly onDidChangeDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;
@@ -80,6 +112,8 @@ class GitDecorationProvider implements DecorationProvider {
private onDidRunGitStatus(): void {
let newDecorations = new Map<string, DecorationData>();
this.collectSubmoduleDecorationData(newDecorations);
this.collectDecorationData(this.repository.indexGroup, newDecorations);
this.collectDecorationData(this.repository.workingTreeGroup, newDecorations);
this.collectDecorationData(this.repository.mergeGroup, newDecorations);
@@ -101,6 +135,12 @@ class GitDecorationProvider implements DecorationProvider {
});
}
private collectSubmoduleDecorationData(bucket: Map<string, DecorationData>): void {
for (const submodule of this.repository.submodules) {
bucket.set(Uri.file(path.join(this.repository.root, submodule.path)).toString(), GitDecorationProvider.SubmoduleDecorationData);
}
}
provideDecoration(uri: Uri): DecorationData | undefined {
return this.decorations.get(uri.toString());
}
@@ -113,17 +153,21 @@ class GitDecorationProvider implements DecorationProvider {
export class GitDecorations {
private configListener: Disposable;
private modelListener: Disposable[] = [];
private disposables: Disposable[] = [];
private modelDisposables: Disposable[] = [];
private providers = new Map<Repository, Disposable>();
constructor(private model: Model) {
this.configListener = workspace.onDidChangeConfiguration(e => e.affectsConfiguration('git.decorations.enabled') && this.update());
this.disposables.push(new GitIgnoreDecorationProvider(model));
const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.decorations.enabled'));
onEnablementChange(this.update, this, this.disposables);
this.update();
}
private update(): void {
const enabled = workspace.getConfiguration('git').get('decorations.enabled');
if (enabled) {
this.enable();
} else {
@@ -132,26 +176,25 @@ export class GitDecorations {
}
private enable(): void {
this.modelListener = [];
this.model.onDidOpenRepository(this.onDidOpenRepository, this, this.modelListener);
this.model.onDidCloseRepository(this.onDidCloseRepository, this, this.modelListener);
this.model.onDidOpenRepository(this.onDidOpenRepository, this, this.modelDisposables);
this.model.onDidCloseRepository(this.onDidCloseRepository, this, this.modelDisposables);
this.model.repositories.forEach(this.onDidOpenRepository, this);
}
private disable(): void {
this.modelListener.forEach(d => d.dispose());
this.modelDisposables = dispose(this.modelDisposables);
this.providers.forEach(value => value.dispose());
this.providers.clear();
}
private onDidOpenRepository(repository: Repository): void {
const provider = new GitDecorationProvider(repository);
const ignoreProvider = new GitIgnoreDecorationProvider(repository);
this.providers.set(repository, Disposable.from(provider, ignoreProvider));
this.providers.set(repository, provider);
}
private onDidCloseRepository(repository: Repository): void {
const provider = this.providers.get(repository);
if (provider) {
provider.dispose();
this.providers.delete(repository);
@@ -159,9 +202,7 @@ export class GitDecorations {
}
dispose(): void {
this.configListener.dispose();
this.modelListener.forEach(d => d.dispose());
this.providers.forEach(value => value.dispose);
this.providers.clear();
this.disable();
this.disposables = dispose(this.disposables);
}
}

View File

@@ -78,7 +78,7 @@ function _throttle<T>(fn: Function, key: string): Function {
export const throttle = decorate(_throttle);
function _sequentialize<T>(fn: Function, key: string): Function {
function _sequentialize(fn: Function, key: string): Function {
const currentKey = `__$sequence$${key}`;
return function (this: any, ...args: any[]) {

View File

@@ -16,7 +16,7 @@ import * as filetype from 'file-type';
import { assign, uniqBy, groupBy, denodeify, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent } from './util';
import { CancellationToken } from 'vscode';
const readfile = denodeify<string>(fs.readFile);
const readfile = denodeify<string, string | null, string>(fs.readFile);
export interface IGit {
path: string;
@@ -267,6 +267,7 @@ export class GitError {
this.message = data.error.message;
} else {
this.error = void 0;
this.message = '';
}
this.message = this.message || data.message || 'Git error';
@@ -325,7 +326,9 @@ export const GitErrorCodes = {
BranchAlreadyExists: 'BranchAlreadyExists',
NoLocalChanges: 'NoLocalChanges',
NoStashFound: 'NoStashFound',
LocalChangesOverwritten: 'LocalChangesOverwritten'
LocalChangesOverwritten: 'LocalChangesOverwritten',
NoUpstreamBranch: 'NoUpstreamBranch',
IsInSubmodule: 'IsInSubmodule'
};
function getGitErrorCode(stderr: string): string | undefined {
@@ -359,7 +362,6 @@ function getGitErrorCode(stderr: string): string | undefined {
export class Git {
private gitPath: string;
private version: string;
private env: any;
private _onOutput = new EventEmitter();
@@ -367,7 +369,6 @@ export class Git {
constructor(options: IGitOptions) {
this.gitPath = options.gitPath;
this.version = options.version;
this.env = options.env || {};
}
@@ -460,7 +461,7 @@ export class Git {
});
if (options.log !== false) {
this.log(`git ${args.join(' ')}\n`);
this.log(`> git ${args.join(' ')}\n`);
}
return cp.spawn(this.gitPath, args, options);
@@ -542,6 +543,72 @@ export class GitStatusParser {
}
}
export interface Submodule {
name: string;
path: string;
url: string;
}
export function parseGitmodules(raw: string): Submodule[] {
const regex = /\r?\n/g;
let position = 0;
let match: RegExpExecArray | null = null;
const result: Submodule[] = [];
let submodule: Partial<Submodule> = {};
function parseLine(line: string): void {
const sectionMatch = /^\s*\[submodule "([^"]+)"\]\s*$/.exec(line);
if (sectionMatch) {
if (submodule.name && submodule.path && submodule.url) {
result.push(submodule as Submodule);
}
const name = sectionMatch[1];
if (name) {
submodule = { name };
return;
}
}
if (!submodule) {
return;
}
const propertyMatch = /^\s*(\w+) = (.*)$/.exec(line);
if (!propertyMatch) {
return;
}
const [, key, value] = propertyMatch;
switch (key) {
case 'path': submodule.path = value; break;
case 'url': submodule.url = value; break;
}
}
while (match = regex.exec(raw)) {
parseLine(raw.substring(position, match.index));
position = match.index + match[0].length;
}
parseLine(raw.substring(position));
if (submodule.name && submodule.path && submodule.url) {
result.push(submodule as Submodule);
}
return result;
}
export interface DiffOptions {
cached?: boolean;
}
export class Repository {
constructor(
@@ -611,7 +678,7 @@ export class Repository {
return stdout;
}
async lstree(treeish: string, path: string): Promise<{ mode: number, object: string, size: number }> {
async lstree(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }> {
if (!treeish) { // index
const { stdout } = await this.run(['ls-files', '--stage', '--', path]);
@@ -625,7 +692,7 @@ export class Repository {
const catFile = await this.run(['cat-file', '-s', object]);
const size = parseInt(catFile.stdout);
return { mode: parseInt(mode), object, size };
return { mode, object, size };
}
const { stdout } = await this.run(['ls-tree', '-l', treeish, '--', path]);
@@ -637,7 +704,7 @@ export class Repository {
}
const [, mode, , object, size] = match;
return { mode: parseInt(mode), object, size: parseInt(size) };
return { mode, object, size: parseInt(size) };
}
async detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }> {
@@ -680,6 +747,19 @@ export class Repository {
}
}
async diff(path: string, options: DiffOptions = {}): Promise<string> {
const args = ['diff'];
if (options.cached) {
args.push('--cached');
}
args.push('--', path);
const result = await this.run(args);
return result.stdout;
}
async add(paths: string[]): Promise<void> {
const args = ['add', '-A', '--'];
@@ -706,7 +786,16 @@ export class Repository {
});
}
await this.run(['update-index', '--cacheinfo', '100644', hash, path]);
let mode: string;
try {
const details = await this.lstree('HEAD', path);
mode = details.mode;
} catch (err) {
mode = '100644';
}
await this.run(['update-index', '--cacheinfo', mode, hash, path]);
}
async checkout(treeish: string, paths: string[]): Promise<void> {
@@ -953,6 +1042,8 @@ export class Repository {
err.gitErrorCode = GitErrorCodes.PushRejected;
} else if (/Could not read from remote repository/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
} else if (/^fatal: The current branch .* has no upstream branch/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.NoUpstreamBranch;
}
throw err;
@@ -1067,7 +1158,7 @@ export class Repository {
}
async getRefs(): Promise<Ref[]> {
const result = await this.run(['for-each-ref', '--format', '%(refname) %(objectname)']);
const result = await this.run(['for-each-ref', '--format', '%(refname) %(objectname)', '--sort', '-committerdate']);
const fn = (line: string): Ref | null => {
let match: RegExpExecArray | null;
@@ -1186,4 +1277,24 @@ export class Repository {
return { hash: match[1], message: match[2] };
}
async updateSubmodules(paths: string[]): Promise<void> {
const args = ['submodule', 'update', '--', ...paths];
await this.run(args);
}
async getSubmodules(): Promise<Submodule[]> {
const gitmodulesPath = path.join(this.root, '.gitmodules');
try {
const gitmodulesRaw = await readfile(gitmodulesPath, 'utf8');
return parseGitmodules(gitmodulesRaw);
} catch (err) {
if (/ENOENT/.test(err.message)) {
return [];
}
throw err;
}
}
}

View File

@@ -6,7 +6,7 @@
'use strict';
import * as nls from 'vscode-nls';
const localize = nls.config(process.env.VSCODE_NLS_CONFIG)();
const localize = nls.loadMessageBundle();
import { ExtensionContext, workspace, window, Disposable, commands, Uri, OutputChannel } from 'vscode';
import { findGit, Git, IGit } from './git';
import { Model } from './model';
@@ -14,21 +14,19 @@ import { CommandCenter } from './commands';
import { GitContentProvider } from './contentProvider';
import { GitDecorations } from './decorationProvider';
import { Askpass } from './askpass';
import { toDisposable } from './util';
import { toDisposable, filterEvent, eventToPromise } from './util';
import TelemetryReporter from 'vscode-extension-telemetry';
import { API, createApi } from './api';
async function init(context: ExtensionContext, outputChannel: OutputChannel, disposables: Disposable[]): Promise<Model> {
const { name, version, aiKey } = require(context.asAbsolutePath('./package.json')) as { name: string, version: string, aiKey: string };
const telemetryReporter: TelemetryReporter = new TelemetryReporter(name, version, aiKey);
disposables.push(telemetryReporter);
let telemetryReporter: TelemetryReporter;
async function init(context: ExtensionContext, outputChannel: OutputChannel, disposables: Disposable[]): Promise<Model> {
const pathHint = workspace.getConfiguration('git').get<string>('path');
const info = await findGit(pathHint, path => outputChannel.appendLine(localize('looking', "Looking for git in: {0}", path)));
const askpass = new Askpass();
const env = await askpass.getEnv();
const git = new Git({ gitPath: info.path, version: info.version, env });
const model = new Model(git, context.globalState);
const model = new Model(git, context.globalState, outputChannel);
disposables.push(model);
const onRepository = () => commands.executeCommand('setContext', 'gitOpenRepositoryCount', `${model.repositories.length}`);
@@ -38,7 +36,15 @@ async function init(context: ExtensionContext, outputChannel: OutputChannel, dis
outputChannel.appendLine(localize('using git', "Using git {0} from {1}", info.version, info.path));
const onOutput = (str: string) => outputChannel.append(str);
const onOutput = (str: string) => {
const lines = str.split(/\r?\n/mg);
while (/^\s*$/.test(lines[lines.length - 1])) {
lines.pop();
}
outputChannel.appendLine(lines.join('\n'));
};
git.onOutput.addListener('log', onOutput);
disposables.push(toDisposable(() => git.onOutput.removeListener('log', onOutput)));
@@ -77,7 +83,7 @@ async function _activate(context: ExtensionContext, disposables: Disposable[]):
outputChannel.show();
const download = localize('downloadgit', "Download Git");
const neverShowAgain = localize('neverShowAgain', "Don't show again");
const neverShowAgain = localize('neverShowAgain', "Don't Show Again");
const choice = await window.showWarningMessage(
localize('notfound', "Git not found. Install it or configure it using the 'git.path' setting."),
download,
@@ -93,11 +99,30 @@ async function _activate(context: ExtensionContext, disposables: Disposable[]):
}
export function activate(context: ExtensionContext): API {
const config = workspace.getConfiguration('git', null);
const enabled = config.get<boolean>('enabled');
const disposables: Disposable[] = [];
context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose()));
const activatePromise = _activate(context, disposables);
const modelPromise = activatePromise.then(model => model || Promise.reject<Model>('Git model not found'));
const { name, version, aiKey } = require(context.asAbsolutePath('./package.json')) as { name: string, version: string, aiKey: string };
telemetryReporter = new TelemetryReporter(name, version, aiKey);
let activatePromise: Promise<Model | undefined>;
if (enabled) {
activatePromise = _activate(context, disposables);
} else {
const onConfigChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git'));
const onEnabled = filterEvent(onConfigChange, () => workspace.getConfiguration('git', null).get<boolean>('enabled') === true);
activatePromise = eventToPromise(onEnabled)
.then(() => _activate(context, disposables));
}
const modelPromise = activatePromise
.then(model => model || Promise.reject<Model>('Git model not found'));
activatePromise.catch(err => console.error(err));
return createApi(modelPromise);
@@ -123,7 +148,7 @@ async function checkGitVersion(info: IGit): Promise<void> {
}
const update = localize('updateGit', "Update Git");
const neverShowAgain = localize('neverShowAgain', "Don't show again");
const neverShowAgain = localize('neverShowAgain', "Don't Show Again");
const choice = await window.showWarningMessage(
localize('git20', "You seem to have git {0} installed. Code works best with git >= 2", info.version),
@@ -139,3 +164,7 @@ async function checkGitVersion(info: IGit): Promise<void> {
// {{SQL CARBON EDIT}}
*/
}
export function deactivate(): Promise<any> {
return telemetryReporter ? telemetryReporter.dispose() : Promise.resolve(null);
}

View File

@@ -5,10 +5,10 @@
'use strict';
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, ConfigurationChangeEvent } from 'vscode';
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, OutputChannel } from 'vscode';
import { Repository, RepositoryState } from './repository';
import { memoize, sequentialize, debounce } from './decorators';
import { dispose, anyEvent, filterEvent, IDisposable, isDescendant } from './util';
import { dispose, anyEvent, filterEvent, isDescendant, firstIndex } from './util';
import { Git, GitErrorCodes } from './git';
import * as path from 'path';
import * as fs from 'fs';
@@ -28,7 +28,7 @@ class RepositoryPick implements QuickPickItem {
.join(' ');
}
constructor(public readonly repository: Repository) { }
constructor(public readonly repository: Repository, public readonly index: number) { }
}
export interface ModelChangeEvent {
@@ -66,7 +66,7 @@ export class Model {
private disposables: Disposable[] = [];
constructor(private git: Git, private globalState: Memento) {
constructor(private git: Git, private globalState: Memento, private outputChannel: OutputChannel) {
workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables);
this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] });
@@ -93,17 +93,25 @@ export class Model {
private async scanWorkspaceFolders(): Promise<void> {
for (const folder of workspace.workspaceFolders || []) {
const root = folder.uri.fsPath;
const children = await new Promise<string[]>((c, e) => fs.readdir(root, (err, r) => err ? e(err) : c(r)));
children
.filter(child => child !== '.git')
.forEach(child => this.tryOpenRepository(path.join(root, child)));
try {
const children = await new Promise<string[]>((c, e) => fs.readdir(root, (err, r) => err ? e(err) : c(r)));
children
.filter(child => child !== '.git')
.forEach(child => this.tryOpenRepository(path.join(root, child)));
} catch (err) {
// noop
}
}
}
private onPossibleGitRepositoryChange(uri: Uri): void {
const possibleGitRepositoryPath = uri.fsPath.replace(/\.git.*$/, '');
this.possibleGitRepositoryPaths.add(possibleGitRepositoryPath);
this.eventuallyScanPossibleGitRepository(uri.fsPath.replace(/\.git.*$/, ''));
}
private eventuallyScanPossibleGitRepository(path: string) {
this.possibleGitRepositoryPaths.add(path);
this.eventuallyScanPossibleGitRepositories();
}
@@ -150,6 +158,13 @@ export class Model {
}
private onDidChangeVisibleTextEditors(editors: TextEditor[]): void {
const config = workspace.getConfiguration('git');
const enabled = config.get<boolean>('autoRepositoryDetection') === true;
if (!enabled) {
return;
}
editors.forEach(editor => {
const uri = editor.document.uri;
@@ -181,11 +196,13 @@ export class Model {
}
try {
const repositoryRoot = await this.git.getRepositoryRoot(path);
const rawRoot = await this.git.getRepositoryRoot(path);
// This can happen whenever `path` has the wrong case sensitivity in
// case insensitive file systems
// https://github.com/Microsoft/vscode/issues/33498
const repositoryRoot = Uri.file(rawRoot).fsPath;
if (this.getRepository(repositoryRoot)) {
return;
}
@@ -203,15 +220,31 @@ export class Model {
}
private open(repository: Repository): void {
this.outputChannel.appendLine(`Open repository: ${repository.root}`);
const onDidDisappearRepository = filterEvent(repository.onDidChangeState, state => state === RepositoryState.Disposed);
const disappearListener = onDidDisappearRepository(() => dispose());
const changeListener = repository.onDidChangeRepository(uri => this._onDidChangeRepository.fire({ repository, uri }));
const originalResourceChangeListener = repository.onDidChangeOriginalResource(uri => this._onDidChangeOriginalResource.fire({ repository, uri }));
const checkForSubmodules = () => {
if (repository.submodules.length > 10) {
window.showWarningMessage(localize('too many submodules', "The '{0}' repository has {1} submodules which won't be opened automatically. You can still open each one individually by opening a file within.", path.basename(repository.root), repository.submodules.length));
statusListener.dispose();
return;
}
this.scanSubmodules(repository);
};
const statusListener = repository.onDidRunGitStatus(checkForSubmodules);
checkForSubmodules();
const dispose = () => {
disappearListener.dispose();
changeListener.dispose();
originalResourceChangeListener.dispose();
statusListener.dispose();
repository.dispose();
this.openRepositories = this.openRepositories.filter(e => e !== openRepository);
@@ -223,6 +256,20 @@ export class Model {
this._onDidOpenRepository.fire(repository);
}
private scanSubmodules(repository: Repository): void {
const shouldScanSubmodules = workspace
.getConfiguration('git', Uri.file(repository.root))
.get<boolean>('detectSubmodules') === true;
if (!shouldScanSubmodules) {
return;
}
repository.submodules
.map(r => path.join(repository.root, r.path))
.forEach(p => this.eventuallyScanPossibleGitRepository(p));
}
close(repository: Repository): void {
const openRepository = this.getOpenRepository(repository);
@@ -230,6 +277,7 @@ export class Model {
return;
}
this.outputChannel.appendLine(`Close repository: ${repository.root}`);
openRepository.dispose();
}
@@ -238,7 +286,16 @@ export class Model {
throw new Error(localize('no repositories', "There are no available repositories"));
}
const picks = this.openRepositories.map(e => new RepositoryPick(e.repository));
const picks = this.openRepositories.map((e, index) => new RepositoryPick(e.repository, index));
const active = window.activeTextEditor;
const repository = active && this.getRepository(active.document.fileName);
const index = firstIndex(picks, pick => pick.repository === repository);
// Move repository pick containing the active text editor to appear first
if (index > -1) {
picks.unshift(...picks.splice(index, 1));
}
const placeHolder = localize('pick repo', "Choose a repository");
const pick = await window.showQuickPick(picks, { placeHolder });
@@ -281,12 +338,21 @@ export class Model {
resourcePath = hint.fsPath;
}
for (const liveRepository of this.openRepositories) {
const relativePath = path.relative(liveRepository.repository.root, resourcePath);
if (isDescendant(liveRepository.repository.root, resourcePath)) {
return liveRepository;
outer:
for (const liveRepository of this.openRepositories.sort((a, b) => b.repository.root.length - a.repository.root.length)) {
if (!isDescendant(liveRepository.repository.root, resourcePath)) {
continue;
}
for (const submodule of liveRepository.repository.submodules) {
const submoduleRoot = path.join(liveRepository.repository.root, submodule.path);
if (isDescendant(submoduleRoot, resourcePath)) {
continue outer;
}
}
return liveRepository;
}
return undefined;
@@ -307,6 +373,20 @@ export class Model {
return undefined;
}
getRepositoryForSubmodule(submoduleUri: Uri): Repository | undefined {
for (const repository of this.repositories) {
for (const submodule of repository.submodules) {
const submodulePath = path.join(repository.root, submodule.path);
if (submodulePath === submoduleUri.fsPath) {
return repository;
}
}
}
return undefined;
}
dispose(): void {
const openRepositories = [...this.openRepositories];
openRepositories.forEach(r => r.dispose());

View File

@@ -5,9 +5,9 @@
'use strict';
import { Uri, Command, EventEmitter, Event, scm, SourceControl, SourceControlInputBox, SourceControlResourceGroup, SourceControlResourceState, SourceControlResourceDecorations, Disposable, ProgressLocation, window, workspace, WorkspaceEdit, ThemeColor, DecorationData, Memento } from 'vscode';
import { Repository as BaseRepository, Ref, Branch, Remote, Commit, GitErrorCodes, Stash, RefType, GitError } from './git';
import { anyEvent, filterEvent, eventToPromise, dispose, find, isDescendant } from './util';
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 { Repository as BaseRepository, Ref, Branch, Remote, Commit, GitErrorCodes, Stash, RefType, GitError, Submodule, DiffOptions } from './git';
import { anyEvent, filterEvent, eventToPromise, dispose, find, isDescendant, IDisposable, onceEvent, EmptyDisposable, debounceEvent } from './util';
import { memoize, throttle, debounce } from './decorators';
import { toGitUri } from './uri';
import { AutoFetcher } from './autofetch';
@@ -105,7 +105,7 @@ export class Resource implements SourceControlResourceState {
}
};
private getIconPath(theme: string): Uri | undefined {
private getIconPath(theme: string): Uri {
switch (this.type) {
case Status.INDEX_MODIFIED: return Resource.Icons[theme].Modified;
case Status.MODIFIED: return Resource.Icons[theme].Modified;
@@ -123,7 +123,6 @@ export class Resource implements SourceControlResourceState {
case Status.DELETED_BY_US: return Resource.Icons[theme].Conflict;
case Status.BOTH_ADDED: return Resource.Icons[theme].Conflict;
case Status.BOTH_MODIFIED: return Resource.Icons[theme].Conflict;
default: return void 0;
}
}
@@ -182,7 +181,7 @@ export class Resource implements SourceControlResourceState {
return { strikeThrough, faded, tooltip, light, dark, letter, color, source: 'git.resource' /*todo@joh*/ };
}
get letter(): string | undefined {
get letter(): string {
switch (this.type) {
case Status.INDEX_MODIFIED:
case Status.MODIFIED:
@@ -207,12 +206,10 @@ export class Resource implements SourceControlResourceState {
case Status.BOTH_ADDED:
case Status.BOTH_MODIFIED:
return 'C';
default:
return undefined;
}
}
get color(): ThemeColor | undefined {
get color(): ThemeColor {
switch (this.type) {
case Status.INDEX_MODIFIED:
case Status.MODIFIED:
@@ -235,8 +232,6 @@ export class Resource implements SourceControlResourceState {
case Status.BOTH_ADDED:
case Status.BOTH_MODIFIED:
return new ThemeColor('gitDecoration.conflictingResourceForeground');
default:
return undefined;
}
}
@@ -261,7 +256,7 @@ export class Resource implements SourceControlResourceState {
}
}
get resourceDecoration(): DecorationData | undefined {
get resourceDecoration(): DecorationData {
const title = this.tooltip;
const abbreviation = this.letter;
const color = this.color;
@@ -280,6 +275,7 @@ export class Resource implements SourceControlResourceState {
export enum Operation {
Status = 'Status',
Diff = 'Diff',
Add = 'Add',
RevertFiles = 'RevertFiles',
Commit = 'Commit',
@@ -301,7 +297,8 @@ export enum Operation {
Tag = 'Tag',
Stash = 'Stash',
CheckIgnore = 'CheckIgnore',
LSTree = 'LSTree'
LSTree = 'LSTree',
SubmoduleUpdate = 'SubmoduleUpdate'
}
function isReadOnly(operation: Operation): boolean {
@@ -330,6 +327,7 @@ function shouldShowProgress(operation: Operation): boolean {
export interface Operations {
isIdle(): boolean;
shouldShowProgress(): boolean;
isRunning(operation: Operation): boolean;
}
@@ -366,6 +364,18 @@ class OperationsImpl implements Operations {
return true;
}
shouldShowProgress(): boolean {
const operations = this.operations.keys();
for (const operation of operations) {
if (shouldShowProgress(operation)) {
return true;
}
}
return false;
}
}
export interface CommitOptions {
@@ -373,7 +383,6 @@ export interface CommitOptions {
amend?: boolean;
signoff?: boolean;
signCommit?: boolean;
defaultMsg?: string;
}
export interface GitResourceGroup extends SourceControlResourceGroup {
@@ -385,8 +394,33 @@ export interface OperationResult {
error: any;
}
class ProgressManager {
private disposable: IDisposable = EmptyDisposable;
constructor(repository: Repository) {
const start = onceEvent(filterEvent(repository.onDidChangeOperations, () => repository.operations.shouldShowProgress()));
const end = onceEvent(filterEvent(debounceEvent(repository.onDidChangeOperations, 300), () => !repository.operations.shouldShowProgress()));
const setup = () => {
this.disposable = start(() => {
const promise = eventToPromise(end).then(() => setup());
window.withProgress({ location: ProgressLocation.SourceControl }, () => promise);
});
};
setup();
}
dispose(): void {
this.disposable.dispose();
}
}
export class Repository implements Disposable {
private static readonly InputValidationLength = 72;
private _onDidChangeRepository = new EventEmitter<Uri>();
readonly onDidChangeRepository: Event<Uri> = this._onDidChangeRepository.event;
@@ -439,6 +473,11 @@ export class Repository implements Disposable {
return this._remotes;
}
private _submodules: Submodule[] = [];
get submodules(): Submodule[] {
return this._submodules;
}
private _operations = new OperationsImpl();
get operations(): Operations { return this._operations; }
@@ -474,7 +513,7 @@ export class Repository implements Disposable {
const onWorkspaceChange = anyEvent(fsWatcher.onDidChange, fsWatcher.onDidCreate, fsWatcher.onDidDelete);
const onRepositoryChange = filterEvent(onWorkspaceChange, uri => isDescendant(repository.root, uri.fsPath));
const onRelevantRepositoryChange = filterEvent(onRepositoryChange, uri => !/\/\.git\/index\.lock$/.test(uri.path));
const onRelevantRepositoryChange = filterEvent(onRepositoryChange, uri => !/\/\.git(\/index\.lock)?$/.test(uri.path));
onRelevantRepositoryChange(this.onFSChange, this, this.disposables);
const onRelevantGitChange = filterEvent(onRelevantRepositoryChange, uri => /\/\.git\//.test(uri.path));
@@ -484,6 +523,7 @@ export class Repository implements Disposable {
this._sourceControl.inputBox.placeholder = localize('commitMessage', "Message (press {0} to commit)");
this._sourceControl.acceptInputCommand = { command: 'git.commitWithInput', title: localize('commit', "Commit"), arguments: [this._sourceControl] };
this._sourceControl.quickDiffProvider = this;
this._sourceControl.inputBox.validateInput = this.validateInput.bind(this);
this.disposables.push(this._sourceControl);
this._mergeGroup = this._sourceControl.createResourceGroup('merge', localize('merge changes', "Merge Changes"));
@@ -504,16 +544,56 @@ export class Repository implements Disposable {
statusBar.onDidChange(() => this._sourceControl.statusBarCommands = statusBar.commands, null, this.disposables);
this._sourceControl.statusBarCommands = statusBar.commands;
const progressManager = new ProgressManager(this);
this.disposables.push(progressManager);
this.updateCommitTemplate();
this.status();
}
validateInput(text: string, position: number): SourceControlInputBoxValidation | undefined {
const config = workspace.getConfiguration('git');
const setting = config.get<'always' | 'warn' | 'off'>('inputValidation');
if (setting === 'off') {
return;
}
let start = 0, end;
let match: RegExpExecArray | null;
const regex = /\r?\n/g;
while ((match = regex.exec(text)) && position > match.index) {
start = match.index + match[0].length;
}
end = match ? match.index : text.length;
const line = text.substring(start, end);
if (line.length <= Repository.InputValidationLength) {
if (setting !== 'always') {
return;
}
return {
message: localize('commitMessageCountdown', "{0} characters left in current line", Repository.InputValidationLength - line.length),
type: SourceControlInputBoxValidationType.Information
};
} else {
return {
message: localize('commitMessageWarning', "{0} characters over {1} in current line", line.length - Repository.InputValidationLength, Repository.InputValidationLength),
type: SourceControlInputBoxValidationType.Warning
};
}
}
provideOriginalResource(uri: Uri): Uri | undefined {
if (uri.scheme !== 'file') {
return;
}
return toGitUri(uri, '', true);
return toGitUri(uri, '', { replaceFileExtension: true });
}
private async updateCommitTemplate(): Promise<void> {
@@ -524,21 +604,15 @@ export class Repository implements Disposable {
}
}
// @throttle
// async init(): Promise<void> {
// if (this.state !== State.NotAGitRepository) {
// return;
// }
// await this.git.init(this.workspaceRoot.fsPath);
// await this.status();
// }
@throttle
async status(): Promise<void> {
await this.run(Operation.Status);
}
diff(path: string, options: DiffOptions = {}): Promise<string> {
return this.run(Operation.Diff, () => this.repository.diff(path, options));
}
async add(resources: Uri[]): Promise<void> {
await this.run(Operation.Add, () => this.repository.add(resources.map(r => r.fsPath)));
}
@@ -567,8 +641,18 @@ export class Repository implements Disposable {
await this.run(Operation.Clean, async () => {
const toClean: string[] = [];
const toCheckout: string[] = [];
const submodulesToUpdate: string[] = [];
resources.forEach(r => {
const fsPath = r.fsPath;
for (const submodule of this.submodules) {
if (path.join(this.root, submodule.path) === fsPath) {
submodulesToUpdate.push(fsPath);
return;
}
}
const raw = r.toString();
const scmResource = find(this.workingTreeGroup.resourceStates, sr => sr.resourceUri.toString() === raw);
@@ -579,11 +663,11 @@ export class Repository implements Disposable {
switch (scmResource.type) {
case Status.UNTRACKED:
case Status.IGNORED:
toClean.push(r.fsPath);
toClean.push(fsPath);
break;
default:
toCheckout.push(r.fsPath);
toCheckout.push(fsPath);
break;
}
});
@@ -598,6 +682,10 @@ export class Repository implements Disposable {
promises.push(this.repository.checkout('', toCheckout));
}
if (submodulesToUpdate.length > 0) {
promises.push(this.repository.updateSubmodules(submodulesToUpdate));
}
await Promise.all(promises);
});
}
@@ -702,15 +790,15 @@ export class Repository implements Disposable {
async buffer(ref: string, filePath: string): Promise<Buffer> {
return await this.run(Operation.Show, async () => {
const relativePath = path.relative(this.repository.root, filePath).replace(/\\/g, '/');
const configFiles = workspace.getConfiguration('files', Uri.file(filePath));
const encoding = configFiles.get<string>('encoding');
// const configFiles = workspace.getConfiguration('files', Uri.file(filePath));
// const encoding = configFiles.get<string>('encoding');
// TODO@joao: REsource config api
return await this.repository.buffer(`${ref}:${relativePath}`);
});
}
lstree(ref: string, filePath: string): Promise<{ mode: number, object: string, size: number }> {
lstree(ref: string, filePath: string): Promise<{ mode: string, object: string, size: number }> {
return this.run(Operation.LSTree, () => this.repository.lstree(ref, filePath));
}
@@ -780,7 +868,11 @@ export class Repository implements Disposable {
// paths are separated by the null-character
resolve(new Set<string>(data.split('\0')));
} else {
reject(new GitError({ stdout: data, stderr, exitCode }));
if (/ is in submodule /.test(stderr)) {
reject(new GitError({ stdout: data, stderr, exitCode, gitErrorCode: GitErrorCodes.IsInSubmodule }));
} else {
reject(new GitError({ stdout: data, stderr, exitCode }));
}
}
};
@@ -807,37 +899,31 @@ export class Repository implements Disposable {
throw new Error('Repository not initialized');
}
const run = async () => {
let error: any = null;
let error: any = null;
this._operations.start(operation);
this._onRunOperation.fire(operation);
this._operations.start(operation);
this._onRunOperation.fire(operation);
try {
const result = await this.retryRun(runOperation);
try {
const result = await this.retryRun(runOperation);
if (!isReadOnly(operation)) {
await this.updateModelState();
}
return result;
} catch (err) {
error = err;
if (err.gitErrorCode === GitErrorCodes.NotAGitRepository) {
this.state = RepositoryState.Disposed;
}
throw err;
} finally {
this._operations.end(operation);
this._onDidRunOperation.fire({ operation, error });
if (!isReadOnly(operation)) {
await this.updateModelState();
}
};
return shouldShowProgress(operation)
? window.withProgress({ location: ProgressLocation.SourceControl }, run)
: run();
return result;
} catch (err) {
error = err;
if (err.gitErrorCode === GitErrorCodes.NotAGitRepository) {
this.state = RepositoryState.Disposed;
}
throw err;
} finally {
this._operations.end(operation);
this._onDidRunOperation.fire({ operation, error });
}
}
private async retryRun<T>(runOperation: () => Promise<T> = () => Promise.resolve<any>(null)): Promise<T> {
@@ -868,10 +954,9 @@ export class Repository implements Disposable {
this.isRepositoryHuge = didHitLimit;
if (didHitLimit && !shouldIgnore && !this.didWarnAboutLimit) {
const ok = { title: localize('ok', "OK"), isCloseAffordance: true };
const neverAgain = { title: localize('neveragain', "Never Show Again") };
const neverAgain = { title: localize('neveragain', "Don't Show Again") };
window.showWarningMessage(localize('huge', "The git repository at '{0}' has too many active changes, only a subset of Git features will be enabled.", this.repository.root), ok, neverAgain).then(result => {
window.showWarningMessage(localize('huge', "The git repository at '{0}' has too many active changes, only a subset of Git features will be enabled.", this.repository.root), neverAgain).then(result => {
if (result === neverAgain) {
config.update('ignoreLimitWarning', true, false);
}
@@ -896,11 +981,12 @@ export class Repository implements Disposable {
// noop
}
const [refs, remotes] = await Promise.all([this.repository.getRefs(), this.repository.getRemotes()]);
const [refs, remotes, submodules] = await Promise.all([this.repository.getRefs(), this.repository.getRemotes(), this.repository.getSubmodules()]);
this._HEAD = HEAD;
this._refs = refs;
this._remotes = remotes;
this._submodules = submodules;
const index: Resource[] = [];
const workingTree: Resource[] = [];
@@ -922,10 +1008,8 @@ export class Repository implements Disposable {
case 'UU': return merge.push(new Resource(ResourceGroupType.Merge, uri, Status.BOTH_MODIFIED, useIcons));
}
let isModifiedInIndex = false;
switch (raw.x) {
case 'M': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_MODIFIED, useIcons)); isModifiedInIndex = true; break;
case 'M': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_MODIFIED, useIcons)); break;
case 'A': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_ADDED, useIcons)); break;
case 'D': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_DELETED, useIcons)); break;
case 'R': index.push(new Resource(ResourceGroupType.Index, uri, Status.INDEX_RENAMED, useIcons, renameUri)); break;
@@ -954,13 +1038,9 @@ export class Repository implements Disposable {
this._sourceControl.count = count;
// set context key
let stateContextKey = '';
switch (this.state) {
case RepositoryState.Idle: stateContextKey = 'idle'; break;
case RepositoryState.Disposed: stateContextKey = 'norepo'; break;
}
// Disable `Discard All Changes` for "fresh" repositories
// https://github.com/Microsoft/vscode/issues/43066
commands.executeCommand('setContext', 'gitFreshRepository', !this._HEAD || !this._HEAD.commit);
this._onDidChangeStatus.fire();
}

View File

@@ -72,10 +72,16 @@ export function toLineRanges(selections: Selection[], textDocument: TextDocument
return result;
}
function getModifiedRange(textDocument: TextDocument, diff: LineChange): Range {
return diff.modifiedEndLineNumber === 0
? new Range(textDocument.lineAt(diff.modifiedStartLineNumber - 1).range.end, textDocument.lineAt(diff.modifiedStartLineNumber).range.start)
: new Range(textDocument.lineAt(diff.modifiedStartLineNumber - 1).range.start, textDocument.lineAt(diff.modifiedEndLineNumber - 1).range.end);
export function getModifiedRange(textDocument: TextDocument, diff: LineChange): Range {
if (diff.modifiedEndLineNumber === 0) {
if (diff.modifiedStartLineNumber === 0) {
return new Range(textDocument.lineAt(diff.modifiedStartLineNumber).range.end, textDocument.lineAt(diff.modifiedStartLineNumber).range.start);
} else {
return new Range(textDocument.lineAt(diff.modifiedStartLineNumber - 1).range.end, textDocument.lineAt(diff.modifiedStartLineNumber).range.start);
}
} else {
return new Range(textDocument.lineAt(diff.modifiedStartLineNumber - 1).range.start, textDocument.lineAt(diff.modifiedEndLineNumber - 1).range.end);
}
}
export function intersectDiffWithRange(textDocument: TextDocument, diff: LineChange, range: Range): LineChange | null {

View File

@@ -6,7 +6,7 @@
'use strict';
import 'mocha';
import { GitStatusParser } from '../git';
import { GitStatusParser, parseGitmodules } from '../git';
import * as assert from 'assert';
suite('git', () => {
@@ -135,4 +135,44 @@ suite('git', () => {
]);
});
});
suite('parseGitmodules', () => {
test('empty', () => {
assert.deepEqual(parseGitmodules(''), []);
});
test('sample', () => {
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' }
]);
});
test('big', () => {
const sample = `[submodule "deps/spdlog"]
path = deps/spdlog
url = https://github.com/gabime/spdlog.git
[submodule "deps/spdlog2"]
path = deps/spdlog2
url = https://github.com/gabime/spdlog.git
[submodule "deps/spdlog3"]
path = deps/spdlog3
url = https://github.com/gabime/spdlog.git
[submodule "deps/spdlog4"]
path = deps/spdlog4
url = https://github.com/gabime/spdlog4.git
`;
assert.deepEqual(parseGitmodules(sample), [
{ name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' },
{ name: 'deps/spdlog2', path: 'deps/spdlog2', url: 'https://github.com/gabime/spdlog.git' },
{ name: 'deps/spdlog3', path: 'deps/spdlog3', url: 'https://github.com/gabime/spdlog.git' },
{ name: 'deps/spdlog4', path: 'deps/spdlog4', url: 'https://github.com/gabime/spdlog4.git' }
]);
});
});
});

View File

@@ -7,20 +7,45 @@
import { Uri } from 'vscode';
export function fromGitUri(uri: Uri): { path: string; ref: string; } {
export interface GitUriParams {
path: string;
ref: string;
submoduleOf?: string;
}
export function fromGitUri(uri: Uri): GitUriParams {
return JSON.parse(uri.query);
}
export interface GitUriOptions {
replaceFileExtension?: boolean;
submoduleOf?: string;
}
// As a mitigation for extensions like ESLint showing warnings and errors
// for git URIs, let's change the file extension of these uris to .git,
// when `replaceFileExtension` is true.
export function toGitUri(uri: Uri, ref: string, replaceFileExtension = false): Uri {
export function toGitUri(uri: Uri, ref: string, options: GitUriOptions = {}): Uri {
const params: GitUriParams = {
path: uri.fsPath,
ref
};
if (options.submoduleOf) {
params.submoduleOf = options.submoduleOf;
}
let path = uri.path;
if (options.replaceFileExtension) {
path = `${path}.git`;
} else if (options.submoduleOf) {
path = `${path}.diff`;
}
return uri.with({
scheme: 'git',
path: replaceFileExtension ? `${uri.path}.git` : uri.path,
query: JSON.stringify({
path: uri.fsPath,
ref
})
path,
query: JSON.stringify(params)
});
}

View File

@@ -34,6 +34,10 @@ export function combinedDisposable(disposables: IDisposable[]): IDisposable {
export const EmptyDisposable = toDisposable(() => null);
export function fireEvent<T>(event: Event<T>): Event<T> {
return (listener, thisArgs = null, disposables?) => event(_ => listener.call(thisArgs), null, disposables);
}
export function mapEvent<I, O>(event: Event<I>, map: (i: I) => O): Event<O> {
return (listener, thisArgs = null, disposables?) => event(i => listener.call(thisArgs, map(i)), null, disposables);
}
@@ -69,6 +73,16 @@ export function onceEvent<T>(event: Event<T>): Event<T> {
};
}
export function debounceEvent<T>(event: Event<T>, delay: number): Event<T> {
return (listener, thisArgs = null, disposables?) => {
let timer: NodeJS.Timer;
return event(e => {
clearTimeout(timer);
timer = setTimeout(() => listener.call(thisArgs, e), delay);
}, null, disposables);
};
}
export function eventToPromise<T>(event: Event<T>): Promise<T> {
return new Promise<T>(c => onceEvent(event)(c));
}
@@ -116,6 +130,10 @@ export function groupBy<T>(arr: T[], fn: (el: T) => string): { [key: string]: T[
}, Object.create(null));
}
export function denodeify<A, B, C, R>(fn: Function): (a: A, b: B, c: C) => Promise<R>;
export function denodeify<A, B, R>(fn: Function): (a: A, b: B) => Promise<R>;
export function denodeify<A, R>(fn: Function): (a: A) => Promise<R>;
export function denodeify<R>(fn: Function): (...args: any[]) => Promise<R>;
export function denodeify<R>(fn: Function): (...args: any[]) => Promise<R> {
return (...args) => new Promise<R>((c, e) => fn(...args, (err: any, r: any) => err ? e(err) : c(r)));
}
@@ -177,6 +195,16 @@ export function uniqueFilter<T>(keyFn: (t: T) => string): (t: T) => boolean {
};
}
export function firstIndex<T>(array: T[], fn: (t: T) => boolean): number {
for (let i = 0; i < array.length; i++) {
if (fn(array[i])) {
return i;
}
}
return -1;
}
export function find<T>(array: T[], fn: (t: T) => boolean): T | undefined {
let result: T | undefined = undefined;
@@ -211,7 +239,7 @@ export async function grep(filename: string, pattern: RegExp): Promise<boolean>
export function readBytes(stream: Readable, bytes: number): Promise<Buffer> {
return new Promise<Buffer>((complete, error) => {
let done = false;
let buffer = new Buffer(bytes);
let buffer = Buffer.allocUnsafe(bytes);
let bytesRead = 0;
stream.on('data', (data: Buffer) => {