Merge VS Code 1.30.1 (#4092)

This commit is contained in:
Matt Irvine
2019-02-21 17:17:23 -08:00
committed by GitHub
parent a764a481f3
commit 826856c390
11465 changed files with 119542 additions and 255338 deletions

View File

@@ -1,65 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Model } from './model';
import { Repository as ModelRepository } from './repository';
import { Uri, SourceControlInputBox } from 'vscode';
export interface InputBox {
value: string;
}
export class InputBoxImpl implements InputBox {
set value(value: string) { this.inputBox.value = value; }
get value(): string { return this.inputBox.value; }
constructor(private inputBox: SourceControlInputBox) { }
}
export interface Repository {
readonly rootUri: Uri;
readonly inputBox: InputBox;
}
export class RepositoryImpl implements Repository {
readonly rootUri: Uri;
readonly inputBox: InputBox;
constructor(repository: ModelRepository) {
this.rootUri = Uri.file(repository.root);
this.inputBox = new InputBoxImpl(repository.inputBox);
}
}
export interface API {
getRepositories(): Promise<Repository[]>;
getGitPath(): Promise<string>;
}
export class APIImpl implements API {
constructor(private model: Model) { }
async getGitPath(): Promise<string> {
return this.model.git.path;
}
async getRepositories(): Promise<Repository[]> {
return this.model.repositories.map(repository => new RepositoryImpl(repository));
}
}
export class NoopAPIImpl implements API {
async getGitPath(): Promise<string> {
throw new Error('Git model not found');
}
async getRepositories(): Promise<Repository[]> {
throw new Error('Git model not found');
}
}

View File

@@ -0,0 +1,208 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Model } from '../model';
import { Repository as BaseRepository, Resource } from '../repository';
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, Ref, Submodule, Commit, Change, RepositoryUIState, Status } from './git';
import { Event, SourceControlInputBox, Uri, SourceControl } from 'vscode';
import { mapEvent } from '../util';
class ApiInputBox implements InputBox {
set value(value: string) { this._inputBox.value = value; }
get value(): string { return this._inputBox.value; }
constructor(private _inputBox: SourceControlInputBox) { }
}
export class ApiChange implements Change {
get uri(): Uri { return this.resource.resourceUri; }
get originalUri(): Uri { return this.resource.original; }
get renameUri(): Uri | undefined { return this.resource.renameResourceUri; }
get status(): Status { return this.resource.type; }
constructor(private readonly resource: Resource) { }
}
export class ApiRepositoryState implements RepositoryState {
get HEAD(): Branch | undefined { return this._repository.HEAD; }
get refs(): Ref[] { return [...this._repository.refs]; }
get remotes(): Remote[] { return [...this._repository.remotes]; }
get submodules(): Submodule[] { return [...this._repository.submodules]; }
get rebaseCommit(): Commit | undefined { return this._repository.rebaseCommit; }
get mergeChanges(): Change[] { return this._repository.mergeGroup.resourceStates.map(r => new ApiChange(r)); }
get indexChanges(): Change[] { return this._repository.indexGroup.resourceStates.map(r => new ApiChange(r)); }
get workingTreeChanges(): Change[] { return this._repository.workingTreeGroup.resourceStates.map(r => new ApiChange(r)); }
readonly onDidChange: Event<void> = this._repository.onDidRunGitStatus;
constructor(private _repository: BaseRepository) { }
}
export class ApiRepositoryUIState implements RepositoryUIState {
get selected(): boolean { return this._sourceControl.selected; }
readonly onDidChange: Event<void> = mapEvent<boolean, void>(this._sourceControl.onDidChangeSelection, () => null);
constructor(private _sourceControl: SourceControl) { }
}
export class ApiRepository implements Repository {
readonly rootUri: Uri = Uri.file(this._repository.root);
readonly inputBox: InputBox = new ApiInputBox(this._repository.inputBox);
readonly state: RepositoryState = new ApiRepositoryState(this._repository);
readonly ui: RepositoryUIState = new ApiRepositoryUIState(this._repository.sourceControl);
constructor(private _repository: BaseRepository) { }
apply(patch: string, reverse?: boolean): Promise<void> {
return this._repository.apply(patch, reverse);
}
getConfigs(): Promise<{ key: string; value: string; }[]> {
return this._repository.getConfigs();
}
getConfig(key: string): Promise<string> {
return this._repository.getConfig(key);
}
setConfig(key: string, value: string): Promise<string> {
return this._repository.setConfig(key, value);
}
getObjectDetails(treeish: string, path: string): Promise<{ mode: string; object: string; size: number; }> {
return this._repository.getObjectDetails(treeish, path);
}
detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }> {
return this._repository.detectObjectType(object);
}
buffer(ref: string, filePath: string): Promise<Buffer> {
return this._repository.buffer(ref, filePath);
}
show(ref: string, path: string): Promise<string> {
return this._repository.show(ref, path);
}
getCommit(ref: string): Promise<Commit> {
return this._repository.getCommit(ref);
}
clean(paths: string[]) {
return this._repository.clean(paths.map(p => Uri.file(p)));
}
diff(cached?: boolean) {
return this._repository.diff(cached);
}
diffWithHEAD(path: string): Promise<string> {
return this._repository.diffWithHEAD(path);
}
diffWith(ref: string, path: string): Promise<string> {
return this._repository.diffWith(ref, path);
}
diffIndexWithHEAD(path: string): Promise<string> {
return this._repository.diffIndexWithHEAD(path);
}
diffIndexWith(ref: string, path: string): Promise<string> {
return this._repository.diffIndexWith(ref, path);
}
diffBlobs(object1: string, object2: string): Promise<string> {
return this._repository.diffBlobs(object1, object2);
}
diffBetween(ref1: string, ref2: string, path: string): Promise<string> {
return this._repository.diffBetween(ref1, ref2, path);
}
hashObject(data: string): Promise<string> {
return this._repository.hashObject(data);
}
createBranch(name: string, checkout: boolean, ref?: string | undefined): Promise<void> {
return this._repository.branch(name, checkout, ref);
}
deleteBranch(name: string, force?: boolean): Promise<void> {
return this._repository.deleteBranch(name, force);
}
getBranch(name: string): Promise<Branch> {
return this._repository.getBranch(name);
}
setBranchUpstream(name: string, upstream: string): Promise<void> {
return this._repository.setBranchUpstream(name, upstream);
}
getMergeBase(ref1: string, ref2: string): Promise<string> {
return this._repository.getMergeBase(ref1, ref2);
}
status(): Promise<void> {
return this._repository.status();
}
checkout(treeish: string): Promise<void> {
return this._repository.checkout(treeish);
}
addRemote(name: string, url: string): Promise<void> {
return this._repository.addRemote(name, url);
}
removeRemote(name: string): Promise<void> {
return this._repository.removeRemote(name);
}
fetch(remote?: string | undefined, ref?: string | undefined): Promise<void> {
return this._repository.fetch(remote, ref);
}
pull(): Promise<void> {
return this._repository.pull();
}
push(remoteName?: string, branchName?: string, setUpstream: boolean = false): Promise<void> {
return this._repository.pushTo(remoteName, branchName, setUpstream);
}
}
export class ApiGit implements Git {
get path(): string { return this._model.git.path; }
constructor(private _model: Model) { }
}
export class ApiImpl implements API {
readonly git = new ApiGit(this._model);
get onDidOpenRepository(): Event<Repository> {
return mapEvent(this._model.onDidOpenRepository, r => new ApiRepository(r));
}
get onDidCloseRepository(): Event<Repository> {
return mapEvent(this._model.onDidCloseRepository, r => new ApiRepository(r));
}
get repositories(): Repository[] {
return this._model.repositories.map(r => new ApiRepository(r));
}
constructor(private _model: Model) { }
}

View File

@@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Model } from '../model';
import { GitExtension, Repository, API } from './git';
import { ApiRepository, ApiImpl } from './api1';
import { Event, EventEmitter } from 'vscode';
import { latchEvent } from '../util';
export function deprecated(_target: any, key: string, descriptor: any): void {
if (typeof descriptor.value !== 'function') {
throw new Error('not supported');
}
const fn = descriptor.value;
descriptor.value = function () {
console.warn(`Git extension API method '${key}' is deprecated.`);
return fn.apply(this, arguments);
};
}
export class GitExtensionImpl implements GitExtension {
enabled: boolean = false;
private _onDidChangeEnablement = new EventEmitter<boolean>();
readonly onDidChangeEnablement: Event<boolean> = latchEvent(this._onDidChangeEnablement.event);
private _model: Model | undefined = undefined;
set model(model: Model | undefined) {
this._model = model;
this.enabled = !!model;
this._onDidChangeEnablement.fire(this.enabled);
}
constructor(model?: Model) {
if (model) {
this.enabled = true;
this._model = model;
}
}
@deprecated
async getGitPath(): Promise<string> {
if (!this._model) {
throw new Error('Git model not found');
}
return this._model.git.path;
}
@deprecated
async getRepositories(): Promise<Repository[]> {
if (!this._model) {
throw new Error('Git model not found');
}
return this._model.repositories.map(repository => new ApiRepository(repository));
}
getAPI(version: number): API {
if (!this._model) {
throw new Error('Git model not found');
}
if (version !== 1) {
throw new Error(`No API version ${version} found.`);
}
return new ApiImpl(this._model);
}
}

217
extensions/git/src/api/git.d.ts vendored Normal file
View File

