mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-03 01:25:38 -05:00
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:
@@ -6,7 +6,7 @@
|
||||
'use strict';
|
||||
|
||||
import { Model } from './model';
|
||||
import { SourceControlInputBox, Uri } from 'vscode';
|
||||
import { Uri } from 'vscode';
|
||||
|
||||
export interface InputBox {
|
||||
value: string;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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)
|
||||
});
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user