@@ -0,0 +1,217 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri, SourceControlInputBox, Event, CancellationToken } from 'vscode';
export interface Git {
readonly path: string;
}
export interface InputBox {
value: string;
}
export const enum RefType {
Head,
RemoteHead,
Tag
}
export interface Ref {
readonly type: RefType;
readonly name?: string;
readonly commit?: string;
readonly remote?: string;
}
export interface UpstreamRef {
readonly remote: string;
readonly name: string;
}
export interface Branch extends Ref {
readonly upstream?: UpstreamRef;
readonly ahead?: number;
readonly behind?: number;
}
export interface Commit {
readonly hash: string;
readonly message: string;
readonly parents: string[];
}
export interface Submodule {
readonly name: string;
readonly path: string;
readonly url: string;
}
export interface Remote {
readonly name: string;
readonly fetchUrl?: string;
readonly pushUrl?: string;
readonly isReadOnly: boolean;
}
export const enum Status {
INDEX_MODIFIED,
INDEX_ADDED,
INDEX_DELETED,
INDEX_RENAMED,
INDEX_COPIED,
MODIFIED,
DELETED,
UNTRACKED,
IGNORED,
ADDED_BY_US,
ADDED_BY_THEM,
DELETED_BY_US,
DELETED_BY_THEM,
BOTH_ADDED,
BOTH_DELETED,
BOTH_MODIFIED
}
export interface Change {
/**
* Returns either `originalUri` or `renameUri`, depending
* on whether this change is a rename change. When
* in doubt always use `uri` over the other two alternatives.
*/
readonly uri: Uri;
readonly originalUri: Uri;
readonly renameUri: Uri | undefined;
readonly status: Status;
}
export interface RepositoryState {
readonly HEAD: Branch | undefined;
readonly refs: Ref[];
readonly remotes: Remote[];
readonly submodules: Submodule[];
readonly rebaseCommit: Commit | undefined;
readonly mergeChanges: Change[];
readonly indexChanges: Change[];
readonly workingTreeChanges: Change[];
readonly onDidChange: Event<void>;
}
export interface RepositoryUIState {
readonly selected: boolean;
readonly onDidChange: Event<void>;
}
export interface Repository {
readonly rootUri: Uri;
readonly inputBox: InputBox;
readonly state: RepositoryState;
readonly ui: RepositoryUIState;
getConfigs(): Promise<{ key: string; value: string; }[]>;
getConfig(key: string): Promise<string>;
setConfig(key: string, value: string): Promise<string>;
getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>;
detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }>;
buffer(ref: string, path: string): Promise<Buffer>;
show(ref: string, path: string): Promise<string>;
getCommit(ref: string): Promise<Commit>;
clean(paths: string[]): Promise<void>;
apply(patch: string, reverse?: boolean): Promise<void>;
diff(cached?: boolean): Promise<string>;
diffWithHEAD(path: string): Promise<string>;
diffWith(ref: string, path: string): Promise<string>;
diffIndexWithHEAD(path: string): Promise<string>;
diffIndexWith(ref: string, path: string): Promise<string>;
diffBlobs(object1: string, object2: string): Promise<string>;
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
hashObject(data: string): Promise<string>;
createBranch(name: string, checkout: boolean, ref?: string): Promise<void>;
deleteBranch(name: string, force?: boolean): Promise<void>;
getBranch(name: string): Promise<Branch>;
setBranchUpstream(name: string, upstream: string): Promise<void>;
getMergeBase(ref1: string, ref2: string): Promise<string>;
status(): Promise<void>;
checkout(treeish: string): Promise<void>;
addRemote(name: string, url: string): Promise<void>;
removeRemote(name: string): Promise<void>;
fetch(remote?: string, ref?: string): Promise<void>;
pull(): Promise<void>;
push(remoteName?: string, branchName?: string, setUpstream?: boolean): Promise<void>;
}
export interface API {
readonly git: Git;
readonly repositories: Repository[];
readonly onDidOpenRepository: Event<Repository>;
readonly onDidCloseRepository: Event<Repository>;
}
export interface GitExtension {
readonly enabled: boolean;
readonly onDidChangeEnablement: Event<boolean>;
/**
* Returns a specific API version.
*
* Throws error if git extension is disabled. You can listed to the
* [GitExtension.onDidChangeEnablement](#GitExtension.onDidChangeEnablement) event
* to know when the extension becomes enabled/disabled.
*
* @param version Version number.
* @returns API instance
*/
getAPI(version: 1): API;
}
export const enum GitErrorCodes {
BadConfigFile = 'BadConfigFile',
AuthenticationFailed = 'AuthenticationFailed',
NoUserNameConfigured = 'NoUserNameConfigured',
NoUserEmailConfigured = 'NoUserEmailConfigured',
NoRemoteRepositorySpecified = 'NoRemoteRepositorySpecified',
NotAGitRepository = 'NotAGitRepository',
NotAtRepositoryRoot = 'NotAtRepositoryRoot',
Conflict = 'Conflict',
StashConflict = 'StashConflict',
UnmergedChanges = 'UnmergedChanges',
PushRejected = 'PushRejected',
RemoteConnectionError = 'RemoteConnectionError',
DirtyWorkTree = 'DirtyWorkTree',
CantOpenResource = 'CantOpenResource',
GitNotFound = 'GitNotFound',
CantCreatePipe = 'CantCreatePipe',
CantAccessRemote = 'CantAccessRemote',
RepositoryNotFound = 'RepositoryNotFound',
RepositoryIsLocked = 'RepositoryIsLocked',
BranchNotFullyMerged = 'BranchNotFullyMerged',
NoRemoteReference = 'NoRemoteReference',
InvalidBranchName = 'InvalidBranchName',
BranchAlreadyExists = 'BranchAlreadyExists',
NoLocalChanges = 'NoLocalChanges',
NoStashFound = 'NoStashFound',
LocalChangesOverwritten = 'LocalChangesOverwritten',
NoUpstreamBranch = 'NoUpstreamBranch',
IsInSubmodule = 'IsInSubmodule',
WrongCase = 'WrongCase',
CantLockRef = 'CantLockRef',
CantRebaseMultipleBranches = 'CantRebaseMultipleBranches',
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as http from 'http';
import * as fs from 'fs';
import * as nls from 'vscode-nls';

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Disposable, window, InputBoxOptions } from 'vscode';
import { denodeify } from './util';
import * as path from 'path';

View File

@@ -3,13 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
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';
import * as nls from 'vscode-nls';
import { GitErrorCodes } from './api/git';
const localize = nls.loadMessageBundle();
@@ -103,7 +101,7 @@ export class AutoFetcher {
}
try {
await this.repository.fetch();
await this.repository.fetchDefault();
} catch (err) {
if (err.gitErrorCode === GitErrorCodes.AuthenticationFailed) {
this.disable();

569
extensions/git/src/commands.ts Normal file → Executable file
View File

@@ -3,11 +3,9 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Uri, commands, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation, TextEditor, MessageOptions } from 'vscode';
import { Ref, RefType, Git, GitErrorCodes, Branch } from './git';
import { Repository, Resource, Status, CommitOptions, ResourceGroupType } from './repository';
import { Uri, commands, Disposable, window, workspace, QuickPickItem, OutputChannel, Range, WorkspaceEdit, Position, LineChange, SourceControlResourceState, TextDocumentShowOptions, ViewColumn, ProgressLocation, TextEditor, MessageOptions, WorkspaceFolder } from 'vscode';
import { Git, CommitOptions, Stash, ForcePushMode } from './git';
import { Repository, Resource, ResourceGroupType } from './repository';
import { Model } from './model';
import { toGitUri, fromGitUri } from './uri';
import { grep, isDescendant, pathEquals } from './util';
@@ -17,20 +15,20 @@ import { lstat, Stats } from 'fs';
import * as os from 'os';
import TelemetryReporter from 'vscode-extension-telemetry';
import * as nls from 'vscode-nls';
import { Ref, RefType, Branch, GitErrorCodes, Status } from './api/git';
const localize = nls.loadMessageBundle();
class CheckoutItem implements QuickPickItem {
protected get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); }
protected get treeish(): string | undefined { return this.ref.name; }
get label(): string { return this.ref.name || this.shortCommit; }
get description(): string { return this.shortCommit; }
constructor(protected ref: Ref) { }
async run(repository: Repository): Promise<void> {
const ref = this.treeish;
const ref = this.ref.name;
if (!ref) {
return;
@@ -53,13 +51,12 @@ class CheckoutRemoteHeadItem extends CheckoutItem {
return localize('remote branch at', "Remote branch at {0}", this.shortCommit);
}
protected get treeish(): string | undefined {
async run(repository: Repository): Promise<void> {
if (!this.ref.name) {
return;
}
const match = /^[^/]+\/(.*)$/.exec(this.ref.name);
return match ? match[1] : this.ref.name;
await repository.checkoutTracking(this.ref.name);
}
}
@@ -99,6 +96,8 @@ class CreateBranchItem implements QuickPickItem {
get label(): string { return localize('create branch', '$(plus) Create new branch'); }
get description(): string { return ''; }
get alwaysShow(): boolean { return true; }
async run(repository: Repository): Promise<void> {
await this.cc.branch(repository);
}
@@ -119,7 +118,7 @@ interface Command {
const Commands: Command[] = [];
function command(commandId: string, options: CommandOptions = {}): Function {
return (target: any, key: string, descriptor: any) => {
return (_target: any, key: string, descriptor: any) => {
if (!(typeof descriptor.value === 'function')) {
throw new Error('not supported');
}
@@ -137,20 +136,34 @@ const ImageMimetypes = [
'image/bmp'
];
async function categorizeResourceByResolution(resources: Resource[]): Promise<{ merge: Resource[], resolved: Resource[], unresolved: Resource[] }> {
async function categorizeResourceByResolution(resources: Resource[]): Promise<{ merge: Resource[], resolved: Resource[], unresolved: Resource[], deletionConflicts: Resource[] }> {
const selection = resources.filter(s => s instanceof Resource) as Resource[];
const merge = selection.filter(s => s.resourceGroupType === ResourceGroupType.Merge);
const isBothAddedOrModified = (s: Resource) => s.type === Status.BOTH_MODIFIED || s.type === Status.BOTH_ADDED;
const isAnyDeleted = (s: Resource) => s.type === Status.DELETED_BY_THEM || s.type === Status.DELETED_BY_US;
const possibleUnresolved = merge.filter(isBothAddedOrModified);
const promises = possibleUnresolved.map(s => grep(s.resourceUri.fsPath, /^<{7}|^={7}|^>{7}/));
const unresolvedBothModified = await Promise.all<boolean>(promises);
const resolved = possibleUnresolved.filter((s, i) => !unresolvedBothModified[i]);
const resolved = possibleUnresolved.filter((_s, i) => !unresolvedBothModified[i]);
const deletionConflicts = merge.filter(s => isAnyDeleted(s));
const unresolved = [
...merge.filter(s => !isBothAddedOrModified(s)),
...possibleUnresolved.filter((s, i) => unresolvedBothModified[i])
...merge.filter(s => !isBothAddedOrModified(s) && !isAnyDeleted(s)),
...possibleUnresolved.filter((_s, i) => unresolvedBothModified[i])
];
return { merge, resolved, unresolved };
return { merge, resolved, unresolved, deletionConflicts };
}
enum PushType {
Push,
PushTo,
PushTags,
}
interface PushOptions {
pushType: PushType;
forcePush?: boolean;
silent?: boolean;
}
export class CommandCenter {
@@ -181,7 +194,20 @@ export class CommandCenter {
@command('git.openResource')
async openResource(resource: Resource): Promise<void> {
await this._openResource(resource, undefined, true, false);
const repository = this.model.getRepository(resource.resourceUri);
if (!repository) {
return;
}
const config = workspace.getConfiguration('git', Uri.file(repository.root));
const openDiffOnClick = config.get<boolean>('openDiffOnClick');
if (openDiffOnClick) {
await this._openResource(resource, undefined, true, false);
} else {
await this.openFile(resource);
}
}
private async _openResource(resource: Resource, preview?: boolean, preserveFocus?: boolean, preserveSelection?: boolean): Promise<void> {
@@ -203,7 +229,10 @@ export class CommandCenter {
right = toGitUri(resource.resourceUri, resource.resourceGroupType === ResourceGroupType.Index ? 'index' : 'wt', { submoduleOf: repository.root });
}
} else {
left = await this.getLeftResource(resource);
if (resource.type !== Status.DELETED_BY_THEM) {
left = await this.getLeftResource(resource);
}
right = await this.getRightResource(resource);
}
@@ -230,7 +259,7 @@ export class CommandCenter {
}
if (!left) {
await commands.executeCommand<void>('vscode.open', right, opts);
await commands.executeCommand<void>('vscode.open', right, opts, title);
} else {
await commands.executeCommand<void>('vscode.diff', left, right, title, opts);
}
@@ -287,6 +316,7 @@ export class CommandCenter {
case Status.DELETED_BY_THEM:
return this.getURI(resource.resourceUri, '');
}
return undefined;
}
private async getRightResource(resource: Resource): Promise<Uri | undefined> {
@@ -298,10 +328,15 @@ export class CommandCenter {
return this.getURI(resource.resourceUri, '');
case Status.INDEX_DELETED:
case Status.DELETED_BY_THEM:
case Status.DELETED:
return this.getURI(resource.resourceUri, 'HEAD');
case Status.DELETED_BY_US:
return this.getURI(resource.resourceUri, '~3');
case Status.DELETED_BY_THEM:
return this.getURI(resource.resourceUri, '~2');
case Status.MODIFIED:
case Status.UNTRACKED:
case Status.IGNORED:
@@ -324,6 +359,7 @@ export class CommandCenter {
case Status.BOTH_MODIFIED:
return resource.resourceUri;
}
return undefined;
}
private getTitle(resource: Resource): string {
@@ -332,13 +368,18 @@ export class CommandCenter {
switch (resource.type) {
case Status.INDEX_MODIFIED:
case Status.INDEX_RENAMED:
case Status.DELETED_BY_THEM:
return `${basename} (Index)`;
case Status.MODIFIED:
case Status.BOTH_ADDED:
case Status.BOTH_MODIFIED:
return `${basename} (Working Tree)`;
case Status.DELETED_BY_US:
return `${basename} (Theirs)`;
case Status.DELETED_BY_THEM:
return `${basename} (Ours)`;
}
return '';
@@ -454,21 +495,27 @@ export class CommandCenter {
@command('git.init')
async init(): Promise<void> {
let path: string | undefined;
let repositoryPath: string | undefined = undefined;
let askToOpen = true;
if (workspace.workspaceFolders && workspace.workspaceFolders.length > 1) {
if (workspace.workspaceFolders) {
const placeHolder = localize('init', "Pick workspace folder to initialize git repo in");
const items = workspace.workspaceFolders.map(folder => ({ label: folder.name, description: folder.uri.fsPath, folder }));
const pick = { label: localize('choose', "Choose Folder...") };
const items: { label: string, folder?: WorkspaceFolder }[] = [
...workspace.workspaceFolders.map(folder => ({ label: folder.name, description: folder.uri.fsPath, folder })),
pick
];
const item = await window.showQuickPick(items, { placeHolder, ignoreFocusOut: true });
if (!item) {
return;
} else if (item.folder) {
repositoryPath = item.folder.uri.fsPath;
askToOpen = false;
}
path = item.folder.uri.fsPath;
}
if (!path) {
if (!repositoryPath) {
const homeUri = Uri.file(os.homedir());
const defaultUri = workspace.workspaceFolders && workspace.workspaceFolders.length > 0
? Uri.file(workspace.workspaceFolders[0].uri.fsPath)
@@ -497,11 +544,40 @@ export class CommandCenter {
}
}
path = uri.fsPath;
repositoryPath = uri.fsPath;
if (workspace.workspaceFolders && workspace.workspaceFolders.some(w => w.uri.toString() === uri.toString())) {
askToOpen = false;
}
}
await this.git.init(path);
await this.model.openRepository(path);
await this.git.init(repositoryPath);
const choices = [];
let message = localize('proposeopen init', "Would you like to open the initialized repository?");
const open = localize('openrepo', "Open Repository");
choices.push(open);
if (!askToOpen) {
return;
}
const addToWorkspace = localize('add', "Add to Workspace");
if (workspace.workspaceFolders) {
message = localize('proposeopen2 init', "Would you like to open the initialized repository, or add it to the current workspace?");
choices.push(addToWorkspace);
}
const result = await window.showInformationMessage(message, ...choices);
const uri = Uri.file(repositoryPath);
if (result === open) {
commands.executeCommand('vscode.openFolder', uri);
} else if (result === addToWorkspace) {
workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
} else {
await this.model.openRepository(repositoryPath);
}
}
@command('git.openRepository', { repository: false })
@@ -562,22 +638,27 @@ export class CommandCenter {
return;
}
const preview = uris.length === 1 ? true : false;
const activeTextEditor = window.activeTextEditor;
for (const uri of uris) {
const opts: TextDocumentShowOptions = {
preserveFocus,
preview,
preview: false,
viewColumn: ViewColumn.Active
};
const document = await workspace.openTextDocument(uri);
// Check if active text editor has same path as other editor. we cannot compare via
// URI.toString() here because the schemas can be different. Instead we just go by path.
if (activeTextEditor && activeTextEditor.document.uri.path === uri.path) {
// preserve not only selection but also visible range
opts.selection = activeTextEditor.selection;
const previousVisibleRanges = activeTextEditor.visibleRanges;
const editor = await window.showTextDocument(document, opts);
editor.revealRange(previousVisibleRanges[0]);
} else {
await window.showTextDocument(document, opts);
}
await commands.executeCommand<void>('vscode.open', uri, opts);
}
}
@@ -589,6 +670,7 @@ export class CommandCenter {
@command('git.openHEADFile')
async openHEADFile(arg?: Resource | Uri): Promise<void> {
let resource: Resource | undefined = undefined;
const preview = !(arg instanceof Resource);
if (arg instanceof Resource) {
resource = arg;
@@ -609,12 +691,18 @@ export class CommandCenter {
return;
}
return await commands.executeCommand<void>('vscode.open', HEAD);
const opts: TextDocumentShowOptions = {
preview
};
return await commands.executeCommand<void>('vscode.open', HEAD, opts);
}
@command('git.openChange')
async openChange(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
const preserveFocus = arg instanceof Resource;
const preview = !(arg instanceof Resource);
const preserveSelection = arg instanceof Uri || !arg;
let resources: Resource[] | undefined = undefined;
@@ -641,7 +729,6 @@ export class CommandCenter {
return;
}
const preview = resources.length === 1 ? undefined : false;
for (const resource of resources) {
await this._openResource(resource, preview, preserveFocus, preserveSelection);
}
@@ -666,7 +753,7 @@ export class CommandCenter {
}
const selection = resourceStates.filter(s => s instanceof Resource) as Resource[];
const { resolved, unresolved } = await categorizeResourceByResolution(selection);
const { resolved, unresolved, deletionConflicts } = await categorizeResourceByResolution(selection);
if (unresolved.length > 0) {
const message = unresolved.length > 1
@@ -681,6 +768,20 @@ export class CommandCenter {
}
}
try {
await this.runByRepository(deletionConflicts.map(r => r.resourceUri), async (repository, resources) => {
for (const resource of resources) {
await this._stageDeletionConflict(repository, resource);
}
});
} catch (err) {
if (/Cancelled/.test(err.message)) {
return;
}
throw err;
}
const workingTree = selection.filter(s => s.resourceGroupType === ResourceGroupType.WorkingTree);
const scmResources = [...workingTree, ...resolved, ...unresolved];
@@ -696,7 +797,19 @@ export class CommandCenter {
@command('git.stageAll', { repository: true })
async stageAll(repository: Repository): Promise<void> {
const resources = repository.mergeGroup.resourceStates.filter(s => s instanceof Resource) as Resource[];
const { merge, unresolved } = await categorizeResourceByResolution(resources);
const { merge, unresolved, deletionConflicts } = await categorizeResourceByResolution(resources);
try {
for (const deletionConflict of deletionConflicts) {
await this._stageDeletionConflict(repository, deletionConflict.resourceUri);
}
} catch (err) {
if (/Cancelled/.test(err.message)) {
return;
}
throw err;
}
if (unresolved.length > 0) {
const message = unresolved.length > 1
@@ -714,6 +827,41 @@ export class CommandCenter {
await repository.add([]);
}
private async _stageDeletionConflict(repository: Repository, uri: Uri): Promise<void> {
const uriString = uri.toString();
const resource = repository.mergeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
if (!resource) {
return;
}
if (resource.type === Status.DELETED_BY_THEM) {
const keepIt = localize('keep ours', "Keep Our Version");
const deleteIt = localize('delete', "Delete File");
const result = await window.showInformationMessage(localize('deleted by them', "File '{0}' was deleted by them and modified by us.\n\nWhat would you like to do?", path.basename(uri.fsPath)), { modal: true }, keepIt, deleteIt);
if (result === keepIt) {
await repository.add([uri]);
} else if (result === deleteIt) {
await repository.rm([uri]);
} else {
throw new Error('Cancelled');
}
} else if (resource.type === Status.DELETED_BY_US) {
const keepIt = localize('keep theirs', "Keep Their Version");
const deleteIt = localize('delete', "Delete File");
const result = await window.showInformationMessage(localize('deleted by us', "File '{0}' was deleted by us and modified by them.\n\nWhat would you like to do?", path.basename(uri.fsPath)), { modal: true }, keepIt, deleteIt);
if (result === keepIt) {
await repository.add([uri]);
} else if (result === deleteIt) {
await repository.rm([uri]);
} else {
throw new Error('Cancelled');
}
}
}
@command('git.stageChange')
async stageChange(uri: Uri, changes: LineChange[], index: number): Promise<void> {
const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0];
@@ -916,10 +1064,20 @@ export class CommandCenter {
message = localize('confirm delete', "Are you sure you want to DELETE {0}?", path.basename(scmResources[0].resourceUri.fsPath));
yes = localize('delete file', "Delete file");
} else {
message = localize('confirm discard', "Are you sure you want to discard changes in {0}?", path.basename(scmResources[0].resourceUri.fsPath));
if (scmResources[0].type === Status.DELETED) {
yes = localize('restore file', "Restore file");
message = localize('confirm restore', "Are you sure you want to restore {0}?", path.basename(scmResources[0].resourceUri.fsPath));
} else {
message = localize('confirm discard', "Are you sure you want to discard changes in {0}?", path.basename(scmResources[0].resourceUri.fsPath));
}
}
} else {
message = localize('confirm discard multiple', "Are you sure you want to discard changes in {0} files?", scmResources.length);
if (scmResources.every(resource => resource.type === Status.DELETED)) {
yes = localize('restore files', "Restore files");
message = localize('confirm restore multiple', "Are you sure you want to restore {0} files?", scmResources.length);
} else {
message = localize('confirm discard multiple', "Are you sure you want to discard changes in {0} files?", scmResources.length);
}
if (untrackedCount > 0) {
message = `${message}\n\n${localize('warn untracked', "This will DELETE {0} untracked files!", untrackedCount)}`;
@@ -1071,10 +1229,13 @@ export class CommandCenter {
}
if (
// no changes
(noStagedChanges && noUnstagedChanges)
// or no staged changes and not `all`
|| (!opts.all && noStagedChanges)
(
// no changes
(noStagedChanges && noUnstagedChanges)
// or no staged changes and not `all`
|| (!opts.all && noStagedChanges)
)
&& !opts.empty
) {
window.showInformationMessage(localize('no changes', "There are no changes to commit."));
return false;
@@ -1088,6 +1249,17 @@ export class CommandCenter {
await repository.commit(message, opts);
const postCommitCommand = config.get<'none' | 'push' | 'sync'>('postCommitCommand');
switch (postCommitCommand) {
case 'push':
await this._push(repository, { pushType: PushType.Push, silent: true });
break;
case 'sync':
await this.sync(repository);
break;
}
return true;
}
@@ -1167,6 +1339,33 @@ export class CommandCenter {
await this.commitWithAnyInput(repository, { all: true, amend: true });
}
@command('git.commitEmpty', { repository: true })
async commitEmpty(repository: Repository): Promise<void> {
const root = Uri.file(repository.root);
const config = workspace.getConfiguration('git', root);
const shouldPrompt = config.get<boolean>('confirmEmptyCommits') === true;
if (shouldPrompt) {
const message = localize('confirm emtpy commit', "Are you sure you want to create an empty commit?");
const yes = localize('yes', "Yes");
const neverAgain = localize('yes never again', "Yes, Don't Show Again");
const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);
if (pick === neverAgain) {
await config.update('confirmEmptyCommits', false, true);
} else if (pick !== yes) {
return;
}
}
await this.commitWithAnyInput(repository, { empty: true });
}
@command('git.restoreCommitTemplate', { repository: true })
async restoreCommitTemplate(repository: Repository): Promise<void> {
repository.inputBox.value = await repository.getCommitTemplate();
}
@command('git.undoCommit', { repository: true })
async undoCommit(repository: Repository): Promise<void> {
const HEAD = repository.HEAD;
@@ -1189,9 +1388,10 @@ export class CommandCenter {
}
@command('git.checkout', { repository: true })
async checkout(repository: Repository, treeish: string): Promise<void> {
async checkout(repository: Repository, treeish: string): Promise<boolean> {
if (typeof treeish === 'string') {
return await repository.checkout(treeish);
await repository.checkout(treeish);
return true;
}
const config = workspace.getConfiguration('git');
@@ -1215,26 +1415,49 @@ export class CommandCenter {
const choice = await window.showQuickPick(picks, { placeHolder });
if (!choice) {
return;
return false;
}
await choice.run(repository);
return true;
}
@command('git.branch', { repository: true })
async branch(repository: Repository): Promise<void> {
const config = workspace.getConfiguration('git');
const branchValidationRegex = config.get<string>('branchValidationRegex')!;
const branchWhitespaceChar = config.get<string>('branchWhitespaceChar')!;
const validateName = new RegExp(branchValidationRegex);
const sanitize = (name: string) => {
name = name.trim();
if (!name) {
return name;
}
return name.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, branchWhitespaceChar);
};
const result = await window.showInputBox({
placeHolder: localize('branch name', "Branch name"),
prompt: localize('provide branch name', "Please provide a branch name"),
ignoreFocusOut: true
ignoreFocusOut: true,
validateInput: (name: string) => {
if (validateName.test(sanitize(name))) {
return null;
}
return localize('branch name format invalid', "Branch name needs to match regex: {0}", branchValidationRegex);
}
});
if (!result) {
const name = sanitize(result || '');
if (!name) {
return;
}
const name = result.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-');
await repository.branch(name);
await repository.branch(name, true);
}
@command('git.deleteBranch', { repository: true })
@@ -1354,7 +1577,28 @@ export class CommandCenter {
return;
}
await repository.fetch();
await repository.fetchDefault();
}
@command('git.fetchPrune', { repository: true })
async fetchPrune(repository: Repository): Promise<void> {
if (repository.remotes.length === 0) {
window.showWarningMessage(localize('no remotes to fetch', "This repository has no remotes configured to fetch from."));
return;
}
await repository.fetchPrune();
}
@command('git.fetchAll', { repository: true })
async fetchAll(repository: Repository): Promise<void> {
if (repository.remotes.length === 0) {
window.showWarningMessage(localize('no remotes to fetch', "This repository has no remotes configured to fetch from."));
return;
}
await repository.fetchAll();
}
@command('git.pullFrom', { repository: true })
@@ -1377,7 +1621,8 @@ export class CommandCenter {
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 });
const branchPlaceHolder = localize('pick branch pull', "Pick a branch to pull from");
const branchPick = await window.showQuickPick(branchPicks, { placeHolder: branchPlaceHolder });
if (!branchPick) {
return;
@@ -1412,76 +1657,118 @@ export class CommandCenter {
await repository.pullWithRebase(repository.HEAD);
}
@command('git.push', { repository: true })
async push(repository: Repository): Promise<void> {
private async _push(repository: Repository, pushOptions: PushOptions) {
const remotes = repository.remotes;
if (remotes.length === 0) {
window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."));
if (!pushOptions.silent) {
window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."));
}
return;
}
const config = workspace.getConfiguration('git', Uri.file(repository.root));
let forcePushMode: ForcePushMode | undefined = undefined;
if (pushOptions.forcePush) {
if (!config.get<boolean>('allowForcePush')) {
await window.showErrorMessage(localize('force push not allowed', "Force push is not allowed, please enable it with the 'git.allowForcePush' setting."));
return;
}
forcePushMode = config.get<boolean>('useForcePushWithLease') === true ? ForcePushMode.ForceWithLease : ForcePushMode.Force;
if (config.get<boolean>('confirmForcePush')) {
const message = localize('confirm force push', "You are about to force push your changes, this can be destructive and could inadvertedly overwrite changes made by others.\n\nAre you sure to continue?");
const yes = localize('ok', "OK");
const neverAgain = localize('never ask again', "OK, Don't Ask Again");
const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);
if (pick === neverAgain) {
config.update('confirmForcePush', false, true);
} else if (pick !== yes) {
return;
}
}
}
if (pushOptions.pushType === PushType.PushTags) {
await repository.pushTags(undefined, forcePushMode);
window.showInformationMessage(localize('push with tags success', "Successfully pushed with tags."));
return;
}
if (!repository.HEAD || !repository.HEAD.name) {
window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
if (!pushOptions.silent) {
window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
}
return;
}
try {
await repository.push(repository.HEAD);
} catch (err) {
if (err.gitErrorCode !== GitErrorCodes.NoUpstreamBranch) {
throw err;
}
if (pushOptions.pushType === PushType.Push) {
try {
await repository.push(repository.HEAD, forcePushMode);
} catch (err) {
if (err.gitErrorCode !== GitErrorCodes.NoUpstreamBranch) {
throw err;
}
if (pushOptions.silent) {
return;
}
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);
}
}
} else {
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);
const picks = remotes.filter(r => r.pushUrl !== undefined).map(r => ({ label: r.name, description: r.pushUrl! }));
const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
const pick = await window.showQuickPick(picks, { placeHolder });
if (pick === yes) {
await this.publish(repository);
if (!pick) {
return;
}
await repository.pushTo(pick.label, branchName, undefined, forcePushMode);
}
}
@command('git.push', { repository: true })
async push(repository: Repository): Promise<void> {
await this._push(repository, { pushType: PushType.Push });
}
@command('git.pushForce', { repository: true })
async pushForce(repository: Repository): Promise<void> {
await this._push(repository, { pushType: PushType.Push, forcePush: true });
}
@command('git.pushWithTags', { repository: true })
async pushWithTags(repository: Repository): Promise<void> {
const remotes = repository.remotes;
await this._push(repository, { pushType: PushType.PushTags });
}
if (remotes.length === 0) {
window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."));
return;
}
await repository.pushTags();
window.showInformationMessage(localize('push with tags success', "Successfully pushed with tags."));
@command('git.pushWithTagsForce', { repository: true })
async pushWithTagsForce(repository: Repository): Promise<void> {
await this._push(repository, { pushType: PushType.PushTags, forcePush: true });
}
@command('git.pushTo', { repository: true })
async pushTo(repository: Repository): Promise<void> {
const remotes = repository.remotes;
await this._push(repository, { pushType: PushType.PushTo });
}
if (remotes.length === 0) {
window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."));
return;
}
if (!repository.HEAD || !repository.HEAD.name) {
window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
return;
}
const branchName = repository.HEAD.name;
const picks = remotes.filter(r => r.pushUrl !== undefined).map(r => ({ label: r.name, description: r.pushUrl! }));
const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
const pick = await window.showQuickPick(picks, { placeHolder });
if (!pick) {
return;
}
await repository.pushTo(pick.label, branchName);
@command('git.pushToForce', { repository: true })
async pushToForce(repository: Repository): Promise<void> {
await this._push(repository, { pushType: PushType.PushTo, forcePush: true });
}
private async _sync(repository: Repository, rebase: boolean): Promise<void> {
@@ -1627,22 +1914,14 @@ export class CommandCenter {
@command('git.stashPop', { repository: true })
async stashPop(repository: Repository): Promise<void> {
const stashes = await repository.getStashes();
if (stashes.length === 0) {
window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
return;
}
const picks = stashes.map(r => ({ label: `#${r.index}: ${r.description}`, description: '', details: '', id: r.index }));
const placeHolder = localize('pick stash to pop', "Pick a stash to pop");
const choice = await window.showQuickPick(picks, { placeHolder });
const stash = await this.pickStash(repository, placeHolder);
if (!choice) {
if (!stash) {
return;
}
await repository.popStash(choice.id);
await repository.popStash(stash.index);
}
@command('git.stashPopLatest', { repository: true })
@@ -1650,13 +1929,50 @@ export class CommandCenter {
const stashes = await repository.getStashes();
if (stashes.length === 0) {
window.showInformationMessage(localize('no stashes', "There are no stashes to restore."));
window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
return;
}
await repository.popStash();
}
@command('git.stashApply', { repository: true })
async stashApply(repository: Repository): Promise<void> {
const placeHolder = localize('pick stash to apply', "Pick a stash to apply");
const stash = await this.pickStash(repository, placeHolder);
if (!stash) {
return;
}
await repository.applyStash(stash.index);
}
@command('git.stashApplyLatest', { repository: true })
async stashApplyLatest(repository: Repository): Promise<void> {
const stashes = await repository.getStashes();
if (stashes.length === 0) {
window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
return;
}
await repository.applyStash();
}
private async pickStash(repository: Repository, placeHolder: string): Promise<Stash | undefined> {
const stashes = await repository.getStashes();
if (stashes.length === 0) {
window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
return;
}
const picks = stashes.map(stash => ({ label: `#${stash.index}: ${stash.description}`, description: '', details: '', stash }));
const result = await window.showQuickPick(picks, { placeHolder });
return result && result.stash;
}
private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any {
const result = (...args: any[]) => {
let result: Promise<any>;
@@ -1700,6 +2016,11 @@ export class CommandCenter {
let message: string;
let type: 'error' | 'warning' = 'error';
const choices = new Map<string, () => void>();
const openOutputChannelChoice = localize('open git log', "Open Git Log");
const outputChannel = this.outputChannel as OutputChannel;
choices.set(openOutputChannelChoice, () => outputChannel.show());
switch (err.gitErrorCode) {
case GitErrorCodes.DirtyWorkTree:
message = localize('clean repo', "Please clean your repository working tree before checkout.");
@@ -1712,6 +2033,16 @@ export class CommandCenter {
type = 'warning';
options.modal = false;
break;
case GitErrorCodes.StashConflict:
message = localize('stash merge conflicts', "There were merge conflicts while applying the stash.");
type = 'warning';
options.modal = false;
break;
case GitErrorCodes.NoUserNameConfigured:
case GitErrorCodes.NoUserEmailConfigured:
message = localize('missing user info', "Make sure you configure your 'user.name' and 'user.email' in git.");
choices.set(localize('learn more', "Learn More"), () => commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup')));
break;
default:
const hint = (err.stderr || err.message || String(err))
.replace(/^error: /mi, '')
@@ -1732,14 +2063,17 @@ export class CommandCenter {
return;
}
const outputChannel = this.outputChannel as OutputChannel;
const openOutputChannelChoice = localize('open git log', "Open Git Log");
const choice = type === 'error'
? await window.showErrorMessage(message, options, openOutputChannelChoice)
: await window.showWarningMessage(message, options, openOutputChannelChoice);
const allChoices = Array.from(choices.keys());
const result = type === 'error'
? await window.showErrorMessage(message, options, ...allChoices)
: await window.showWarningMessage(message, options, ...allChoices);
if (choice === openOutputChannelChoice) {
outputChannel.show();
if (result) {
const resultFn = choices.get(result);
if (resultFn) {
resultFn();
}
}
});
};
@@ -1778,6 +2112,7 @@ export class CommandCenter {
return repository.workingTreeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]
|| repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
}
return undefined;
}
private runByRepository<T>(resource: Uri, fn: (repository: Repository, resource: Uri) => Promise<T>): Promise<T[]>;

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { workspace, Uri, Disposable, Event, EventEmitter, window } from 'vscode';
import { debounce, throttle } from './decorators';
import { fromGitUri, toGitUri } from './uri';
@@ -92,7 +90,11 @@ export class GitContentProvider {
return '';
}
return await repository.diff(path, { cached: ref === 'index' });
if (ref === 'index') {
return await repository.diffIndexWithHEAD(path);
} else {
return await repository.diffWithHEAD(path);
}
}
const repository = this.model.getRepository(uri);
@@ -112,6 +114,8 @@ export class GitContentProvider {
const uriString = fileUri.toString();
const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString);
ref = indexStatus ? '' : 'HEAD';
} else if (/^~\d$/.test(ref)) {
ref = `:${ref[1]}`;
}
try {

View File

@@ -3,15 +3,13 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'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 { Repository, GitResourceGroup } from './repository';
import { Model } from './model';
import { debounce } from './decorators';
import { filterEvent, dispose, anyEvent, fireEvent } from './util';
import { GitErrorCodes } from './git';
import { GitErrorCodes, Status } from './api/git';
type Callback = { resolve: (status: boolean) => void, reject: (err: any) => void };
@@ -56,6 +54,7 @@ class GitIgnoreDecorationProvider implements DecorationProvider {
color: new ThemeColor('gitDecoration.ignoredResourceForeground')
};
}
return undefined;
});
}
@@ -93,7 +92,7 @@ class GitDecorationProvider implements DecorationProvider {
private static SubmoduleDecorationData: DecorationData = {
title: 'Submodule',
abbreviation: 'S',
letter: 'S',
color: new ThemeColor('gitDecoration.submoduleResourceForeground')
};

View File

@@ -3,12 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { done } from './util';
function decorate(decorator: (fn: Function, key: string) => Function): Function {
return (target: any, key: string, descriptor: any) => {
return (_target: any, key: string, descriptor: any) => {
let fnKey: string | null = null;
let fn: Function | null = null;

View File

@@ -3,13 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as jschardet from 'jschardet';
jschardet.Constants.MINIMUM_THRESHOLD = 0.2;
function detectEncodingByBOM(buffer: NodeBuffer): string | null {
function detectEncodingByBOM(buffer: Buffer): string | null {
if (!buffer || buffer.length < 2) {
return null;
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
@@ -16,6 +14,7 @@ import * as filetype from 'file-type';
import { assign, groupBy, denodeify, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent } from './util';
import { CancellationToken } from 'vscode';
import { detectEncoding } from './encoding';
import { Ref, RefType, Branch, Remote, GitErrorCodes } from './api/git';
const readfile = denodeify<string, string | null, string>(fs.readFile);
@@ -31,40 +30,15 @@ export interface IFileStatus {
rename?: string;
}
export interface Remote {
name: string;
fetchUrl?: string;
pushUrl?: string;
isReadOnly: boolean;
}
export interface Stash {
index: number;
description: string;
}
export enum RefType {
Head,
RemoteHead,
Tag
}
export interface Ref {
type: RefType;
name?: string;
commit?: string;
remote?: string;
}
export interface UpstreamRef {
remote: string;
name: string;
}
export interface Branch extends Ref {
upstream?: UpstreamRef;
ahead?: number;
behind?: number;
interface MutableRemote extends Remote {
fetchUrl?: string;
pushUrl?: string;
isReadOnly: boolean;
}
function parseVersion(raw: string): string {
@@ -309,37 +283,6 @@ export interface IGitOptions {
env?: any;
}
export const GitErrorCodes = {
BadConfigFile: 'BadConfigFile',
AuthenticationFailed: 'AuthenticationFailed',
NoUserNameConfigured: 'NoUserNameConfigured',
NoUserEmailConfigured: 'NoUserEmailConfigured',
NoRemoteRepositorySpecified: 'NoRemoteRepositorySpecified',
NotAGitRepository: 'NotAGitRepository',
NotAtRepositoryRoot: 'NotAtRepositoryRoot',
Conflict: 'Conflict',
UnmergedChanges: 'UnmergedChanges',
PushRejected: 'PushRejected',
RemoteConnectionError: 'RemoteConnectionError',
DirtyWorkTree: 'DirtyWorkTree',
CantOpenResource: 'CantOpenResource',
GitNotFound: 'GitNotFound',
CantCreatePipe: 'CantCreatePipe',
CantAccessRemote: 'CantAccessRemote',
RepositoryNotFound: 'RepositoryNotFound',
RepositoryIsLocked: 'RepositoryIsLocked',
BranchNotFullyMerged: 'BranchNotFullyMerged',
NoRemoteReference: 'NoRemoteReference',
InvalidBranchName: 'InvalidBranchName',
BranchAlreadyExists: 'BranchAlreadyExists',
NoLocalChanges: 'NoLocalChanges',
NoStashFound: 'NoStashFound',
LocalChangesOverwritten: 'LocalChangesOverwritten',
NoUpstreamBranch: 'NoUpstreamBranch',
IsInSubmodule: 'IsInSubmodule',
WrongCase: 'WrongCase',
};
function getGitErrorCode(stderr: string): string | undefined {
if (/Another git process seems to be running in this repository|If no other git process is currently running/.test(stderr)) {
return GitErrorCodes.RepositoryIsLocked;
@@ -427,6 +370,10 @@ export class Git {
return await this._exec(args, options);
}
async exec2(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
return await this._exec(args, options);
}
stream(cwd: string, args: string[], options: SpawnOptions = {}): cp.ChildProcess {
options = assign({ cwd }, options || {});
return this.spawn(args, options);
@@ -674,8 +621,17 @@ export function parseLsFiles(raw: string): LsFilesElement[] {
.map(([, mode, object, stage, file]) => ({ mode, object, stage, file }));
}
export interface DiffOptions {
cached?: boolean;
export interface CommitOptions {
all?: boolean;
amend?: boolean;
signoff?: boolean;
signCommit?: boolean;
empty?: boolean;
}
export enum ForcePushMode {
Force,
ForceWithLease
}
export class Repository {
@@ -706,7 +662,7 @@ export class Repository {
return this.git.spawn(args, options);
}
async config(scope: string, key: string, value: any, options: SpawnOptions): Promise<string> {
async config(scope: string, key: string, value: any = null, options: SpawnOptions = {}): Promise<string> {
const args = ['config'];
if (scope) {
@@ -720,7 +676,25 @@ export class Repository {
}
const result = await this.run(args, options);
return result.stdout;
return result.stdout.trim();
}
async getConfigs(scope: string): Promise<{ key: string; value: string; }[]> {
const args = ['config'];
if (scope) {
args.push('--' + scope);
}
args.push('-l');
const result = await this.run(args);
const lines = result.stdout.trim().split(/\r|\r\n|\n/);
return lines.map(entry => {
const equalsIndex = entry.indexOf('=');
return { key: entry.substr(0, equalsIndex), value: entry.substr(equalsIndex + 1) };
});
}
async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false): Promise<string> {
@@ -848,19 +822,78 @@ export class Repository {
}
}
async diff(path: string, options: DiffOptions = {}): Promise<string> {
async apply(patch: string, reverse?: boolean): Promise<void> {
const args = ['apply', patch];
if (reverse) {
args.push('-R');
}
await this.run(args);
}
async diff(cached = false): Promise<string> {
const args = ['diff'];
if (options.cached) {
if (cached) {
args.push('--cached');
}
args.push('--', path);
const result = await this.run(args);
return result.stdout;
}
async diffWithHEAD(path: string): Promise<string> {
const args = ['diff', '--', path];
const result = await this.run(args);
return result.stdout;
}
async diffWith(ref: string, path: string): Promise<string> {
const args = ['diff', ref, '--', path];
const result = await this.run(args);
return result.stdout;
}
async diffIndexWithHEAD(path: string): Promise<string> {
const args = ['diff', '--cached', '--', path];
const result = await this.run(args);
return result.stdout;
}
async diffIndexWith(ref: string, path: string): Promise<string> {
const args = ['diff', '--cached', ref, '--', path];
const result = await this.run(args);
return result.stdout;
}
async diffBlobs(object1: string, object2: string): Promise<string> {
const args = ['diff', object1, object2];
const result = await this.run(args);
return result.stdout;
}
async diffBetween(ref1: string, ref2: string, path: string): Promise<string> {
const args = ['diff', `${ref1}...${ref2}`, '--', path];
const result = await this.run(args);
return result.stdout.trim();
}
async getMergeBase(ref1: string, ref2: string): Promise<string> {
const args = ['merge-base', ref1, ref2];
const result = await this.run(args);
return result.stdout.trim();
}
async hashObject(data: string): Promise<string> {
const args = ['hash-object', '-w', '--stdin'];
const result = await this.run(args, { input: data });
return result.stdout.trim();
}
async add(paths: string[]): Promise<void> {
const args = ['add', '-A', '--'];
@@ -873,6 +906,18 @@ export class Repository {
await this.run(args);
}
async rm(paths: string[]): Promise<void> {
const args = ['rm', '--'];
if (!paths || !paths.length) {
return;
}
args.push(...paths);
await this.run(args);
}
async stage(path: string, data: string): Promise<void> {
const child = this.stream(['hash-object', '--stdin', '-w', '--path', path], { stdio: [null, null, null] });
child.stdin.end(data, 'utf8');
@@ -899,9 +944,13 @@ export class Repository {
await this.run(['update-index', '--cacheinfo', mode, hash, path]);
}
async checkout(treeish: string, paths: string[]): Promise<void> {
async checkout(treeish: string, paths: string[], opts: { track?: boolean } = Object.create(null)): Promise<void> {
const args = ['checkout', '-q'];
if (opts.track) {
args.push('--track');
}
if (treeish) {
args.push(treeish);
}
@@ -914,7 +963,7 @@ export class Repository {
try {
await this.run(args);
} catch (err) {
if (/Please, commit your changes or stash them/.test(err.stderr || '')) {
if (/Please,? commit your changes or stash them/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.DirtyWorkTree;
}
@@ -922,7 +971,7 @@ export class Repository {
}
}
async commit(message: string, opts: { all?: boolean, amend?: boolean, signoff?: boolean, signCommit?: boolean } = Object.create(null)): Promise<void> {
async commit(message: string, opts: CommitOptions = Object.create(null)): Promise<void> {
const args = ['commit', '--quiet', '--allow-empty-message', '--file', '-'];
if (opts.all) {
@@ -940,6 +989,9 @@ export class Repository {
if (opts.signCommit) {
args.push('-S');
}
if (opts.empty) {
args.push('--allow-empty');
}
try {
await this.run(args, { input: message || '' });
@@ -981,8 +1033,13 @@ export class Repository {
throw commitErr;
}
async branch(name: string, checkout: boolean): Promise<void> {
async branch(name: string, checkout: boolean, ref?: string): Promise<void> {
const args = checkout ? ['checkout', '-q', '-b', name] : ['branch', '-q', name];
if (ref) {
args.push(ref);
}
await this.run(args);
}
@@ -996,6 +1053,11 @@ export class Repository {
await this.run(args);
}
async setBranchUpstream(name: string, upstream: string): Promise<void> {
const args = ['branch', '--set-upstream-to', upstream, name];
await this.run(args);
}
async deleteRef(ref: string): Promise<void> {
const args = ['update-ref', '-d', ref];
await this.run(args);
@@ -1052,14 +1114,7 @@ export class Repository {
}
async reset(treeish: string, hard: boolean = false): Promise<void> {
const args = ['reset'];
if (hard) {
args.push('--hard');
}
args.push(treeish);
const args = ['reset', hard ? '--hard' : '--soft', treeish];
await this.run(args);
}
@@ -1093,9 +1148,36 @@ export class Repository {
}
}
async fetch(): Promise<void> {
async addRemote(name: string, url: string): Promise<void> {
const args = ['remote', 'add', name, url];
await this.run(args);
}
async removeRemote(name: string): Promise<void> {
const args = ['remote', 'rm', name];
await this.run(args);
}
async fetch(options: { remote?: string, ref?: string, all?: boolean, prune?: boolean } = {}): Promise<void> {
const args = ['fetch'];
if (options.remote) {
args.push(options.remote);
if (options.ref) {
args.push(options.ref);
}
} else if (options.all) {
args.push('--all');
}
if (options.prune) {
args.push('--prune');
}
try {
await this.run(['fetch']);
await this.run(args);
} catch (err) {
if (/No remote repository specified\./.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.NoRemoteRepositorySpecified;
@@ -1131,15 +1213,25 @@ export class Repository {
} else if (/Pull is not possible because you have unmerged files|Cannot pull with rebase: You have unstaged changes|Your local changes to the following files would be overwritten|Please, commit your changes before you can merge/i.test(err.stderr)) {
err.stderr = err.stderr.replace(/Cannot pull with rebase: You have unstaged changes/i, 'Cannot pull with rebase, you have unstaged changes');
err.gitErrorCode = GitErrorCodes.DirtyWorkTree;
} else if (/cannot lock ref|unable to update local ref/i.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.CantLockRef;
} else if (/cannot rebase onto multiple branches/i.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.CantRebaseMultipleBranches;
}
throw err;
}
}
async push(remote?: string, name?: string, setUpstream: boolean = false, tags = false): Promise<void> {
async push(remote?: string, name?: string, setUpstream: boolean = false, tags = false, forcePushMode?: ForcePushMode): Promise<void> {
const args = ['push'];
if (forcePushMode === ForcePushMode.ForceWithLease) {
args.push('--force-with-lease');
} else if (forcePushMode === ForcePushMode.Force) {
args.push('--force');
}
if (setUpstream) {
args.push('-u');
}
@@ -1194,9 +1286,17 @@ export class Repository {
}
async popStash(index?: number): Promise<void> {
try {
const args = ['stash', 'pop'];
const args = ['stash', 'pop'];
await this.popOrApplyStash(args, index);
}
async applyStash(index?: number): Promise<void> {
const args = ['stash', 'apply'];
await this.popOrApplyStash(args, index);
}
private async popOrApplyStash(args: string[], index?: number): Promise<void> {
try {
if (typeof index === 'number') {
args.push(`stash@{${index}}`);
}
@@ -1207,6 +1307,8 @@ export class Repository {
err.gitErrorCode = GitErrorCodes.NoStashFound;
} else if (/error: Your local changes to the following files would be overwritten/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.LocalChangesOverwritten;
} else if (/^CONFLICT/m.test(err.stdout || '')) {
err.gitErrorCode = GitErrorCodes.StashConflict;
}
throw err;
@@ -1316,7 +1418,7 @@ export class Repository {
async getRemotes(): Promise<Remote[]> {
const result = await this.run(['remote', '--verbose']);
const lines = result.stdout.trim().split('\n').filter(l => !!l);
const remotes: Remote[] = [];
const remotes: MutableRemote[] = [];
for (const line of lines) {
const parts = line.split(/\s/);
@@ -1348,6 +1450,10 @@ export class Repository {
async getBranch(name: string): Promise<Branch> {
if (name === 'HEAD') {
return this.getHEAD();
} else if (/^@/.test(name)) {
const symbolicFullNameResult = await this.run(['rev-parse', '--symbolic-full-name', name]);
const symbolicFullName = symbolicFullNameResult.stdout.trim();
name = symbolicFullName || name;
}
const result = await this.run(['rev-parse', name]);

View File

@@ -1,57 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
function* filter<T>(it: IterableIterator<T>, condition: (t: T, i: number) => boolean): IterableIterator<T> {
let i = 0;
for (let t of it) {
if (condition(t, i++)) {
yield t;
}
}
}
function* map<T, R>(it: IterableIterator<T>, fn: (t: T, i: number) => R): IterableIterator<R> {
let i = 0;
for (let t of it) {
yield fn(t, i++);
}
}
export interface FunctionalIterator<T> extends Iterable<T> {
filter(condition: (t: T, i: number) => boolean): FunctionalIterator<T>;
map<R>(fn: (t: T, i: number) => R): FunctionalIterator<R>;
toArray(): T[];
}
class FunctionalIteratorImpl<T> implements FunctionalIterator<T> {
constructor(private iterator: IterableIterator<T>) { }
filter(condition: (t: T, i: number) => boolean): FunctionalIterator<T> {
return new FunctionalIteratorImpl(filter(this.iterator, condition));
}
map<R>(fn: (t: T, i: number) => R): FunctionalIterator<R> {
return new FunctionalIteratorImpl(map<T, R>(this.iterator, fn));
}
toArray(): T[] {
return Array.from(this.iterator);
}
[Symbol.iterator](): IterableIterator<T> {
return this.iterator;
}
}
export function iterate<T>(obj: T[] | IterableIterator<T>): FunctionalIterator<T> {
if (Array.isArray(obj)) {
return new FunctionalIteratorImpl(obj[Symbol.iterator]());
}
return new FunctionalIteratorImpl(obj);
}

View File

@@ -3,11 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ExtensionContext, workspace, window, Disposable, commands, OutputChannel } from 'vscode';
import { ExtensionContext, workspace, window, Disposable, commands, OutputChannel } from 'vscode'; // {{SQL CARBON EDIT}} - remove unused imports
import { findGit, Git, IGit } from './git';
import { Model } from './model';
import { CommandCenter } from './commands';
@@ -16,8 +15,12 @@ import { GitDecorations } from './decorationProvider';
import { Askpass } from './askpass';
import { toDisposable, filterEvent, eventToPromise } from './util';
import TelemetryReporter from 'vscode-extension-telemetry';
import { API, NoopAPIImpl, APIImpl } from './api';
import { GitExtension } from './api/git';
import { GitProtocolHandler } from './protocolHandler';
import { GitExtensionImpl } from './api/extension';
// {{SQL CARBON EDIT}} - remove unused imports
// import * as path from 'path';
// import * as fs from 'fs';
const deactivateTasks: { (): Promise<any>; }[] = [];
@@ -69,7 +72,57 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann
return model;
}
export async function activate(context: ExtensionContext): Promise<API> {
// {{SQL CARBON EDIT}} - Comment out function that is unused due to our edit below
// async function isGitRepository(folder: WorkspaceFolder): Promise<boolean> {
// if (folder.uri.scheme !== 'file') {
// return false;
// }
// const dotGit = path.join(folder.uri.fsPath, '.git');
// try {
// const dotGitStat = await new Promise<fs.Stats>((c, e) => fs.stat(dotGit, (err, stat) => err ? e(err) : c(stat)));
// return dotGitStat.isDirectory();
// } catch (err) {
// return false;
// }
// }
// {{SQL CARBON EDIT}} - Comment out function that is unused due to our edit below
// async function warnAboutMissingGit(): Promise<void> {
// const config = workspace.getConfiguration('git');
// const shouldIgnore = config.get<boolean>('ignoreMissingGitWarning') === true;
// if (shouldIgnore) {
// return;
// }
// if (!workspace.workspaceFolders) {
// return;
// }
// const areGitRepositories = await Promise.all(workspace.workspaceFolders.map(isGitRepository));
// if (areGitRepositories.every(isGitRepository => !isGitRepository)) {
// return;
// }
// const download = localize('downloadgit', "Download Git");
// 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,
// neverShowAgain
// );
// if (choice === download) {
// commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/'));
// } else if (choice === neverShowAgain) {
// await config.update('ignoreMissingGitWarning', true, true);
// }
// }
export async function activate(context: ExtensionContext): Promise<GitExtension> {
const disposables: Disposable[] = [];
context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose()));
@@ -77,7 +130,7 @@ export async function activate(context: ExtensionContext): Promise<API> {
commands.registerCommand('git.showOutput', () => outputChannel.show());
disposables.push(outputChannel);
const { name, version, aiKey } = require(context.asAbsolutePath('./package.json')) as { name: string, version: string, aiKey: string };
const { name, version, aiKey } = require('../package.json') as { name: string, version: string, aiKey: string };
const telemetryReporter = new TelemetryReporter(name, version, aiKey);
deactivateTasks.push(() => telemetryReporter.dispose());
@@ -87,46 +140,32 @@ export async function activate(context: ExtensionContext): Promise<API> {
if (!enabled) {
const onConfigChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git'));
const onEnabled = filterEvent(onConfigChange, () => workspace.getConfiguration('git', null).get<boolean>('enabled') === true);
await eventToPromise(onEnabled);
const result = new GitExtensionImpl();
eventToPromise(onEnabled).then(async () => result.model = await createModel(context, outputChannel, telemetryReporter, disposables));
return result;
}
try {
const model = await createModel(context, outputChannel, telemetryReporter, disposables);
return new APIImpl(model);
return new GitExtensionImpl(model);
} catch (err) {
if (!/Git installation not found/.test(err.message || '')) {
throw err;
}
// {{SQL CARBON EDIT}} turn-off Git missing prompt
//const config = workspace.getConfiguration('git');
//const shouldIgnore = config.get<boolean>('ignoreMissingGitWarning') === true;
// console.warn(err.message);
// outputChannel.appendLine(err.message);
// if (!shouldIgnore) {
// console.warn(err.message);
// outputChannel.appendLine(err.message);
// outputChannel.show();
// warnAboutMissingGit();
// const download = localize('downloadgit', "Download Git");
// 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,
// neverShowAgain
// );
// if (choice === download) {
// commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/'));
// } else if (choice === neverShowAgain) {
// await config.update('ignoreMissingGitWarning', true, true);
// }
// }
return new NoopAPIImpl();
return new GitExtensionImpl();
}
}
async function checkGitVersion(info: IGit): Promise<void> {
// {{SQL CARBON EDIT}} - Rename info to _info to prevent error due to unused variable
async function checkGitVersion(_info: IGit): Promise<void> {
// {{SQL CARBON EDIT}}
// remove Git version check for azuredatastudio

View File

@@ -3,17 +3,16 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
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, isDescendant, firstIndex } from './util';
import { Git, GitErrorCodes } from './git';
import { Git } from './git';
import * as path from 'path';
import * as fs from 'fs';
import * as nls from 'vscode-nls';
import { fromGitUri } from './uri';
import { GitErrorCodes } from './api/git';
const localize = nls.loadMessageBundle();
@@ -107,6 +106,18 @@ export class Model {
children
.filter(child => child !== '.git')
.forEach(child => this.openRepository(path.join(root, child)));
const folderConfig = workspace.getConfiguration('git', folder.uri);
const paths = folderConfig.get<string[]>('scanRepositories') || [];
for (const possibleRepositoryPath of paths) {
if (path.isAbsolute(possibleRepositoryPath)) {
console.warn(localize('not supported', "Absolute paths not supported in 'git.scanRepositories' setting."));
continue;
}
this.openRepository(path.join(root, possibleRepositoryPath));
}
} catch (err) {
// noop
}
@@ -241,17 +252,28 @@ export class Model {
const changeListener = repository.onDidChangeRepository(uri => this._onDidChangeRepository.fire({ repository, uri }));
const originalResourceChangeListener = repository.onDidChangeOriginalResource(uri => this._onDidChangeOriginalResource.fire({ repository, uri }));
const shouldDetectSubmodules = workspace
.getConfiguration('git', Uri.file(repository.root))
.get<boolean>('detectSubmodules') as boolean;
const submodulesLimit = workspace
.getConfiguration('git', Uri.file(repository.root))
.get<number>('detectSubmodulesLimit') as number;
const checkForSubmodules = () => {
if (!shouldDetectSubmodules) {
return;
}
if (repository.submodules.length > submodulesLimit) {
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();
}
this.scanSubmodules(repository, submodulesLimit);
repository.submodules
.slice(0, submodulesLimit)
.map(r => path.join(repository.root, r.path))
.forEach(p => this.eventuallyScanPossibleGitRepository(p));
};
const statusListener = repository.onDidRunGitStatus(checkForSubmodules);
@@ -273,21 +295,6 @@ export class Model {
this._onDidOpenRepository.fire(repository);
}
private scanSubmodules(repository: Repository, limit: number): void {
const shouldScanSubmodules = workspace
.getConfiguration('git', Uri.file(repository.root))
.get<boolean>('detectSubmodules') === true;
if (!shouldScanSubmodules) {
return;
}
repository.submodules
.slice(0, limit)
.map(r => path.join(repository.root, r.path))
.forEach(p => this.eventuallyScanPossibleGitRepository(p));
}
close(repository: Repository): void {
const openRepository = this.getOpenRepository(repository);

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { UriHandler, Uri, window, Disposable, commands } from 'vscode';
import { dispose } from './util';
import * as querystring from 'querystring';

View File

@@ -3,10 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
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 { Repository as BaseRepository, Commit, Stash, GitError, Submodule, CommitOptions, ForcePushMode } from './git';
import { anyEvent, filterEvent, eventToPromise, dispose, find, isDescendant, IDisposable, onceEvent, EmptyDisposable, debounceEvent } from './util';
import { memoize, throttle, debounce } from './decorators';
import { toGitUri } from './uri';
@@ -15,6 +13,7 @@ import * as path from 'path';
import * as nls from 'vscode-nls';
import * as fs from 'fs';
import { StatusBarCommands } from './statusbar';
import { Branch, Ref, Remote, RefType, GitErrorCodes, Status } from './api/git';
const timeout = (millis: number) => new Promise(c => setTimeout(c, millis));
@@ -25,33 +24,12 @@ function getIconUri(iconName: string, theme: string): Uri {
return Uri.file(path.join(iconsRootPath, theme, `${iconName}.svg`));
}
export enum RepositoryState {
export const enum RepositoryState {
Idle,
Disposed
}
export enum Status {
INDEX_MODIFIED,
INDEX_ADDED,
INDEX_DELETED,
INDEX_RENAMED,
INDEX_COPIED,
MODIFIED,
DELETED,
UNTRACKED,
IGNORED,
ADDED_BY_US,
ADDED_BY_THEM,
DELETED_BY_US,
DELETED_BY_THEM,
BOTH_ADDED,
BOTH_DELETED,
BOTH_MODIFIED
}
export enum ResourceGroupType {
export const enum ResourceGroupType {
Merge,
Index,
WorkingTree
@@ -123,6 +101,7 @@ 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: throw new Error('Unknown git status: ' + this.type);
}
}
@@ -197,15 +176,19 @@ export class Resource implements SourceControlResourceState {
return 'U';
case Status.IGNORED:
return 'I';
case Status.DELETED_BY_THEM:
return 'D';
case Status.DELETED_BY_US:
return 'D';
case Status.INDEX_COPIED:
case Status.BOTH_DELETED:
case Status.ADDED_BY_US:
case Status.DELETED_BY_THEM:
case Status.ADDED_BY_THEM:
case Status.DELETED_BY_US:
case Status.BOTH_ADDED:
case Status.BOTH_MODIFIED:
return 'C';
default:
throw new Error('Unknown git status: ' + this.type);
}
}
@@ -233,6 +216,8 @@ export class Resource implements SourceControlResourceState {
case Status.BOTH_ADDED:
case Status.BOTH_MODIFIED:
return new ThemeColor('gitDecoration.conflictingResourceForeground');
default:
throw new Error('Unknown git status: ' + this.type);
}
}
@@ -259,10 +244,10 @@ export class Resource implements SourceControlResourceState {
get resourceDecoration(): DecorationData {
const title = this.tooltip;
const abbreviation = this.letter;
const letter = this.letter;
const color = this.color;
const priority = this.priority;
return { bubble: true, source: 'git.resource', title, abbreviation, color, priority };
return { bubble: true, source: 'git.resource', title, letter, color, priority };
}
constructor(
@@ -274,16 +259,24 @@ export class Resource implements SourceControlResourceState {
) { }
}
export enum Operation {
export const enum Operation {
Status = 'Status',
Config = 'Config',
Diff = 'Diff',
MergeBase = 'MergeBase',
Add = 'Add',
Remove = 'Remove',
RevertFiles = 'RevertFiles',
Commit = 'Commit',
Clean = 'Clean',
Branch = 'Branch',
GetBranch = 'GetBranch',
SetBranchUpstream = 'SetBranchUpstream',
HashObject = 'HashObject',
Checkout = 'Checkout',
CheckoutTracking = 'CheckoutTracking',
Reset = 'Reset',
Remote = 'Remote',
Fetch = 'Fetch',
Pull = 'Pull',
Push = 'Push',
@@ -302,6 +295,7 @@ export enum Operation {
GetObjectDetails = 'GetObjectDetails',
SubmoduleUpdate = 'SubmoduleUpdate',
RebaseContinue = 'RebaseContinue',
Apply = 'Apply'
}
function isReadOnly(operation: Operation): boolean {
@@ -310,6 +304,7 @@ function isReadOnly(operation: Operation): boolean {
case Operation.GetCommitTemplate:
case Operation.CheckIgnore:
case Operation.GetObjectDetails:
case Operation.MergeBase:
return true;
default:
return false;
@@ -381,13 +376,6 @@ class OperationsImpl implements Operations {
}
}
export interface CommitOptions {
all?: boolean;
amend?: boolean;
signoff?: boolean;
signCommit?: boolean;
}
export interface GitResourceGroup extends SourceControlResourceGroup {
resourceStates: Resource[];
}
@@ -399,11 +387,32 @@ export interface OperationResult {
class ProgressManager {
private enabled = false;
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()));
constructor(private repository: Repository) {
const onDidChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git', Uri.file(this.repository.root)));
onDidChange(_ => this.updateEnablement());
this.updateEnablement();
}
private updateEnablement(): void {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
if (config.get<boolean>('showProgress')) {
this.enable();
} else {
this.disable();
}
}
private enable(): void {
if (this.enabled) {
return;
}
const start = onceEvent(filterEvent(this.repository.onDidChangeOperations, () => this.repository.operations.shouldShowProgress()));
const end = onceEvent(filterEvent(debounceEvent(this.repository.onDidChangeOperations, 300), () => !this.repository.operations.shouldShowProgress()));
const setup = () => {
this.disposable = start(() => {
@@ -413,17 +422,26 @@ class ProgressManager {
};
setup();
this.enabled = true;
}
private disable(): void {
if (!this.enabled) {
return;
}
this.disposable.dispose();
this.disposable = EmptyDisposable;
this.enabled = false;
}
dispose(): void {
this.disposable.dispose();
this.disable();
}
}
export class Repository implements Disposable {
private static readonly InputValidationLength = 72;
private _onDidChangeRepository = new EventEmitter<Uri>();
readonly onDidChangeRepository: Event<Uri> = this._onDidChangeRepository.event;
@@ -521,6 +539,7 @@ export class Repository implements Disposable {
private isRepositoryHuge = false;
private didWarnAboutLimit = false;
private isFreshRepository: boolean | undefined = undefined;
private disposables: Disposable[] = [];
constructor(
@@ -530,15 +549,27 @@ export class Repository implements Disposable {
const fsWatcher = workspace.createFileSystemWatcher('**');
this.disposables.push(fsWatcher);
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 workspaceFilter = (uri: Uri) => isDescendant(repository.root, uri.fsPath);
const onWorkspaceDelete = filterEvent(fsWatcher.onDidDelete, workspaceFilter);
const onWorkspaceChange = filterEvent(anyEvent(fsWatcher.onDidChange, fsWatcher.onDidCreate), workspaceFilter);
const onRepositoryDotGitDelete = filterEvent(onWorkspaceDelete, uri => /\/\.git$/.test(uri.path));
const onRepositoryChange = anyEvent(onWorkspaceDelete, onWorkspaceChange);
// relevant repository changes are:
// - DELETE .git folder
// - ANY CHANGE within .git folder except .git itself and .git/index.lock
const onRelevantRepositoryChange = anyEvent(
onRepositoryDotGitDelete,
filterEvent(onRepositoryChange, uri => !/\/\.git(\/index\.lock)?$/.test(uri.path))
);
onRelevantRepositoryChange(this.onFSChange, this, this.disposables);
const onRelevantGitChange = filterEvent(onRelevantRepositoryChange, uri => /\/\.git\//.test(uri.path));
onRelevantGitChange(this._onDidChangeRepository.fire, this._onDidChangeRepository, this.disposables);
this._sourceControl = scm.createSourceControl('git', 'Git', Uri.file(repository.root));
const root = Uri.file(repository.root);
this._sourceControl = scm.createSourceControl('git', 'Git', root);
this._sourceControl.inputBox.placeholder = localize('commitMessage', "Message (press {0} to commit)");
this._sourceControl.acceptInputCommand = { command: 'git.commitWithInput', title: localize('commit', "Commit"), arguments: [this._sourceControl] };
this._sourceControl.quickDiffProvider = this;
@@ -549,8 +580,16 @@ export class Repository implements Disposable {
this._indexGroup = this._sourceControl.createResourceGroup('index', localize('staged changes', "Staged Changes"));
this._workingTreeGroup = this._sourceControl.createResourceGroup('workingTree', localize('changes', "Changes"));
const updateIndexGroupVisibility = () => {
const config = workspace.getConfiguration('git', root);
this.indexGroup.hideWhenEmpty = !config.get<boolean>('alwaysShowStagedChangesResourceGroup');
};
const onConfigListener = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.alwaysShowStagedChangesResourceGroup', root));
onConfigListener(updateIndexGroupVisibility, this, this.disposables);
updateIndexGroupVisibility();
this.mergeGroup.hideWhenEmpty = true;
this.indexGroup.hideWhenEmpty = true;
this.disposables.push(this.mergeGroup);
this.disposables.push(this.indexGroup);
@@ -615,19 +654,20 @@ export class Repository implements Disposable {
end = match ? match.index : text.length;
const line = text.substring(start, end);
const threshold = Math.max(config.get<number>('inputValidationLength') || 72, 0) || 72;
if (line.length <= Repository.InputValidationLength) {
if (line.length <= threshold) {
if (setting !== 'always') {
return;
}
return {
message: localize('commitMessageCountdown', "{0} characters left in current line", Repository.InputValidationLength - line.length),
message: localize('commitMessageCountdown', "{0} characters left in current line", threshold - line.length),
type: SourceControlInputBoxValidationType.Information
};
} else {
return {
message: localize('commitMessageWarning', "{0} characters over {1} in current line", line.length - Repository.InputValidationLength, Repository.InputValidationLength),
message: localize('commitMessageWarning', "{0} characters over {1} in current line", line.length - threshold, threshold),
type: SourceControlInputBoxValidationType.Warning
};
}
@@ -649,19 +689,67 @@ export class Repository implements Disposable {
}
}
getConfigs(): Promise<{ key: string; value: string; }[]> {
return this.run(Operation.Config, () => this.repository.getConfigs('local'));
}
getConfig(key: string): Promise<string> {
return this.run(Operation.Config, () => this.repository.config('local', key));
}
setConfig(key: string, value: string): Promise<string> {
return this.run(Operation.Config, () => this.repository.config('local', key, value));
}
@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));
diff(cached?: boolean): Promise<string> {
return this.run(Operation.Diff, () => this.repository.diff(cached));
}
diffWithHEAD(path: string): Promise<string> {
return this.run(Operation.Diff, () => this.repository.diffWithHEAD(path));
}
diffWith(ref: string, path: string): Promise<string> {
return this.run(Operation.Diff, () => this.repository.diffWith(ref, path));
}
diffIndexWithHEAD(path: string): Promise<string> {
return this.run(Operation.Diff, () => this.repository.diffIndexWithHEAD(path));
}
diffIndexWith(ref: string, path: string): Promise<string> {
return this.run(Operation.Diff, () => this.repository.diffIndexWith(ref, path));
}
diffBlobs(object1: string, object2: string): Promise<string> {
return this.run(Operation.Diff, () => this.repository.diffBlobs(object1, object2));
}
diffBetween(ref1: string, ref2: string, path: string): Promise<string> {
return this.run(Operation.Diff, () => this.repository.diffBetween(ref1, ref2, path));
}
getMergeBase(ref1: string, ref2: string): Promise<string> {
return this.run(Operation.MergeBase, () => this.repository.getMergeBase(ref1, ref2));
}
async hashObject(data: string): Promise<string> {
return this.run(Operation.HashObject, () => this.repository.hashObject(data));
}
async add(resources: Uri[]): Promise<void> {
await this.run(Operation.Add, () => this.repository.add(resources.map(r => r.fsPath)));
}
async rm(resources: Uri[]): Promise<void> {
await this.run(Operation.Remove, () => this.repository.rm(resources.map(r => r.fsPath)));
}
async stage(resource: Uri, contents: string): Promise<void> {
const relativePath = path.relative(this.repository.root, resource.fsPath).replace(/\\/g, '/');
await this.run(Operation.Stage, () => this.repository.stage(relativePath, contents));
@@ -745,8 +833,8 @@ export class Repository implements Disposable {
});
}
async branch(name: string): Promise<void> {
await this.run(Operation.Branch, () => this.repository.branch(name, true));
async branch(name: string, _checkout: boolean, _ref?: string): Promise<void> {
await this.run(Operation.Branch, () => this.repository.branch(name, _checkout, _ref));
}
async deleteBranch(name: string, force?: boolean): Promise<void> {
@@ -757,6 +845,14 @@ export class Repository implements Disposable {
await this.run(Operation.RenameBranch, () => this.repository.renameBranch(name));
}
async getBranch(name: string): Promise<Branch> {
return await this.run(Operation.GetBranch, () => this.repository.getBranch(name));
}
async setBranchUpstream(name: string, upstream: string): Promise<void> {
await this.run(Operation.SetBranchUpstream, () => this.repository.setBranchUpstream(name, upstream));
}
async merge(ref: string): Promise<void> {
await this.run(Operation.Merge, () => this.repository.merge(ref));
}
@@ -769,6 +865,10 @@ export class Repository implements Disposable {
await this.run(Operation.Checkout, () => this.repository.checkout(treeish, []));
}
async checkoutTracking(treeish: string): Promise<void> {
await this.run(Operation.CheckoutTracking, () => this.repository.checkout(treeish, [], { track: true }));
}
async getCommit(ref: string): Promise<Commit> {
return await this.repository.getCommit(ref);
}
@@ -781,11 +881,33 @@ export class Repository implements Disposable {
await this.run(Operation.DeleteRef, () => this.repository.deleteRef(ref));
}
async addRemote(name: string, url: string): Promise<void> {
await this.run(Operation.Remote, () => this.repository.addRemote(name, url));
}
async removeRemote(name: string): Promise<void> {
await this.run(Operation.Remote, () => this.repository.removeRemote(name));
}
@throttle
async fetch(): Promise<void> {
async fetchDefault(): Promise<void> {
await this.run(Operation.Fetch, () => this.repository.fetch());
}
@throttle
async fetchPrune(): Promise<void> {
await this.run(Operation.Fetch, () => this.repository.fetch({ prune: true }));
}
@throttle
async fetchAll(): Promise<void> {
await this.run(Operation.Fetch, () => this.repository.fetch({ all: true }));
}
async fetch(remote?: string, ref?: string): Promise<void> {
await this.run(Operation.Fetch, () => this.repository.fetch({ remote, ref }));
}
@throttle
async pullWithRebase(head: Branch | undefined): Promise<void> {
let remote: string | undefined;
@@ -796,11 +918,18 @@ export class Repository implements Disposable {
branch = `${head.upstream.name}`;
}
await this.run(Operation.Pull, () => this.repository.pull(true, remote, branch));
const config = workspace.getConfiguration('git', Uri.file(this.root));
const fetchOnPull = config.get<boolean>('fetchOnPull');
if (fetchOnPull) {
await this.run(Operation.Pull, () => this.repository.pull(true));
} else {
await this.run(Operation.Pull, () => this.repository.pull(true, remote, branch));
}
}
@throttle
async pull(head: Branch | undefined): Promise<void> {
async pull(head?: Branch): Promise<void> {
let remote: string | undefined;
let branch: string | undefined;
@@ -809,15 +938,29 @@ export class Repository implements Disposable {
branch = `${head.upstream.name}`;
}
await this.run(Operation.Pull, () => this.repository.pull(false, remote, branch));
const config = workspace.getConfiguration('git', Uri.file(this.root));
const fetchOnPull = config.get<boolean>('fetchOnPull');
if (fetchOnPull) {
await this.run(Operation.Pull, () => this.repository.pull(false));
} else {
await this.run(Operation.Pull, () => this.repository.pull(false, remote, branch));
}
}
async pullFrom(rebase?: boolean, remote?: string, branch?: string): Promise<void> {
await this.run(Operation.Pull, () => this.repository.pull(rebase, remote, branch));
const config = workspace.getConfiguration('git', Uri.file(this.root));
const fetchOnPull = config.get<boolean>('fetchOnPull');
if (fetchOnPull) {
await this.run(Operation.Pull, () => this.repository.pull(rebase));
} else {
await this.run(Operation.Pull, () => this.repository.pull(rebase, remote, branch));
}
}
@throttle
async push(head: Branch): Promise<void> {
async push(head: Branch, forcePushMode?: ForcePushMode): Promise<void> {
let remote: string | undefined;
let branch: string | undefined;
@@ -826,15 +969,15 @@ export class Repository implements Disposable {
branch = `${head.name}:${head.upstream.name}`;
}
await this.run(Operation.Push, () => this.repository.push(remote, branch));
await this.run(Operation.Push, () => this.repository.push(remote, branch, undefined, undefined, forcePushMode));
}
async pushTo(remote?: string, name?: string, setUpstream: boolean = false): Promise<void> {
await this.run(Operation.Push, () => this.repository.push(remote, name, setUpstream));
async pushTo(remote?: string, name?: string, setUpstream: boolean = false, forcePushMode?: ForcePushMode): Promise<void> {
await this.run(Operation.Push, () => this.repository.push(remote, name, setUpstream, undefined, forcePushMode));
}
async pushTags(remote?: string): Promise<void> {
await this.run(Operation.Push, () => this.repository.push(remote, undefined, false, true));
async pushTags(remote?: string, forcePushMode?: ForcePushMode): Promise<void> {
await this.run(Operation.Push, () => this.repository.push(remote, undefined, false, true, forcePushMode));
}
@throttle
@@ -859,7 +1002,14 @@ export class Repository implements Disposable {
}
await this.run(Operation.Sync, async () => {
await this.repository.pull(rebase, remoteName, pullBranch);
const config = workspace.getConfiguration('git', Uri.file(this.root));
const fetchOnPull = config.get<boolean>('fetchOnPull');
if (fetchOnPull) {
await this.repository.pull(rebase);
} else {
await this.repository.pull(rebase, remoteName, pullBranch);
}
const remote = this.remotes.find(r => r.name === remoteName);
@@ -910,6 +1060,10 @@ export class Repository implements Disposable {
return this.run(Operation.Show, () => this.repository.detectObjectType(object));
}
async apply(patch: string, reverse?: boolean): Promise<void> {
return await this.run(Operation.Apply, () => this.repository.apply(patch, reverse));
}
async getStashes(): Promise<Stash[]> {
return await this.repository.getStashes();
}
@@ -922,6 +1076,10 @@ export class Repository implements Disposable {
return await this.run(Operation.Stash, () => this.repository.popStash(index));
}
async applyStash(index?: number): Promise<void> {
return await this.run(Operation.Stash, () => this.repository.applyStash(index));
}
async getCommitTemplate(): Promise<string> {
return await this.run(Operation.GetCommitTemplate, async () => this.repository.getCommitTemplate());
}
@@ -1009,7 +1167,7 @@ export class Repository implements Disposable {
this._onRunOperation.fire(operation);
try {
const result = await this.retryRun(runOperation);
const result = await this.retryRun(operation, runOperation);
if (!isReadOnly(operation)) {
await this.updateModelState();
@@ -1030,7 +1188,7 @@ export class Repository implements Disposable {
}
}
private async retryRun<T>(runOperation: () => Promise<T> = () => Promise.resolve<any>(null)): Promise<T> {
private async retryRun<T>(operation: Operation, runOperation: () => Promise<T> = () => Promise.resolve<any>(null)): Promise<T> {
let attempt = 0;
while (true) {
@@ -1038,7 +1196,12 @@ export class Repository implements Disposable {
attempt++;
return await runOperation();
} catch (err) {
if (err.gitErrorCode === GitErrorCodes.RepositoryIsLocked && attempt <= 10) {
const shouldRetry = attempt <= 10 && (
(err.gitErrorCode === GitErrorCodes.RepositoryIsLocked)
|| ((operation === Operation.Pull || operation === Operation.Sync || operation === Operation.Fetch) && (err.gitErrorCode === GitErrorCodes.CantLockRef || err.gitErrorCode === GitErrorCodes.CantRebaseMultipleBranches))
);
if (shouldRetry) {
// quatratic backoff
await timeout(Math.pow(attempt, 2) * 50);
} else {
@@ -1125,6 +1288,7 @@ export class Repository implements Disposable {
case 'M': workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.MODIFIED, useIcons, renameUri)); break;
case 'D': workingTree.push(new Resource(ResourceGroupType.WorkingTree, uri, Status.DELETED, useIcons, renameUri)); break;
}
return undefined;
});
// set resource groups
@@ -1145,23 +1309,37 @@ export class Repository implements Disposable {
// Disable `Discard All Changes` for "fresh" repositories
// https://github.com/Microsoft/vscode/issues/43066
commands.executeCommand('setContext', 'gitFreshRepository', !this._HEAD || !this._HEAD.commit);
const isFreshRepository = !this._HEAD || !this._HEAD.commit;
if (this.isFreshRepository !== isFreshRepository) {
commands.executeCommand('setContext', 'gitFreshRepository', isFreshRepository);
this.isFreshRepository = isFreshRepository;
}
this._onDidChangeStatus.fire();
}
private async getRebaseCommit(): Promise<Commit | undefined> {
const rebaseHeadPath = path.join(this.repository.root, '.git', 'REBASE_HEAD');
const rebaseApplyPath = path.join(this.repository.root, '.git', 'rebase-apply');
const rebaseMergePath = path.join(this.repository.root, '.git', 'rebase-merge');
try {
const rebaseHead = await new Promise<string>((c, e) => fs.readFile(rebaseHeadPath, 'utf8', (err, result) => err ? e(err) : c(result)));
const [rebaseApplyExists, rebaseMergePathExists, rebaseHead] = await Promise.all([
new Promise<boolean>(c => fs.exists(rebaseApplyPath, c)),
new Promise<boolean>(c => fs.exists(rebaseMergePath, c)),
new Promise<string>((c, e) => fs.readFile(rebaseHeadPath, 'utf8', (err, result) => err ? e(err) : c(result)))
]);
if (!rebaseApplyExists && !rebaseMergePathExists) {
return undefined;
}
return await this.getCommit(rebaseHead.trim());
} catch (err) {
return undefined;
}
}
private onFSChange(uri: Uri): void {
private onFSChange(_uri: Uri): void {
const config = workspace.getConfiguration('git');
const autorefresh = config.get<boolean>('autorefresh');

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { TextDocument, Range, LineChange, Selection } from 'vscode';
export function applyLineChanges(original: TextDocument, modified: TextDocument, diffs: LineChange[]): string {
@@ -21,9 +19,12 @@ export function applyLineChanges(original: TextDocument, modified: TextDocument,
let fromLine = diff.modifiedStartLineNumber - 1;
let fromCharacter = 0;
// if this is an insertion at the very end of the document,
// then we must start the next range after the last character of the
// previous line, in order to take the correct eol
if (isInsertion && diff.originalStartLineNumber === original.lineCount) {
fromLine = original.lineCount - 1;
fromCharacter = original.lineAt(fromLine).range.end.character;
fromLine -= 1;
fromCharacter = modified.lineAt(fromLine).range.end.character;
}
result.push(modified.getText(new Range(fromLine, fromCharacter, diff.modifiedEndLineNumber, 0)));
@@ -76,6 +77,8 @@ export function getModifiedRange(textDocument: TextDocument, diff: LineChange):
if (diff.modifiedEndLineNumber === 0) {
if (diff.modifiedStartLineNumber === 0) {
return new Range(textDocument.lineAt(diff.modifiedStartLineNumber).range.end, textDocument.lineAt(diff.modifiedStartLineNumber).range.start);
} else if (textDocument.lineCount === diff.modifiedStartLineNumber) {
return new Range(textDocument.lineAt(diff.modifiedStartLineNumber - 1).range.end, textDocument.lineAt(diff.modifiedStartLineNumber - 1).range.end);
} else {
return new Range(textDocument.lineAt(diff.modifiedStartLineNumber - 1).range.end, textDocument.lineAt(diff.modifiedStartLineNumber).range.start);
}

View File

@@ -3,13 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Disposable, Command, EventEmitter, Event } from 'vscode';
import { Branch } from './git';
import { Disposable, Command, EventEmitter, Event, workspace, Uri } from 'vscode';
import { Repository, Operation } from './repository';
import { anyEvent, dispose } from './util';
import * as nls from 'vscode-nls';
import { Branch } from './api/git';
const localize = nls.loadMessageBundle();
@@ -103,7 +101,11 @@ class SyncStatusBar {
if (HEAD.ahead || HEAD.behind) {
text += this.repository.syncLabel;
}
command = 'git.sync';
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const rebaseWhenSync = config.get<string>('rebaseWhenSync');
command = rebaseWhenSync ? 'git.syncRebase' : 'git.sync';
tooltip = localize('sync changes', "Synchronize Changes");
} else {
icon = '$(cloud-upload)';

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'mocha';
import { GitStatusParser, parseGitCommit, parseGitmodules, parseLsTree, parseLsFiles } from '../git';
import * as assert from 'assert';

View File

@@ -3,7 +3,7 @@ declare module 'jschardet' {
encoding: string,
confidence: number
}
export function detect(buffer: NodeBuffer): IDetectedMap;
export function detect(buffer: Buffer): IDetectedMap;
export const Constants: {
MINIMUM_THRESHOLD: number,

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Uri } from 'vscode';
export interface GitUriParams {

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Event } from 'vscode';
import { dirname, sep } from 'path';
import { Readable } from 'stream';
@@ -35,7 +33,7 @@ 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);
return (listener, thisArgs = null, disposables?) => event(_ => (listener as any).call(thisArgs), null, disposables);
}
export function mapEvent<I, O>(event: Event<I>, map: (i: I) => O): Event<O> {
@@ -46,6 +44,18 @@ export function filterEvent<T>(event: Event<T>, filter: (e: T) => boolean): Even
return (listener, thisArgs = null, disposables?) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables);
}
export function latchEvent<T>(event: Event<T>): Event<T> {
let firstCall = true;
let cache: T;
return filterEvent(event, value => {
let shouldEmit = firstCall || value !== cache;
firstCall = false;
cache = value;
return shouldEmit;
});
}
export function anyEvent<T>(...events: Event<T>[]): Event<T> {
return (listener, thisArgs = null, disposables?) => {
const result = combinedDisposable(events.map(event => event(i => listener.call(thisArgs, i))));
@@ -268,7 +278,7 @@ export function readBytes(stream: Readable, bytes: number): Promise<Buffer> {
});
}
export enum Encoding {
export const enum Encoding {
UTF8 = 'utf8',
UTF16be = 'utf16be',
UTF16le = 'utf16le'