Archived
1
0

Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@ -0,0 +1,364 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. 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, LogOptions, APIState, CommitOptions, RefType, RemoteSourceProvider, CredentialsProvider, BranchQuery, PushErrorHandler } from './git';
import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands } from 'vscode';
import { mapEvent } from '../util';
import { toGitUri } from '../uri';
import { pickRemoteSource, PickRemoteSourceOptions } from '../remoteSource';
import { GitExtensionImpl } from './extension';
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);
}
getGlobalConfig(key: string): Promise<string> {
return this._repository.getGlobalConfig(key);
}
getObjectDetails(treeish: string, path: string): Promise<{ mode: string; object: string; size: number; }> {
return this._repository.getObjectDetails(treeish, path);
}
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(): Promise<Change[]>;
diffWithHEAD(path: string): Promise<string>;
diffWithHEAD(path?: string): Promise<string | Change[]> {
return this._repository.diffWithHEAD(path);
}
diffWith(ref: string): Promise<Change[]>;
diffWith(ref: string, path: string): Promise<string>;
diffWith(ref: string, path?: string): Promise<string | Change[]> {
return this._repository.diffWith(ref, path);
}
diffIndexWithHEAD(): Promise<Change[]>;
diffIndexWithHEAD(path: string): Promise<string>;
diffIndexWithHEAD(path?: string): Promise<string | Change[]> {
return this._repository.diffIndexWithHEAD(path);
}
diffIndexWith(ref: string): Promise<Change[]>;
diffIndexWith(ref: string, path: string): Promise<string>;
diffIndexWith(ref: string, path?: string): Promise<string | Change[]> {
return this._repository.diffIndexWith(ref, path);
}
diffBlobs(object1: string, object2: string): Promise<string> {
return this._repository.diffBlobs(object1, object2);
}
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
diffBetween(ref1: string, ref2: string, path?: string): Promise<string | Change[]> {
return this._repository.diffBetween(ref1, ref2, path);
}
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);
}
getBranches(query: BranchQuery): Promise<Ref[]> {
return this._repository.getBranches(query);
}
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);
}
renameRemote(name: string, newName: string): Promise<void> {
return this._repository.renameRemote(name, newName);
}
fetch(remote?: string | undefined, ref?: string | undefined, depth?: number | undefined): Promise<void> {
return this._repository.fetch(remote, ref, depth);
}
pull(unshallow?: boolean): Promise<void> {
return this._repository.pull(undefined, unshallow);
}
push(remoteName?: string, branchName?: string, setUpstream: boolean = false): Promise<void> {
return this._repository.pushTo(remoteName, branchName, setUpstream);
}
blame(path: string): Promise<string> {
return this._repository.blame(path);
}
log(options?: LogOptions): Promise<Commit[]> {
return this._repository.log(options);
}
commit(message: string, opts?: CommitOptions): Promise<void> {
return this._repository.commit(message, opts);
}
}
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 state(): APIState {
return this._model.state;
}
get onDidChangeState(): Event<APIState> {
return this._model.onDidChangeState;
}
get onDidOpenRepository(): Event<Repository> {
return mapEvent(this._model.onDidOpenRepository, r => new ApiRepository(r));
}
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));
}
toGitUri(uri: Uri, ref: string): Uri {
return toGitUri(uri, ref);
}
getRepository(uri: Uri): Repository | null {
const result = this._model.getRepository(uri);
return result ? new ApiRepository(result) : null;
}
async init(root: Uri): Promise<Repository | null> {
const path = root.fsPath;
await this._model.git.init(path);
await this._model.openRepository(path);
return this.getRepository(root) || null;
}
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable {
return this._model.registerRemoteSourceProvider(provider);
}
registerCredentialsProvider(provider: CredentialsProvider): Disposable {
return this._model.registerCredentialsProvider(provider);
}
registerPushErrorHandler(handler: PushErrorHandler): Disposable {
return this._model.registerPushErrorHandler(handler);
}
constructor(private _model: Model) { }
}
function getRefType(type: RefType): string {
switch (type) {
case RefType.Head: return 'Head';
case RefType.RemoteHead: return 'RemoteHead';
case RefType.Tag: return 'Tag';
}
return 'unknown';
}
function getStatus(status: Status): string {
switch (status) {
case Status.INDEX_MODIFIED: return 'INDEX_MODIFIED';
case Status.INDEX_ADDED: return 'INDEX_ADDED';
case Status.INDEX_DELETED: return 'INDEX_DELETED';
case Status.INDEX_RENAMED: return 'INDEX_RENAMED';
case Status.INDEX_COPIED: return 'INDEX_COPIED';
case Status.MODIFIED: return 'MODIFIED';
case Status.DELETED: return 'DELETED';
case Status.UNTRACKED: return 'UNTRACKED';
case Status.IGNORED: return 'IGNORED';
case Status.INTENT_TO_ADD: return 'INTENT_TO_ADD';
case Status.ADDED_BY_US: return 'ADDED_BY_US';
case Status.ADDED_BY_THEM: return 'ADDED_BY_THEM';
case Status.DELETED_BY_US: return 'DELETED_BY_US';
case Status.DELETED_BY_THEM: return 'DELETED_BY_THEM';
case Status.BOTH_ADDED: return 'BOTH_ADDED';
case Status.BOTH_DELETED: return 'BOTH_DELETED';
case Status.BOTH_MODIFIED: return 'BOTH_MODIFIED';
}
return 'UNKNOWN';
}
export function registerAPICommands(extension: GitExtensionImpl): Disposable {
const disposables: Disposable[] = [];
disposables.push(commands.registerCommand('git.api.getRepositories', () => {
const api = extension.getAPI(1);
return api.repositories.map(r => r.rootUri.toString());
}));
disposables.push(commands.registerCommand('git.api.getRepositoryState', (uri: string) => {
const api = extension.getAPI(1);
const repository = api.getRepository(Uri.parse(uri));
if (!repository) {
return null;
}
const state = repository.state;
const ref = (ref: Ref | undefined) => (ref && { ...ref, type: getRefType(ref.type) });
const change = (change: Change) => ({
uri: change.uri.toString(),
originalUri: change.originalUri.toString(),
renameUri: change.renameUri?.toString(),
status: getStatus(change.status)
});
return {
HEAD: ref(state.HEAD),
refs: state.refs.map(ref),
remotes: state.remotes,
submodules: state.submodules,
rebaseCommit: state.rebaseCommit,
mergeChanges: state.mergeChanges.map(change),
indexChanges: state.indexChanges.map(change),
workingTreeChanges: state.workingTreeChanges.map(change)
};
}));
disposables.push(commands.registerCommand('git.api.getRemoteSources', (opts?: PickRemoteSourceOptions) => {
if (!extension.model) {
return;
}
return pickRemoteSource(extension.model, opts);
}));
return Disposable.from(...disposables);
}

View File

@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. 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';
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> = this._onDidChangeEnablement.event;
private _model: Model | undefined = undefined;
set model(model: Model | undefined) {
this._model = model;
const enabled = !!model;
if (this.enabled === enabled) {
return;
}
this.enabled = enabled;
this._onDidChangeEnablement.fire(this.enabled);
}
get model(): Model | undefined {
return this._model;
}
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);
}
}

View File

@ -0,0 +1,304 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri, Event, Disposable, ProviderResult } from 'vscode';
export { ProviderResult } 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[];
readonly authorDate?: Date;
readonly authorName?: string;
readonly authorEmail?: string;
readonly commitDate?: Date;
}
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,
INTENT_TO_ADD,
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>;
}
/**
* Log options.
*/
export interface LogOptions {
/** Max number of log entries to retrieve. If not specified, the default is 32. */
readonly maxEntries?: number;
readonly path?: string;
}
export interface CommitOptions {
all?: boolean | 'tracked';
amend?: boolean;
signoff?: boolean;
signCommit?: boolean;
empty?: boolean;
noVerify?: boolean;
}
export interface BranchQuery {
readonly remote?: boolean;
readonly pattern?: string;
readonly count?: number;
readonly contains?: string;
}
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>;
getGlobalConfig(key: string): Promise<string>;
getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>;
detectObjectType(object: string): Promise<{ mimetype: string, encoding?: string }>;
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(): Promise<Change[]>;
diffWithHEAD(path: string): Promise<string>;
diffWith(ref: string): Promise<Change[]>;
diffWith(ref: string, path: string): Promise<string>;
diffIndexWithHEAD(): Promise<Change[]>;
diffIndexWithHEAD(path: string): Promise<string>;
diffIndexWith(ref: string): Promise<Change[]>;
diffIndexWith(ref: string, path: string): Promise<string>;
diffBlobs(object1: string, object2: string): Promise<string>;
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
hashObject(data: string): Promise<string>;
createBranch(name: string, checkout: boolean, ref?: string): Promise<void>;
deleteBranch(name: string, force?: boolean): Promise<void>;
getBranch(name: string): Promise<Branch>;
getBranches(query: BranchQuery): Promise<Ref[]>;
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>;
renameRemote(name: string, newName: string): Promise<void>;
fetch(remote?: string, ref?: string, depth?: number): Promise<void>;
pull(unshallow?: boolean): Promise<void>;
push(remoteName?: string, branchName?: string, setUpstream?: boolean): Promise<void>;
blame(path: string): Promise<string>;
log(options?: LogOptions): Promise<Commit[]>;
commit(message: string, opts?: CommitOptions): Promise<void>;
}
export interface RemoteSource {
readonly name: string;
readonly description?: string;
readonly url: string | string[];
}
export interface RemoteSourceProvider {
readonly name: string;
readonly icon?: string; // codicon name
readonly supportsQuery?: boolean;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
publishRepository?(repository: Repository): Promise<void>;
}
export interface Credentials {
readonly username: string;
readonly password: string;
}
export interface CredentialsProvider {
getCredentials(host: Uri): ProviderResult<Credentials>;
}
export interface PushErrorHandler {
handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean>;
}
export type APIState = 'uninitialized' | 'initialized';
export interface API {
readonly state: APIState;
readonly onDidChangeState: Event<APIState>;
readonly git: Git;
readonly repositories: Repository[];
readonly onDidOpenRepository: Event<Repository>;
readonly onDidCloseRepository: Event<Repository>;
toGitUri(uri: Uri, ref: string): Uri;
getRepository(uri: Uri): Repository | null;
init(root: Uri): Promise<Repository | null>;
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
registerPushErrorHandler(handler: PushErrorHandler): Disposable;
}
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',
PermissionDenied = 'PermissionDenied',
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',
PatchDoesNotApply = 'PatchDoesNotApply',
NoPathFound = 'NoPathFound',
UnknownPath = 'UnknownPath',
}

View File

@ -0,0 +1,2 @@
#!/bin/sh
echo ''

View File

@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as nls from 'vscode-nls';
import { IPCClient } from './ipc/ipcClient';
const localize = nls.loadMessageBundle();
function fatal(err: any): void {
console.error(localize('missOrInvalid', "Missing or invalid credentials."));
console.error(err);
process.exit(1);
}
function main(argv: string[]): void {
if (argv.length !== 5) {
return fatal('Wrong number of arguments');
}
if (!process.env['VSCODE_GIT_ASKPASS_PIPE']) {
return fatal('Missing pipe');
}
if (process.env['VSCODE_GIT_COMMAND'] === 'fetch' && !!process.env['VSCODE_GIT_FETCH_SILENT']) {
return fatal('Skip silent fetch commands');
}
const output = process.env['VSCODE_GIT_ASKPASS_PIPE'] as string;
const request = argv[2];
const host = argv[4].substring(1, argv[4].length - 2);
const ipcClient = new IPCClient('askpass');
ipcClient.call({ request, host }).then(res => {
fs.writeFileSync(output, res + '\n');
setTimeout(() => process.exit(0), 0);
}).catch(err => fatal(err));
}
main(process.argv);

View File

@ -0,0 +1,5 @@
#!/bin/sh
VSCODE_GIT_ASKPASS_PIPE=`mktemp`
ELECTRON_RUN_AS_NODE="1" VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $*
cat $VSCODE_GIT_ASKPASS_PIPE
rm $VSCODE_GIT_ASKPASS_PIPE

View File

@ -0,0 +1,98 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { window, InputBoxOptions, Uri, OutputChannel, Disposable, workspace } from 'vscode';
import { IDisposable, EmptyDisposable, toDisposable } from './util';
import * as path from 'path';
import { IIPCHandler, IIPCServer, createIPCServer } from './ipc/ipcServer';
import { CredentialsProvider, Credentials } from './api/git';
export class Askpass implements IIPCHandler {
private disposable: IDisposable = EmptyDisposable;
private cache = new Map<string, Credentials>();
private credentialsProviders = new Set<CredentialsProvider>();
static async create(outputChannel: OutputChannel, context?: string): Promise<Askpass> {
try {
return new Askpass(await createIPCServer(context));
} catch (err) {
outputChannel.appendLine(`[error] Failed to create git askpass IPC: ${err}`);
return new Askpass();
}
}
private constructor(private ipc?: IIPCServer) {
if (ipc) {
this.disposable = ipc.registerHandler('askpass', this);
}
}
async handle({ request, host }: { request: string, host: string }): Promise<string> {
const config = workspace.getConfiguration('git', null);
const enabled = config.get<boolean>('enabled');
if (!enabled) {
return '';
}
const uri = Uri.parse(host);
const authority = uri.authority.replace(/^.*@/, '');
const password = /password/i.test(request);
const cached = this.cache.get(authority);
if (cached && password) {
this.cache.delete(authority);
return cached.password;
}
if (!password) {
for (const credentialsProvider of this.credentialsProviders) {
try {
const credentials = await credentialsProvider.getCredentials(uri);
if (credentials) {
this.cache.set(authority, credentials);
setTimeout(() => this.cache.delete(authority), 60_000);
return credentials.username;
}
} catch { }
}
}
const options: InputBoxOptions = {
password,
placeHolder: request,
prompt: `Git: ${host}`,
ignoreFocusOut: true
};
return await window.showInputBox(options) || '';
}
getEnv(): { [key: string]: string; } {
if (!this.ipc) {
return {
GIT_ASKPASS: path.join(__dirname, 'askpass-empty.sh')
};
}
return {
...this.ipc.getEnv(),
GIT_ASKPASS: path.join(__dirname, 'askpass.sh'),
VSCODE_GIT_ASKPASS_NODE: process.execPath,
VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js')
};
}
registerCredentialsProvider(provider: CredentialsProvider): Disposable {
this.credentialsProviders.add(provider);
return toDisposable(() => this.credentialsProviders.delete(provider));
}
dispose(): void {
this.disposable.dispose();
}
}

View File

@ -0,0 +1,125 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workspace, Disposable, EventEmitter, Memento, window, MessageItem, ConfigurationTarget, Uri } from 'vscode';
import { Repository, Operation } from './repository';
import { eventToPromise, filterEvent, onceEvent } from './util';
import * as nls from 'vscode-nls';
import { GitErrorCodes } from './api/git';
const localize = nls.loadMessageBundle();
function isRemoteOperation(operation: Operation): boolean {
return operation === Operation.Pull || operation === Operation.Push || operation === Operation.Sync || operation === Operation.Fetch;
}
export class AutoFetcher {
private static DidInformUser = 'autofetch.didInformUser';
private _onDidChange = new EventEmitter<boolean>();
private onDidChange = this._onDidChange.event;
private _enabled: boolean = false;
get enabled(): boolean { return this._enabled; }
set enabled(enabled: boolean) { this._enabled = enabled; this._onDidChange.fire(enabled); }
private disposables: Disposable[] = [];
constructor(private repository: Repository, private globalState: Memento) {
workspace.onDidChangeConfiguration(this.onConfiguration, this, this.disposables);
this.onConfiguration();
const onGoodRemoteOperation = filterEvent(repository.onDidRunOperation, ({ operation, error }) => !error && isRemoteOperation(operation));
const onFirstGoodRemoteOperation = onceEvent(onGoodRemoteOperation);
onFirstGoodRemoteOperation(this.onFirstGoodRemoteOperation, this, this.disposables);
}
private async onFirstGoodRemoteOperation(): Promise<void> {
const didInformUser = !this.globalState.get<boolean>(AutoFetcher.DidInformUser);
if (this.enabled && !didInformUser) {
this.globalState.update(AutoFetcher.DidInformUser, true);
}
const shouldInformUser = !this.enabled && didInformUser;
if (!shouldInformUser) {
return;
}
const yes: MessageItem = { title: localize('yes', "Yes") };
const no: MessageItem = { isCloseAffordance: true, title: localize('no', "No") };
const askLater: MessageItem = { title: localize('not now', "Ask Me Later") };
const result = await window.showInformationMessage(localize('suggest auto fetch', "Would you like Code to [periodically run 'git fetch']({0})?", 'https://go.microsoft.com/fwlink/?linkid=865294'), yes, no, askLater);
if (result === askLater) {
return;
}
if (result === yes) {
const gitConfig = workspace.getConfiguration('git', Uri.file(this.repository.root));
gitConfig.update('autofetch', true, ConfigurationTarget.Global);
}
this.globalState.update(AutoFetcher.DidInformUser, true);
}
private onConfiguration(): void {
const gitConfig = workspace.getConfiguration('git', Uri.file(this.repository.root));
if (gitConfig.get<boolean>('autofetch') === false) {
this.disable();
} else {
this.enable();
}
}
enable(): void {
if (this.enabled) {
return;
}
this.enabled = true;
this.run();
}
disable(): void {
this.enabled = false;
}
private async run(): Promise<void> {
while (this.enabled) {
await this.repository.whenIdleAndFocused();
if (!this.enabled) {
return;
}
try {
await this.repository.fetchDefault({ silent: true });
} catch (err) {
if (err.gitErrorCode === GitErrorCodes.AuthenticationFailed) {
this.disable();
}
}
if (!this.enabled) {
return;
}
const period = workspace.getConfiguration('git', Uri.file(this.repository.root)).get<number>('autofetchPeriod', 180) * 1000;
const timeout = new Promise(c => setTimeout(c, period));
const whenDisabled = eventToPromise(filterEvent(this.onDidChange, enabled => !enabled));
await Promise.race([timeout, whenDisabled]);
}
}
dispose(): void {
this.disable();
this.disposables.forEach(d => d.dispose());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,207 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor } from 'vscode';
import * as path from 'path';
import { Repository, GitResourceGroup } from './repository';
import { Model } from './model';
import { debounce } from './decorators';
import { filterEvent, dispose, anyEvent, fireEvent, PromiseSource } from './util';
import { GitErrorCodes, Status } from './api/git';
class GitIgnoreDecorationProvider implements FileDecorationProvider {
private static Decoration: FileDecoration = { color: new ThemeColor('gitDecoration.ignoredResourceForeground') };
readonly onDidChange: Event<Uri[]>;
private queue = new Map<string, { repository: Repository; queue: Map<string, PromiseSource<FileDecoration | undefined>>; }>();
private disposables: Disposable[] = [];
constructor(private model: Model) {
this.onDidChange = fireEvent(anyEvent<any>(
filterEvent(workspace.onDidSaveTextDocument, e => /\.gitignore$|\.git\/info\/exclude$/.test(e.uri.path)),
model.onDidOpenRepository,
model.onDidCloseRepository
));
this.disposables.push(window.registerDecorationProvider(this));
}
async provideFileDecoration(uri: Uri): Promise<FileDecoration | undefined> {
const repository = this.model.getRepository(uri);
if (!repository) {
return;
}
let queueItem = this.queue.get(repository.root);
if (!queueItem) {
queueItem = { repository, queue: new Map<string, PromiseSource<FileDecoration | undefined>>() };
this.queue.set(repository.root, queueItem);
}
let promiseSource = queueItem.queue.get(uri.fsPath);
if (!promiseSource) {
promiseSource = new PromiseSource();
queueItem!.queue.set(uri.fsPath, promiseSource);
this.checkIgnoreSoon();
}
return await promiseSource.promise;
}
@debounce(500)
private checkIgnoreSoon(): void {
const queue = new Map(this.queue.entries());
this.queue.clear();
for (const [, item] of queue) {
const paths = [...item.queue.keys()];
item.repository.checkIgnore(paths).then(ignoreSet => {
for (const [path, promiseSource] of item.queue.entries()) {
promiseSource.resolve(ignoreSet.has(path) ? GitIgnoreDecorationProvider.Decoration : undefined);
}
}, err => {
if (err.gitErrorCode !== GitErrorCodes.IsInSubmodule) {
console.error(err);
}
for (const [, promiseSource] of item.queue.entries()) {
promiseSource.reject(err);
}
});
}
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
this.queue.clear();
}
}
class GitDecorationProvider implements FileDecorationProvider {
private static SubmoduleDecorationData: FileDecoration = {
tooltip: 'Submodule',
badge: 'S',
color: new ThemeColor('gitDecoration.submoduleResourceForeground')
};
private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
readonly onDidChange: Event<Uri[]> = this._onDidChangeDecorations.event;
private disposables: Disposable[] = [];
private decorations = new Map<string, FileDecoration>();
constructor(private repository: Repository) {
this.disposables.push(
window.registerDecorationProvider(this),
repository.onDidRunGitStatus(this.onDidRunGitStatus, this)
);
}
private onDidRunGitStatus(): void {
let newDecorations = new Map<string, FileDecoration>();
this.collectSubmoduleDecorationData(newDecorations);
this.collectDecorationData(this.repository.indexGroup, newDecorations);
this.collectDecorationData(this.repository.untrackedGroup, newDecorations);
this.collectDecorationData(this.repository.workingTreeGroup, newDecorations);
this.collectDecorationData(this.repository.mergeGroup, newDecorations);
const uris = new Set([...this.decorations.keys()].concat([...newDecorations.keys()]));
this.decorations = newDecorations;
this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true)));
}
private collectDecorationData(group: GitResourceGroup, bucket: Map<string, FileDecoration>): void {
for (const r of group.resourceStates) {
const decoration = r.resourceDecoration;
if (decoration) {
// not deleted and has a decoration
bucket.set(r.original.toString(), decoration);
if (r.type === Status.INDEX_RENAMED) {
bucket.set(r.resourceUri.toString(), decoration);
}
}
}
}
private collectSubmoduleDecorationData(bucket: Map<string, FileDecoration>): void {
for (const submodule of this.repository.submodules) {
bucket.set(Uri.file(path.join(this.repository.root, submodule.path)).toString(), GitDecorationProvider.SubmoduleDecorationData);
}
}
provideFileDecoration(uri: Uri): FileDecoration | undefined {
return this.decorations.get(uri.toString());
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
}
export class GitDecorations {
private disposables: Disposable[] = [];
private modelDisposables: Disposable[] = [];
private providers = new Map<Repository, Disposable>();
constructor(private model: Model) {
this.disposables.push(new GitIgnoreDecorationProvider(model));
const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.decorations.enabled'));
onEnablementChange(this.update, this, this.disposables);
this.update();
}
private update(): void {
const enabled = workspace.getConfiguration('git').get('decorations.enabled');
if (enabled) {
this.enable();
} else {
this.disable();
}
}
private enable(): void {
this.model.onDidOpenRepository(this.onDidOpenRepository, this, this.modelDisposables);
this.model.onDidCloseRepository(this.onDidCloseRepository, this, this.modelDisposables);
this.model.repositories.forEach(this.onDidOpenRepository, this);
}
private disable(): void {
this.modelDisposables = dispose(this.modelDisposables);
this.providers.forEach(value => value.dispose());
this.providers.clear();
}
private onDidOpenRepository(repository: Repository): void {
const provider = new GitDecorationProvider(repository);
this.providers.set(repository, provider);
}
private onDidCloseRepository(repository: Repository): void {
const provider = this.providers.get(repository);
if (provider) {
provider.dispose();
this.providers.delete(repository);
}
}
dispose(): void {
this.disable();
this.disposables = dispose(this.disposables);
}
}

View File

@ -0,0 +1,101 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { done } from './util';
function decorate(decorator: (fn: Function, key: string) => Function): Function {
return (_target: any, key: string, descriptor: any) => {
let fnKey: string | null = null;
let fn: Function | null = null;
if (typeof descriptor.value === 'function') {
fnKey = 'value';
fn = descriptor.value;
} else if (typeof descriptor.get === 'function') {
fnKey = 'get';
fn = descriptor.get;
}
if (!fn || !fnKey) {
throw new Error('not supported');
}
descriptor[fnKey] = decorator(fn, key);
};
}
function _memoize(fn: Function, key: string): Function {
const memoizeKey = `$memoize$${key}`;
return function (this: any, ...args: any[]) {
if (!this.hasOwnProperty(memoizeKey)) {
Object.defineProperty(this, memoizeKey, {
configurable: false,
enumerable: false,
writable: false,
value: fn.apply(this, args)
});
}
return this[memoizeKey];
};
}
export const memoize = decorate(_memoize);
function _throttle<T>(fn: Function, key: string): Function {
const currentKey = `$throttle$current$${key}`;
const nextKey = `$throttle$next$${key}`;
const trigger = function (this: any, ...args: any[]) {
if (this[nextKey]) {
return this[nextKey];
}
if (this[currentKey]) {
this[nextKey] = done(this[currentKey]).then(() => {
this[nextKey] = undefined;
return trigger.apply(this, args);
});
return this[nextKey];
}
this[currentKey] = fn.apply(this, args) as Promise<T>;
const clear = () => this[currentKey] = undefined;
done(this[currentKey]).then(clear, clear);
return this[currentKey];
};
return trigger;
}
export const throttle = decorate(_throttle);
function _sequentialize(fn: Function, key: string): Function {
const currentKey = `__$sequence$${key}`;
return function (this: any, ...args: any[]) {
const currentPromise = this[currentKey] as Promise<any> || Promise.resolve(null);
const run = async () => await fn.apply(this, args);
this[currentKey] = currentPromise.then(run, run);
return this[currentKey];
};
}
export const sequentialize = decorate(_sequentialize);
export function debounce(delay: number): Function {
return decorate((fn, key) => {
const timerKey = `$debounce$${key}`;
return function (this: any, ...args: any[]) {
clearTimeout(this[timerKey]);
this[timerKey] = setTimeout(() => fn.apply(this, args), delay);
};
});
}

View File

@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { workspace, Uri } from 'vscode';
import { getExtensionContext } from './main';
import { TextDecoder } from 'util';
const emojiRegex = /:([-+_a-z0-9]+):/g;
let emojiMap: Record<string, string> | undefined;
let emojiMapPromise: Promise<void> | undefined;
export async function ensureEmojis() {
if (emojiMap === undefined) {
if (emojiMapPromise === undefined) {
emojiMapPromise = loadEmojiMap();
}
await emojiMapPromise;
}
}
async function loadEmojiMap() {
const context = getExtensionContext();
const uri = (Uri as any).joinPath(context.extensionUri, 'resources', 'emojis.json');
emojiMap = JSON.parse(new TextDecoder('utf8').decode(await workspace.fs.readFile(uri)));
}
export function emojify(message: string) {
if (emojiMap === undefined) {
return message;
}
return message.replace(emojiRegex, (s, code) => {
return emojiMap?.[code] || s;
});
}

View File

@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as jschardet from 'jschardet';
function detectEncodingByBOM(buffer: Buffer): string | null {
if (!buffer || buffer.length < 2) {
return null;
}
const b0 = buffer.readUInt8(0);
const b1 = buffer.readUInt8(1);
// UTF-16 BE
if (b0 === 0xFE && b1 === 0xFF) {
return 'utf16be';
}
// UTF-16 LE
if (b0 === 0xFF && b1 === 0xFE) {
return 'utf16le';
}
if (buffer.length < 3) {
return null;
}
const b2 = buffer.readUInt8(2);
// UTF-8
if (b0 === 0xEF && b1 === 0xBB && b2 === 0xBF) {
return 'utf8';
}
return null;
}
const IGNORE_ENCODINGS = [
'ascii',
'utf-8',
'utf-16',
'utf-32'
];
const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = {
'ibm866': 'cp866',
'big5': 'cp950'
};
export function detectEncoding(buffer: Buffer): string | null {
let result = detectEncodingByBOM(buffer);
if (result) {
return result;
}
const detected = jschardet.detect(buffer);
if (!detected || !detected.encoding) {
return null;
}
const encoding = detected.encoding;
// Ignore encodings that cannot guess correctly
// (http://chardet.readthedocs.io/en/latest/supported-encodings.html)
if (0 <= IGNORE_ENCODINGS.indexOf(encoding.toLowerCase())) {
return null;
}
const normalizedEncodingName = encoding.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
const mapped = JSCHARDET_TO_ICONV_ENCODINGS[normalizedEncodingName];
return mapped || normalizedEncodingName;
}

View File

@ -0,0 +1,216 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workspace, Uri, Disposable, Event, EventEmitter, window, FileSystemProvider, FileChangeEvent, FileStat, FileType, FileChangeType, FileSystemError } from 'vscode';
import { debounce, throttle } from './decorators';
import { fromGitUri, toGitUri } from './uri';
import { Model, ModelChangeEvent, OriginalResourceChangeEvent } from './model';
import { filterEvent, eventToPromise, isDescendant, pathEquals, EmptyDisposable } from './util';
import { Repository } from './repository';
interface CacheRow {
uri: Uri;
timestamp: number;
}
const THREE_MINUTES = 1000 * 60 * 3;
const FIVE_MINUTES = 1000 * 60 * 5;
function sanitizeRef(ref: string, path: string, repository: Repository): string {
if (ref === '~') {
const fileUri = Uri.file(path);
const uriString = fileUri.toString();
const [indexStatus] = repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString);
return indexStatus ? '' : 'HEAD';
}
if (/^~\d$/.test(ref)) {
return `:${ref[1]}`;
}
return ref;
}
export class GitFileSystemProvider implements FileSystemProvider {
private _onDidChangeFile = new EventEmitter<FileChangeEvent[]>();
readonly onDidChangeFile: Event<FileChangeEvent[]> = this._onDidChangeFile.event;
private changedRepositoryRoots = new Set<string>();
private cache = new Map<string, CacheRow>();
private mtime = new Date().getTime();
private disposables: Disposable[] = [];
constructor(private model: Model) {
this.disposables.push(
model.onDidChangeRepository(this.onDidChangeRepository, this),
model.onDidChangeOriginalResource(this.onDidChangeOriginalResource, this),
workspace.registerFileSystemProvider('git', this, { isReadonly: true, isCaseSensitive: true }),
workspace.registerResourceLabelFormatter({
scheme: 'git',
formatting: {
label: '${path} (git)',
separator: '/'
}
})
);
setInterval(() => this.cleanup(), FIVE_MINUTES);
}
private onDidChangeRepository({ repository }: ModelChangeEvent): void {
this.changedRepositoryRoots.add(repository.root);
this.eventuallyFireChangeEvents();
}
private onDidChangeOriginalResource({ uri }: OriginalResourceChangeEvent): void {
if (uri.scheme !== 'file') {
return;
}
const gitUri = toGitUri(uri, '', { replaceFileExtension: true });
this.mtime = new Date().getTime();
this._onDidChangeFile.fire([{ type: FileChangeType.Changed, uri: gitUri }]);
}
@debounce(1100)
private eventuallyFireChangeEvents(): void {
this.fireChangeEvents();
}
@throttle
private async fireChangeEvents(): Promise<void> {
if (!window.state.focused) {
const onDidFocusWindow = filterEvent(window.onDidChangeWindowState, e => e.focused);
await eventToPromise(onDidFocusWindow);
}
const events: FileChangeEvent[] = [];
for (const { uri } of this.cache.values()) {
const fsPath = uri.fsPath;
for (const root of this.changedRepositoryRoots) {
if (isDescendant(root, fsPath)) {
events.push({ type: FileChangeType.Changed, uri });
break;
}
}
}
if (events.length > 0) {
this.mtime = new Date().getTime();
this._onDidChangeFile.fire(events);
}
this.changedRepositoryRoots.clear();
}
private cleanup(): void {
const now = new Date().getTime();
const cache = new Map<string, CacheRow>();
for (const row of this.cache.values()) {
const { path } = fromGitUri(row.uri);
const isOpen = workspace.textDocuments
.filter(d => d.uri.scheme === 'file')
.some(d => pathEquals(d.uri.fsPath, path));
if (isOpen || now - row.timestamp < THREE_MINUTES) {
cache.set(row.uri.toString(), row);
} else {
// TODO: should fire delete events?
}
}
this.cache = cache;
}
watch(): Disposable {
return EmptyDisposable;
}
async stat(uri: Uri): Promise<FileStat> {
await this.model.isInitialized;
const { submoduleOf, path, ref } = fromGitUri(uri);
const repository = submoduleOf ? this.model.getRepository(submoduleOf) : this.model.getRepository(uri);
if (!repository) {
throw FileSystemError.FileNotFound();
}
let size = 0;
try {
const details = await repository.getObjectDetails(sanitizeRef(ref, path, repository), path);
size = details.size;
} catch {
// noop
}
return { type: FileType.File, size: size, mtime: this.mtime, ctime: 0 };
}
readDirectory(): Thenable<[string, FileType][]> {
throw new Error('Method not implemented.');
}
createDirectory(): void {
throw new Error('Method not implemented.');
}
async readFile(uri: Uri): Promise<Uint8Array> {
await this.model.isInitialized;
const { path, ref, submoduleOf } = fromGitUri(uri);
if (submoduleOf) {
const repository = this.model.getRepository(submoduleOf);
if (!repository) {
throw FileSystemError.FileNotFound();
}
const encoder = new TextEncoder();
if (ref === 'index') {
return encoder.encode(await repository.diffIndexWithHEAD(path));
} else {
return encoder.encode(await repository.diffWithHEAD(path));
}
}
const repository = this.model.getRepository(uri);
if (!repository) {
throw FileSystemError.FileNotFound();
}
const timestamp = new Date().getTime();
const cacheValue: CacheRow = { uri, timestamp };
this.cache.set(uri.toString(), cacheValue);
try {
return await repository.buffer(sanitizeRef(ref, path, repository), path);
} catch (err) {
return new Uint8Array(0);
}
}
writeFile(): void {
throw new Error('Method not implemented.');
}
delete(): void {
throw new Error('Method not implemented.');
}
rename(): void {
throw new Error('Method not implemented.');
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,45 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as http from 'http';
export class IPCClient {
private ipcHandlePath: string;
constructor(private handlerName: string) {
const ipcHandlePath = process.env['VSCODE_GIT_IPC_HANDLE'];
if (!ipcHandlePath) {
throw new Error('Missing VSCODE_GIT_IPC_HANDLE');
}
this.ipcHandlePath = ipcHandlePath;
}
call(request: any): Promise<any> {
const opts: http.RequestOptions = {
socketPath: this.ipcHandlePath,
path: `/${this.handlerName}`,
method: 'POST'
};
return new Promise((c, e) => {
const req = http.request(opts, res => {
if (res.statusCode !== 200) {
return e(new Error(`Bad status code: ${res.statusCode}`));
}
const chunks: Buffer[] = [];
res.on('data', d => chunks.push(d));
res.on('end', () => c(JSON.parse(Buffer.concat(chunks).toString('utf8'))));
});
req.on('error', err => e(err));
req.write(JSON.stringify(request));
req.end();
});
}
}

View File

@ -0,0 +1,121 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vscode';
import { toDisposable } from '../util';
import * as path from 'path';
import * as http from 'http';
import * as os from 'os';
import * as fs from 'fs';
import * as crypto from 'crypto';
function getIPCHandlePath(id: string): string {
if (process.platform === 'win32') {
return `\\\\.\\pipe\\vscode-git-${id}-sock`;
}
if (process.env['XDG_RUNTIME_DIR']) {
return path.join(process.env['XDG_RUNTIME_DIR'] as string, `vscode-git-${id}.sock`);
}
return path.join(os.tmpdir(), `vscode-git-${id}.sock`);
}
export interface IIPCHandler {
handle(request: any): Promise<any>;
}
export async function createIPCServer(context?: string): Promise<IIPCServer> {
const server = http.createServer();
const hash = crypto.createHash('sha1');
if (!context) {
const buffer = await new Promise<Buffer>((c, e) => crypto.randomBytes(20, (err, buf) => err ? e(err) : c(buf)));
hash.update(buffer);
} else {
hash.update(context);
}
const ipcHandlePath = getIPCHandlePath(hash.digest('hex').substr(0, 10));
if (process.platform !== 'win32') {
try {
await fs.promises.unlink(ipcHandlePath);
} catch {
// noop
}
}
return new Promise((c, e) => {
try {
server.on('error', err => e(err));
server.listen(ipcHandlePath);
c(new IPCServer(server, ipcHandlePath));
} catch (err) {
e(err);
}
});
}
export interface IIPCServer extends Disposable {
readonly ipcHandlePath: string | undefined;
getEnv(): { [key: string]: string; };
registerHandler(name: string, handler: IIPCHandler): Disposable;
}
class IPCServer implements IIPCServer, Disposable {
private handlers = new Map<string, IIPCHandler>();
get ipcHandlePath(): string { return this._ipcHandlePath; }
constructor(private server: http.Server, private _ipcHandlePath: string) {
this.server.on('request', this.onRequest.bind(this));
}
registerHandler(name: string, handler: IIPCHandler): Disposable {
this.handlers.set(`/${name}`, handler);
return toDisposable(() => this.handlers.delete(name));
}
private onRequest(req: http.IncomingMessage, res: http.ServerResponse): void {
if (!req.url) {
console.warn(`Request lacks url`);
return;
}
const handler = this.handlers.get(req.url);
if (!handler) {
console.warn(`IPC handler for ${req.url} not found`);
return;
}
const chunks: Buffer[] = [];
req.on('data', d => chunks.push(d));
req.on('end', () => {
const request = JSON.parse(Buffer.concat(chunks).toString('utf8'));
handler.handle(request).then(result => {
res.writeHead(200);
res.end(JSON.stringify(result));
}, () => {
res.writeHead(500);
res.end();
});
});
}
getEnv(): { [key: string]: string; } {
return { VSCODE_GIT_IPC_HANDLE: this.ipcHandlePath };
}
dispose(): void {
this.handlers.clear();
this.server.close();
if (this._ipcHandlePath && process.platform !== 'win32') {
fs.unlinkSync(this._ipcHandlePath);
}
}
}

View File

@ -0,0 +1,50 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, EventEmitter } from 'vscode';
/**
* The severity level of a log message
*/
export enum LogLevel {
Trace = 1,
Debug = 2,
Info = 3,
Warning = 4,
Error = 5,
Critical = 6,
Off = 7
}
let _logLevel: LogLevel = LogLevel.Info;
const _onDidChangeLogLevel = new EventEmitter<LogLevel>();
export const Log = {
/**
* Current logging level.
*/
get logLevel(): LogLevel {
return _logLevel;
},
/**
* Current logging level.
*/
set logLevel(logLevel: LogLevel) {
if (_logLevel === logLevel) {
return;
}
_logLevel = logLevel;
_onDidChangeLogLevel.fire(logLevel);
},
/**
* An [event](#Event) that fires when the log level has changed.
*/
get onDidChangeLogLevel(): Event<LogLevel> {
return _onDidChangeLogLevel.event;
}
};

View File

@ -0,0 +1,246 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ExtensionContext, workspace, window, Disposable, commands, Uri, OutputChannel, WorkspaceFolder } from 'vscode';
import { findGit, Git, IGit } from './git';
import { Model } from './model';
import { CommandCenter } from './commands';
import { GitFileSystemProvider } from './fileSystemProvider';
import { GitDecorations } from './decorationProvider';
import { Askpass } from './askpass';
import { toDisposable, filterEvent, eventToPromise } from './util';
import TelemetryReporter from 'vscode-extension-telemetry';
import { GitExtension } from './api/git';
import { GitProtocolHandler } from './protocolHandler';
import { GitExtensionImpl } from './api/extension';
import * as path from 'path';
import * as fs from 'fs';
import { GitTimelineProvider } from './timelineProvider';
import { registerAPICommands } from './api/api1';
import { TerminalEnvironmentManager } from './terminal';
const deactivateTasks: { (): Promise<any>; }[] = [];
export async function deactivate(): Promise<any> {
for (const task of deactivateTasks) {
await task();
}
}
async function createModel(context: ExtensionContext, outputChannel: OutputChannel, telemetryReporter: TelemetryReporter, disposables: Disposable[]): Promise<Model> {
const pathHint = workspace.getConfiguration('git').get<string | string[]>('path');
const info = await findGit(pathHint, path => outputChannel.appendLine(localize('looking', "Looking for git in: {0}", path)));
const askpass = await Askpass.create(outputChannel, context.storagePath);
disposables.push(askpass);
const env = askpass.getEnv();
const terminalEnvironmentManager = new TerminalEnvironmentManager(context, env);
disposables.push(terminalEnvironmentManager);
const git = new Git({ gitPath: info.path, version: info.version, env });
const model = new Model(git, askpass, context.globalState, outputChannel);
disposables.push(model);
const onRepository = () => commands.executeCommand('setContext', 'gitOpenRepositoryCount', `${model.repositories.length}`);
model.onDidOpenRepository(onRepository, null, disposables);
model.onDidCloseRepository(onRepository, null, disposables);
onRepository();
outputChannel.appendLine(localize('using git', "Using git {0} from {1}", info.version, info.path));
const onOutput = (str: string) => {
const lines = str.split(/\r?\n/mg);
while (/^\s*$/.test(lines[lines.length - 1])) {
lines.pop();
}
outputChannel.appendLine(lines.join('\n'));
};
git.onOutput.addListener('log', onOutput);
disposables.push(toDisposable(() => git.onOutput.removeListener('log', onOutput)));
disposables.push(
new CommandCenter(git, model, outputChannel, telemetryReporter),
new GitFileSystemProvider(model),
new GitDecorations(model),
new GitProtocolHandler(),
new GitTimelineProvider(model)
);
checkGitVersion(info);
return model;
}
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;
}
}
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<GitExtensionImpl> {
const disposables: Disposable[] = [];
context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose()));
const outputChannel = window.createOutputChannel('Git');
commands.registerCommand('git.showOutput', () => outputChannel.show());
disposables.push(outputChannel);
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());
const config = workspace.getConfiguration('git', null);
const enabled = config.get<boolean>('enabled');
if (!enabled) {
const onConfigChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git'));
const onEnabled = filterEvent(onConfigChange, () => workspace.getConfiguration('git', null).get<boolean>('enabled') === true);
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 GitExtensionImpl(model);
} catch (err) {
if (!/Git installation not found/.test(err.message || '')) {
throw err;
}
console.warn(err.message);
outputChannel.appendLine(err.message);
commands.executeCommand('setContext', 'git.missing', true);
warnAboutMissingGit();
return new GitExtensionImpl();
}
}
let _context: ExtensionContext;
export function getExtensionContext(): ExtensionContext {
return _context;
}
export async function activate(context: ExtensionContext): Promise<GitExtension> {
_context = context;
const result = await _activate(context);
context.subscriptions.push(registerAPICommands(result));
return result;
}
async function checkGitv1(info: IGit): Promise<void> {
const config = workspace.getConfiguration('git');
const shouldIgnore = config.get<boolean>('ignoreLegacyWarning') === true;
if (shouldIgnore) {
return;
}
if (!/^[01]/.test(info.version)) {
return;
}
const update = localize('updateGit', "Update Git");
const neverShowAgain = localize('neverShowAgain', "Don't Show Again");
const choice = await window.showWarningMessage(
localize('git20', "You seem to have git {0} installed. Code works best with git >= 2", info.version),
update,
neverShowAgain
);
if (choice === update) {
commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/'));
} else if (choice === neverShowAgain) {
await config.update('ignoreLegacyWarning', true, true);
}
}
async function checkGitWindows(info: IGit): Promise<void> {
if (!/^2\.(25|26)\./.test(info.version)) {
return;
}
const config = workspace.getConfiguration('git');
const shouldIgnore = config.get<boolean>('ignoreWindowsGit27Warning') === true;
if (shouldIgnore) {
return;
}
const update = localize('updateGit', "Update Git");
const neverShowAgain = localize('neverShowAgain', "Don't Show Again");
const choice = await window.showWarningMessage(
localize('git2526', "There are known issues with the installed Git {0}. Please update to Git >= 2.27 for the git features to work correctly.", info.version),
update,
neverShowAgain
);
if (choice === update) {
commands.executeCommand('vscode.open', Uri.parse('https://git-scm.com/'));
} else if (choice === neverShowAgain) {
await config.update('ignoreWindowsGit27Warning', true, true);
}
}
async function checkGitVersion(info: IGit): Promise<void> {
await checkGitv1(info);
if (process.platform === 'win32') {
await checkGitWindows(info);
}
}

View File

@ -0,0 +1,508 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, OutputChannel, commands } from 'vscode';
import { Repository, RepositoryState } from './repository';
import { memoize, sequentialize, debounce } from './decorators';
import { dispose, anyEvent, filterEvent, isDescendant, pathEquals, toDisposable, eventToPromise } from './util';
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 { APIState as State, RemoteSourceProvider, CredentialsProvider, PushErrorHandler } from './api/git';
import { Askpass } from './askpass';
import { IRemoteSourceProviderRegistry } from './remoteProvider';
import { IPushErrorHandlerRegistry } from './pushError';
const localize = nls.loadMessageBundle();
class RepositoryPick implements QuickPickItem {
@memoize get label(): string {
return path.basename(this.repository.root);
}
@memoize get description(): string {
return [this.repository.headLabel, this.repository.syncLabel]
.filter(l => !!l)
.join(' ');
}
constructor(public readonly repository: Repository, public readonly index: number) { }
}
export interface ModelChangeEvent {
repository: Repository;
uri: Uri;
}
export interface OriginalResourceChangeEvent {
repository: Repository;
uri: Uri;
}
interface OpenRepository extends Disposable {
repository: Repository;
}
export class Model implements IRemoteSourceProviderRegistry, IPushErrorHandlerRegistry {
private _onDidOpenRepository = new EventEmitter<Repository>();
readonly onDidOpenRepository: Event<Repository> = this._onDidOpenRepository.event;
private _onDidCloseRepository = new EventEmitter<Repository>();
readonly onDidCloseRepository: Event<Repository> = this._onDidCloseRepository.event;
private _onDidChangeRepository = new EventEmitter<ModelChangeEvent>();
readonly onDidChangeRepository: Event<ModelChangeEvent> = this._onDidChangeRepository.event;
private _onDidChangeOriginalResource = new EventEmitter<OriginalResourceChangeEvent>();
readonly onDidChangeOriginalResource: Event<OriginalResourceChangeEvent> = this._onDidChangeOriginalResource.event;
private openRepositories: OpenRepository[] = [];
get repositories(): Repository[] { return this.openRepositories.map(r => r.repository); }
private possibleGitRepositoryPaths = new Set<string>();
private _onDidChangeState = new EventEmitter<State>();
readonly onDidChangeState = this._onDidChangeState.event;
private _state: State = 'uninitialized';
get state(): State { return this._state; }
setState(state: State): void {
this._state = state;
this._onDidChangeState.fire(state);
commands.executeCommand('setContext', 'git.state', state);
}
@memoize
get isInitialized(): Promise<void> {
if (this._state === 'initialized') {
return Promise.resolve();
}
return eventToPromise(filterEvent(this.onDidChangeState, s => s === 'initialized')) as Promise<any>;
}
private remoteSourceProviders = new Set<RemoteSourceProvider>();
private _onDidAddRemoteSourceProvider = new EventEmitter<RemoteSourceProvider>();
readonly onDidAddRemoteSourceProvider = this._onDidAddRemoteSourceProvider.event;
private _onDidRemoveRemoteSourceProvider = new EventEmitter<RemoteSourceProvider>();
readonly onDidRemoveRemoteSourceProvider = this._onDidRemoveRemoteSourceProvider.event;
private pushErrorHandlers = new Set<PushErrorHandler>();
private disposables: Disposable[] = [];
constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private outputChannel: OutputChannel) {
workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables);
window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables);
const fsWatcher = workspace.createFileSystemWatcher('**');
this.disposables.push(fsWatcher);
const onWorkspaceChange = anyEvent(fsWatcher.onDidChange, fsWatcher.onDidCreate, fsWatcher.onDidDelete);
const onGitRepositoryChange = filterEvent(onWorkspaceChange, uri => /\/\.git/.test(uri.path));
const onPossibleGitRepositoryChange = filterEvent(onGitRepositoryChange, uri => !this.getRepository(uri));
onPossibleGitRepositoryChange(this.onPossibleGitRepositoryChange, this, this.disposables);
this.setState('uninitialized');
this.doInitialScan().finally(() => this.setState('initialized'));
}
private async doInitialScan(): Promise<void> {
await Promise.all([
this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] }),
this.onDidChangeVisibleTextEditors(window.visibleTextEditors),
this.scanWorkspaceFolders()
]);
}
/**
* Scans the first level of each workspace folder, looking
* for git repositories.
*/
private async scanWorkspaceFolders(): Promise<void> {
const config = workspace.getConfiguration('git');
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'subFolders') {
return;
}
await Promise.all((workspace.workspaceFolders || []).map(async folder => {
const root = folder.uri.fsPath;
const children = await new Promise<string[]>((c, e) => fs.readdir(root, (err, r) => err ? e(err) : c(r)));
const promises = children
.filter(child => child !== '.git')
.map(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;
}
promises.push(this.openRepository(path.join(root, possibleRepositoryPath)));
}
await Promise.all(promises);
}));
}
private onPossibleGitRepositoryChange(uri: Uri): void {
const config = workspace.getConfiguration('git');
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
if (autoRepositoryDetection === false) {
return;
}
this.eventuallyScanPossibleGitRepository(uri.fsPath.replace(/\.git.*$/, ''));
}
private eventuallyScanPossibleGitRepository(path: string) {
this.possibleGitRepositoryPaths.add(path);
this.eventuallyScanPossibleGitRepositories();
}
@debounce(500)
private eventuallyScanPossibleGitRepositories(): void {
for (const path of this.possibleGitRepositoryPaths) {
this.openRepository(path);
}
this.possibleGitRepositoryPaths.clear();
}
private async onDidChangeWorkspaceFolders({ added, removed }: WorkspaceFoldersChangeEvent): Promise<void> {
const possibleRepositoryFolders = added
.filter(folder => !this.getOpenRepository(folder.uri));
const activeRepositoriesList = window.visibleTextEditors
.map(editor => this.getRepository(editor.document.uri))
.filter(repository => !!repository) as Repository[];
const activeRepositories = new Set<Repository>(activeRepositoriesList);
const openRepositoriesToDispose = removed
.map(folder => this.getOpenRepository(folder.uri))
.filter(r => !!r)
.filter(r => !activeRepositories.has(r!.repository))
.filter(r => !(workspace.workspaceFolders || []).some(f => isDescendant(f.uri.fsPath, r!.repository.root))) as OpenRepository[];
openRepositoriesToDispose.forEach(r => r.dispose());
await Promise.all(possibleRepositoryFolders.map(p => this.openRepository(p.uri.fsPath)));
}
private onDidChangeConfiguration(): void {
const possibleRepositoryFolders = (workspace.workspaceFolders || [])
.filter(folder => workspace.getConfiguration('git', folder.uri).get<boolean>('enabled') === true)
.filter(folder => !this.getOpenRepository(folder.uri));
const openRepositoriesToDispose = this.openRepositories
.map(repository => ({ repository, root: Uri.file(repository.repository.root) }))
.filter(({ root }) => workspace.getConfiguration('git', root).get<boolean>('enabled') !== true)
.map(({ repository }) => repository);
possibleRepositoryFolders.forEach(p => this.openRepository(p.uri.fsPath));
openRepositoriesToDispose.forEach(r => r.dispose());
}
private async onDidChangeVisibleTextEditors(editors: readonly TextEditor[]): Promise<void> {
const config = workspace.getConfiguration('git');
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors') {
return;
}
await Promise.all(editors.map(async editor => {
const uri = editor.document.uri;
if (uri.scheme !== 'file') {
return;
}
const repository = this.getRepository(uri);
if (repository) {
return;
}
await this.openRepository(path.dirname(uri.fsPath));
}));
}
@sequentialize
async openRepository(path: string): Promise<void> {
if (this.getRepository(path)) {
return;
}
const config = workspace.getConfiguration('git', Uri.file(path));
const enabled = config.get<boolean>('enabled') === true;
if (!enabled) {
return;
}
try {
const rawRoot = await this.git.getRepositoryRoot(path);
// This can happen whenever `path` has the wrong case sensitivity in
// case insensitive file systems
// https://github.com/microsoft/vscode/issues/33498
const repositoryRoot = Uri.file(rawRoot).fsPath;
if (this.getRepository(repositoryRoot)) {
return;
}
if (this.shouldRepositoryBeIgnored(rawRoot)) {
return;
}
const dotGit = await this.git.getRepositoryDotGit(repositoryRoot);
const repository = new Repository(this.git.open(repositoryRoot, dotGit), this, this, this.globalState, this.outputChannel);
this.open(repository);
await repository.status();
} catch (err) {
// noop
}
}
private shouldRepositoryBeIgnored(repositoryRoot: string): boolean {
const config = workspace.getConfiguration('git');
const ignoredRepos = config.get<string[]>('ignoredRepositories') || [];
for (const ignoredRepo of ignoredRepos) {
if (path.isAbsolute(ignoredRepo)) {
if (pathEquals(ignoredRepo, repositoryRoot)) {
return true;
}
} else {
for (const folder of workspace.workspaceFolders || []) {
if (pathEquals(path.join(folder.uri.fsPath, ignoredRepo), repositoryRoot)) {
return true;
}
}
}
}
return false;
}
private open(repository: Repository): void {
this.outputChannel.appendLine(`Open repository: ${repository.root}`);
const onDidDisappearRepository = filterEvent(repository.onDidChangeState, state => state === RepositoryState.Disposed);
const disappearListener = onDidDisappearRepository(() => dispose());
const changeListener = repository.onDidChangeRepository(uri => this._onDidChangeRepository.fire({ repository, uri }));
const originalResourceChangeListener = repository.onDidChangeOriginalResource(uri => this._onDidChangeOriginalResource.fire({ repository, uri }));
const 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();
}
repository.submodules
.slice(0, submodulesLimit)
.map(r => path.join(repository.root, r.path))
.forEach(p => this.eventuallyScanPossibleGitRepository(p));
};
const statusListener = repository.onDidRunGitStatus(checkForSubmodules);
checkForSubmodules();
const dispose = () => {
disappearListener.dispose();
changeListener.dispose();
originalResourceChangeListener.dispose();
statusListener.dispose();
repository.dispose();
this.openRepositories = this.openRepositories.filter(e => e !== openRepository);
this._onDidCloseRepository.fire(repository);
};
const openRepository = { repository, dispose };
this.openRepositories.push(openRepository);
this._onDidOpenRepository.fire(repository);
}
close(repository: Repository): void {
const openRepository = this.getOpenRepository(repository);
if (!openRepository) {
return;
}
this.outputChannel.appendLine(`Close repository: ${repository.root}`);
openRepository.dispose();
}
async pickRepository(): Promise<Repository | undefined> {
if (this.openRepositories.length === 0) {
throw new Error(localize('no repositories', "There are no available repositories"));
}
const picks = this.openRepositories.map((e, index) => new RepositoryPick(e.repository, index));
const active = window.activeTextEditor;
const repository = active && this.getRepository(active.document.fileName);
const index = picks.findIndex(pick => pick.repository === repository);
// Move repository pick containing the active text editor to appear first
if (index > -1) {
picks.unshift(...picks.splice(index, 1));
}
const placeHolder = localize('pick repo', "Choose a repository");
const pick = await window.showQuickPick(picks, { placeHolder });
return pick && pick.repository;
}
getRepository(sourceControl: SourceControl): Repository | undefined;
getRepository(resourceGroup: SourceControlResourceGroup): Repository | undefined;
getRepository(path: string): Repository | undefined;
getRepository(resource: Uri): Repository | undefined;
getRepository(hint: any): Repository | undefined {
const liveRepository = this.getOpenRepository(hint);
return liveRepository && liveRepository.repository;
}
private getOpenRepository(repository: Repository): OpenRepository | undefined;
private getOpenRepository(sourceControl: SourceControl): OpenRepository | undefined;
private getOpenRepository(resourceGroup: SourceControlResourceGroup): OpenRepository | undefined;
private getOpenRepository(path: string): OpenRepository | undefined;
private getOpenRepository(resource: Uri): OpenRepository | undefined;
private getOpenRepository(hint: any): OpenRepository | undefined {
if (!hint) {
return undefined;
}
if (hint instanceof Repository) {
return this.openRepositories.filter(r => r.repository === hint)[0];
}
if (typeof hint === 'string') {
hint = Uri.file(hint);
}
if (hint instanceof Uri) {
let resourcePath: string;
if (hint.scheme === 'git') {
resourcePath = fromGitUri(hint).path;
} else {
resourcePath = hint.fsPath;
}
outer:
for (const liveRepository of this.openRepositories.sort((a, b) => b.repository.root.length - a.repository.root.length)) {
if (!isDescendant(liveRepository.repository.root, resourcePath)) {
continue;
}
for (const submodule of liveRepository.repository.submodules) {
const submoduleRoot = path.join(liveRepository.repository.root, submodule.path);
if (isDescendant(submoduleRoot, resourcePath)) {
continue outer;
}
}
return liveRepository;
}
return undefined;
}
for (const liveRepository of this.openRepositories) {
const repository = liveRepository.repository;
if (hint === repository.sourceControl) {
return liveRepository;
}
if (hint === repository.mergeGroup || hint === repository.indexGroup || hint === repository.workingTreeGroup) {
return liveRepository;
}
}
return undefined;
}
getRepositoryForSubmodule(submoduleUri: Uri): Repository | undefined {
for (const repository of this.repositories) {
for (const submodule of repository.submodules) {
const submodulePath = path.join(repository.root, submodule.path);
if (submodulePath === submoduleUri.fsPath) {
return repository;
}
}
}
return undefined;
}
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable {
this.remoteSourceProviders.add(provider);
this._onDidAddRemoteSourceProvider.fire(provider);
return toDisposable(() => {
this.remoteSourceProviders.delete(provider);
this._onDidRemoveRemoteSourceProvider.fire(provider);
});
}
registerCredentialsProvider(provider: CredentialsProvider): Disposable {
return this.askpass.registerCredentialsProvider(provider);
}
getRemoteProviders(): RemoteSourceProvider[] {
return [...this.remoteSourceProviders.values()];
}
registerPushErrorHandler(handler: PushErrorHandler): Disposable {
this.pushErrorHandlers.add(handler);
return toDisposable(() => this.pushErrorHandlers.delete(handler));
}
getPushErrorHandlers(): PushErrorHandler[] {
return [...this.pushErrorHandlers];
}
dispose(): void {
const openRepositories = [...this.openRepositories];
openRepositories.forEach(r => r.dispose());
this.openRepositories = [];
this.possibleGitRepositoryPaths.clear();
this.disposables = dispose(this.disposables);
}
}

View File

@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { UriHandler, Uri, window, Disposable, commands } from 'vscode';
import { dispose } from './util';
import * as querystring from 'querystring';
export class GitProtocolHandler implements UriHandler {
private disposables: Disposable[] = [];
constructor() {
this.disposables.push(window.registerUriHandler(this));
}
handleUri(uri: Uri): void {
switch (uri.path) {
case '/clone': this.clone(uri);
}
}
private clone(uri: Uri): void {
const data = querystring.parse(uri.query);
if (!data.url) {
console.warn('Failed to open URI:', uri);
}
commands.executeCommand('git.clone', data.url);
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
}

View File

@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vscode';
import { PushErrorHandler } from './api/git';
export interface IPushErrorHandlerRegistry {
registerPushErrorHandler(provider: PushErrorHandler): Disposable;
getPushErrorHandlers(): PushErrorHandler[];
}

View File

@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event } from 'vscode';
import { RemoteSourceProvider } from './api/git';
export interface IRemoteSourceProviderRegistry {
readonly onDidAddRemoteSourceProvider: Event<RemoteSourceProvider>;
readonly onDidRemoveRemoteSourceProvider: Event<RemoteSourceProvider>;
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
getRemoteProviders(): RemoteSourceProvider[];
}

View File

@ -0,0 +1,133 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { QuickPickItem, window, QuickPick } from 'vscode';
import * as nls from 'vscode-nls';
import { RemoteSourceProvider, RemoteSource } from './api/git';
import { Model } from './model';
import { throttle, debounce } from './decorators';
const localize = nls.loadMessageBundle();
async function getQuickPickResult<T extends QuickPickItem>(quickpick: QuickPick<T>): Promise<T | undefined> {
const result = await new Promise<T | undefined>(c => {
quickpick.onDidAccept(() => c(quickpick.selectedItems[0]));
quickpick.onDidHide(() => c(undefined));
quickpick.show();
});
quickpick.hide();
return result;
}
class RemoteSourceProviderQuickPick {
private quickpick: QuickPick<QuickPickItem & { remoteSource?: RemoteSource }>;
constructor(private provider: RemoteSourceProvider) {
this.quickpick = window.createQuickPick();
this.quickpick.ignoreFocusOut = true;
if (provider.supportsQuery) {
this.quickpick.placeholder = localize('type to search', "Repository name (type to search)");
this.quickpick.onDidChangeValue(this.onDidChangeValue, this);
} else {
this.quickpick.placeholder = localize('type to filter', "Repository name");
}
}
@debounce(300)
private onDidChangeValue(): void {
this.query();
}
@throttle
private async query(): Promise<void> {
this.quickpick.busy = true;
try {
const remoteSources = await this.provider.getRemoteSources(this.quickpick.value) || [];
if (remoteSources.length === 0) {
this.quickpick.items = [{
label: localize('none found', "No remote repositories found."),
alwaysShow: true
}];
} else {
this.quickpick.items = remoteSources.map(remoteSource => ({
label: remoteSource.name,
description: remoteSource.description || (typeof remoteSource.url === 'string' ? remoteSource.url : remoteSource.url[0]),
remoteSource
}));
}
} catch (err) {
this.quickpick.items = [{ label: localize('error', "$(error) Error: {0}", err.message), alwaysShow: true }];
console.error(err);
} finally {
this.quickpick.busy = false;
}
}
async pick(): Promise<RemoteSource | undefined> {
this.query();
const result = await getQuickPickResult(this.quickpick);
return result?.remoteSource;
}
}
export interface PickRemoteSourceOptions {
readonly providerLabel?: (provider: RemoteSourceProvider) => string;
readonly urlLabel?: string;
}
export async function pickRemoteSource(model: Model, options: PickRemoteSourceOptions = {}): Promise<string | undefined> {
const quickpick = window.createQuickPick<(QuickPickItem & { provider?: RemoteSourceProvider, url?: string })>();
quickpick.ignoreFocusOut = true;
const providers = model.getRemoteProviders()
.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + (options.providerLabel ? options.providerLabel(provider) : provider.name), alwaysShow: true, provider }));
quickpick.placeholder = providers.length === 0
? localize('provide url', "Provide repository URL")
: localize('provide url or pick', "Provide repository URL or pick a repository source.");
const updatePicks = (value?: string) => {
if (value) {
quickpick.items = [{
label: options.urlLabel ?? localize('url', "URL"),
description: value,
alwaysShow: true,
url: value
},
...providers];
} else {
quickpick.items = providers;
}
};
quickpick.onDidChangeValue(updatePicks);
updatePicks();
const result = await getQuickPickResult(quickpick);
if (result) {
if (result.url) {
return result.url;
} else if (result.provider) {
const quickpick = new RemoteSourceProviderQuickPick(result.provider);
const remote = await quickpick.pick();
if (remote) {
if (typeof remote.url === 'string') {
return remote.url;
} else if (remote.url.length > 0) {
return await window.showQuickPick(remote.url, { ignoreFocusOut: true, placeHolder: localize('pick url', "Choose a URL to clone from.") });
}
}
}
}
return undefined;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,128 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TextDocument, Range, LineChange, Selection } from 'vscode';
export function applyLineChanges(original: TextDocument, modified: TextDocument, diffs: LineChange[]): string {
const result: string[] = [];
let currentLine = 0;
for (let diff of diffs) {
const isInsertion = diff.originalEndLineNumber === 0;
const isDeletion = diff.modifiedEndLineNumber === 0;
let endLine = isInsertion ? diff.originalStartLineNumber : diff.originalStartLineNumber - 1;
let endCharacter = 0;
// if this is a deletion at the very end of the document,then we need to account
// for a newline at the end of the last line which may have been deleted
// https://github.com/microsoft/vscode/issues/59670
if (isDeletion && diff.originalEndLineNumber === original.lineCount) {
endLine -= 1;
endCharacter = original.lineAt(endLine).range.end.character;
}
result.push(original.getText(new Range(currentLine, 0, endLine, endCharacter)));
if (!isDeletion) {
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 -= 1;
fromCharacter = modified.lineAt(fromLine).range.end.character;
}
result.push(modified.getText(new Range(fromLine, fromCharacter, diff.modifiedEndLineNumber, 0)));
}
currentLine = isInsertion ? diff.originalStartLineNumber : diff.originalEndLineNumber;
}
result.push(original.getText(new Range(currentLine, 0, original.lineCount, 0)));
return result.join('');
}
export function toLineRanges(selections: Selection[], textDocument: TextDocument): Range[] {
const lineRanges = selections.map(s => {
const startLine = textDocument.lineAt(s.start.line);
const endLine = textDocument.lineAt(s.end.line);
return new Range(startLine.range.start, endLine.range.end);
});
lineRanges.sort((a, b) => a.start.line - b.start.line);
const result = lineRanges.reduce((result, l) => {
if (result.length === 0) {
result.push(l);
return result;
}
const [last, ...rest] = result;
const intersection = l.intersection(last);
if (intersection) {
return [intersection, ...rest];
}
if (l.start.line === last.end.line + 1) {
const merge = new Range(last.start, l.end);
return [merge, ...rest];
}
return [l, ...result];
}, [] as Range[]);
result.reverse();
return result;
}
export function getModifiedRange(textDocument: TextDocument, diff: LineChange): Range {
if (diff.modifiedEndLineNumber === 0) {
if (diff.modifiedStartLineNumber === 0) {
return new Range(textDocument.lineAt(diff.modifiedStartLineNumber).range.end, textDocument.lineAt(diff.modifiedStartLineNumber).range.start);
} else 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);
}
} else {
return new Range(textDocument.lineAt(diff.modifiedStartLineNumber - 1).range.start, textDocument.lineAt(diff.modifiedEndLineNumber - 1).range.end);
}
}
export function intersectDiffWithRange(textDocument: TextDocument, diff: LineChange, range: Range): LineChange | null {
const modifiedRange = getModifiedRange(textDocument, diff);
const intersection = range.intersection(modifiedRange);
if (!intersection) {
return null;
}
if (diff.modifiedEndLineNumber === 0) {
return diff;
} else {
return {
originalStartLineNumber: diff.originalStartLineNumber,
originalEndLineNumber: diff.originalEndLineNumber,
modifiedStartLineNumber: intersection.start.line + 1,
modifiedEndLineNumber: intersection.end.line + 1
};
}
}
export function invertLineChange(diff: LineChange): LineChange {
return {
modifiedStartLineNumber: diff.originalStartLineNumber,
modifiedEndLineNumber: diff.originalEndLineNumber,
originalStartLineNumber: diff.modifiedStartLineNumber,
originalEndLineNumber: diff.modifiedEndLineNumber
};
}

View File

@ -0,0 +1,207 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Command, EventEmitter, Event, workspace, Uri } from 'vscode';
import { Repository, Operation } from './repository';
import { anyEvent, dispose, filterEvent } from './util';
import * as nls from 'vscode-nls';
import { Branch, RemoteSourceProvider } from './api/git';
import { IRemoteSourceProviderRegistry } from './remoteProvider';
const localize = nls.loadMessageBundle();
class CheckoutStatusBar {
private _onDidChange = new EventEmitter<void>();
get onDidChange(): Event<void> { return this._onDidChange.event; }
private disposables: Disposable[] = [];
constructor(private repository: Repository) {
repository.onDidRunGitStatus(this._onDidChange.fire, this._onDidChange, this.disposables);
}
get command(): Command | undefined {
const rebasing = !!this.repository.rebaseCommit;
const title = `$(git-branch) ${this.repository.headLabel}${rebasing ? ` (${localize('rebasing', 'Rebasing')})` : ''}`;
return {
command: 'git.checkout',
tooltip: localize('checkout', "Checkout branch/tag..."),
title,
arguments: [this.repository.sourceControl]
};
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
}
interface SyncStatusBarState {
readonly enabled: boolean;
readonly isSyncRunning: boolean;
readonly hasRemotes: boolean;
readonly HEAD: Branch | undefined;
readonly remoteSourceProviders: RemoteSourceProvider[];
}
class SyncStatusBar {
private _onDidChange = new EventEmitter<void>();
get onDidChange(): Event<void> { return this._onDidChange.event; }
private disposables: Disposable[] = [];
private _state: SyncStatusBarState;
private get state() { return this._state; }
private set state(state: SyncStatusBarState) {
this._state = state;
this._onDidChange.fire();
}
constructor(private repository: Repository, private remoteSourceProviderRegistry: IRemoteSourceProviderRegistry) {
this._state = {
enabled: true,
isSyncRunning: false,
hasRemotes: false,
HEAD: undefined,
remoteSourceProviders: this.remoteSourceProviderRegistry.getRemoteProviders()
.filter(p => !!p.publishRepository)
};
repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables);
repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables);
anyEvent(remoteSourceProviderRegistry.onDidAddRemoteSourceProvider, remoteSourceProviderRegistry.onDidRemoveRemoteSourceProvider)
(this.onDidChangeRemoteSourceProviders, this, this.disposables);
const onEnablementChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.enableStatusBarSync'));
onEnablementChange(this.updateEnablement, this, this.disposables);
this.updateEnablement();
}
private updateEnablement(): void {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const enabled = config.get<boolean>('enableStatusBarSync', true);
this.state = { ... this.state, enabled };
}
private onDidChangeOperations(): void {
const isSyncRunning = this.repository.operations.isRunning(Operation.Sync) ||
this.repository.operations.isRunning(Operation.Push) ||
this.repository.operations.isRunning(Operation.Pull);
this.state = { ...this.state, isSyncRunning };
}
private onDidRunGitStatus(): void {
this.state = {
...this.state,
hasRemotes: this.repository.remotes.length > 0,
HEAD: this.repository.HEAD
};
}
private onDidChangeRemoteSourceProviders(): void {
this.state = {
...this.state,
remoteSourceProviders: this.remoteSourceProviderRegistry.getRemoteProviders()
.filter(p => !!p.publishRepository)
};
}
get command(): Command | undefined {
if (!this.state.enabled) {
return;
}
if (!this.state.hasRemotes) {
if (this.state.remoteSourceProviders.length === 0) {
return;
}
const tooltip = this.state.remoteSourceProviders.length === 1
? localize('publish to', "Publish to {0}", this.state.remoteSourceProviders[0].name)
: localize('publish to...', "Publish to...");
return {
command: 'git.publish',
title: `$(cloud-upload)`,
tooltip,
arguments: [this.repository.sourceControl]
};
}
const HEAD = this.state.HEAD;
let icon = '$(sync)';
let text = '';
let command = '';
let tooltip = '';
if (HEAD && HEAD.name && HEAD.commit) {
if (HEAD.upstream) {
if (HEAD.ahead || HEAD.behind) {
text += this.repository.syncLabel;
}
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const rebaseWhenSync = config.get<string>('rebaseWhenSync');
command = rebaseWhenSync ? 'git.syncRebase' : 'git.sync';
tooltip = this.repository.syncTooltip;
} else {
icon = '$(cloud-upload)';
command = 'git.publish';
tooltip = localize('publish changes', "Publish Changes");
}
} else {
command = '';
tooltip = '';
}
if (this.state.isSyncRunning) {
icon = '$(sync~spin)';
command = '';
tooltip = localize('syncing changes', "Synchronizing Changes...");
}
return {
command,
title: [icon, text].join(' ').trim(),
tooltip,
arguments: [this.repository.sourceControl]
};
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
}
export class StatusBarCommands {
readonly onDidChange: Event<void>;
private syncStatusBar: SyncStatusBar;
private checkoutStatusBar: CheckoutStatusBar;
private disposables: Disposable[] = [];
constructor(repository: Repository, remoteSourceProviderRegistry: IRemoteSourceProviderRegistry) {
this.syncStatusBar = new SyncStatusBar(repository, remoteSourceProviderRegistry);
this.checkoutStatusBar = new CheckoutStatusBar(repository);
this.onDidChange = anyEvent(this.syncStatusBar.onDidChange, this.checkoutStatusBar.onDidChange);
}
get commands(): Command[] {
return [this.checkoutStatusBar.command, this.syncStatusBar.command]
.filter((c): c is Command => !!c);
}
dispose(): void {
this.syncStatusBar.dispose();
this.checkoutStatusBar.dispose();
this.disposables = dispose(this.disposables);
}
}

View File

@ -0,0 +1,44 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ExtensionContext, workspace } from 'vscode';
import { filterEvent, IDisposable } from './util';
export class TerminalEnvironmentManager {
private readonly disposable: IDisposable;
private _enabled = false;
private set enabled(enabled: boolean) {
if (this._enabled === enabled) {
return;
}
this._enabled = enabled;
this.context.environmentVariableCollection.clear();
if (enabled) {
for (const name of Object.keys(this.env)) {
this.context.environmentVariableCollection.replace(name, this.env[name]);
}
}
}
constructor(private readonly context: ExtensionContext, private readonly env: { [key: string]: string }) {
this.disposable = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git'))
(this.refresh, this);
this.refresh();
}
private refresh(): void {
const config = workspace.getConfiguration('git', null);
this.enabled = config.get<boolean>('enabled', true) && config.get('terminalAuthentication', true);
}
dispose(): void {
this.disposable.dispose();
}
}

View File

@ -0,0 +1,399 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import { GitStatusParser, parseGitCommits, parseGitmodules, parseLsTree, parseLsFiles } from '../git';
import * as assert from 'assert';
import { splitInChunks } from '../util';
suite('git', () => {
suite('GitStatusParser', () => {
test('empty parser', () => {
const parser = new GitStatusParser();
assert.deepEqual(parser.status, []);
});
test('empty parser 2', () => {
const parser = new GitStatusParser();
parser.update('');
assert.deepEqual(parser.status, []);
});
test('simple', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('simple 2', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt\0');
parser.update('?? file2.txt\0');
parser.update('?? file3.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('empty lines', () => {
const parser = new GitStatusParser();
parser.update('');
parser.update('?? file.txt\0');
parser.update('');
parser.update('');
parser.update('?? file2.txt\0');
parser.update('');
parser.update('?? file3.txt\0');
parser.update('');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('combined', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt\0?? file2.txt\0?? file3.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('split 1', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt\0?? file2');
parser.update('.txt\0?? file3.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('split 2', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt');
parser.update('\0?? file2.txt\0?? file3.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('split 3', () => {
const parser = new GitStatusParser();
parser.update('?? file.txt\0?? file2.txt\0?? file3.txt');
parser.update('\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('rename', () => {
const parser = new GitStatusParser();
parser.update('R newfile.txt\0file.txt\0?? file2.txt\0?? file3.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: 'newfile.txt', x: 'R', y: ' ' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('rename split', () => {
const parser = new GitStatusParser();
parser.update('R newfile.txt\0fil');
parser.update('e.txt\0?? file2.txt\0?? file3.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file.txt', rename: 'newfile.txt', x: 'R', y: ' ' },
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
test('rename split 3', () => {
const parser = new GitStatusParser();
parser.update('?? file2.txt\0R new');
parser.update('file.txt\0fil');
parser.update('e.txt\0?? file3.txt\0');
assert.deepEqual(parser.status, [
{ path: 'file2.txt', rename: undefined, x: '?', y: '?' },
{ path: 'file.txt', rename: 'newfile.txt', x: 'R', y: ' ' },
{ path: 'file3.txt', rename: undefined, x: '?', y: '?' }
]);
});
});
suite('parseGitmodules', () => {
test('empty', () => {
assert.deepEqual(parseGitmodules(''), []);
});
test('sample', () => {
const sample = `[submodule "deps/spdlog"]
path = deps/spdlog
url = https://github.com/gabime/spdlog.git
`;
assert.deepEqual(parseGitmodules(sample), [
{ name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' }
]);
});
test('big', () => {
const sample = `[submodule "deps/spdlog"]
path = deps/spdlog
url = https://github.com/gabime/spdlog.git
[submodule "deps/spdlog2"]
path = deps/spdlog2
url = https://github.com/gabime/spdlog.git
[submodule "deps/spdlog3"]
path = deps/spdlog3
url = https://github.com/gabime/spdlog.git
[submodule "deps/spdlog4"]
path = deps/spdlog4
url = https://github.com/gabime/spdlog4.git
`;
assert.deepEqual(parseGitmodules(sample), [
{ name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' },
{ name: 'deps/spdlog2', path: 'deps/spdlog2', url: 'https://github.com/gabime/spdlog.git' },
{ name: 'deps/spdlog3', path: 'deps/spdlog3', url: 'https://github.com/gabime/spdlog.git' },
{ name: 'deps/spdlog4', path: 'deps/spdlog4', url: 'https://github.com/gabime/spdlog4.git' }
]);
});
test('whitespace #74844', () => {
const sample = `[submodule "deps/spdlog"]
path = deps/spdlog
url = https://github.com/gabime/spdlog.git
`;
assert.deepEqual(parseGitmodules(sample), [
{ name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' }
]);
});
test('whitespace again #108371', () => {
const sample = `[submodule "deps/spdlog"]
path= deps/spdlog
url=https://github.com/gabime/spdlog.git
`;
assert.deepEqual(parseGitmodules(sample), [
{ name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' }
]);
});
});
suite('parseGitCommit', () => {
test('single parent commit', function () {
const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1
John Doe
john.doe@mail.com
1580811030
1580811031
8e5a374372b8393906c7e380dbb09349c5385554
This is a commit message.\x00`;
assert.deepEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: ['8e5a374372b8393906c7e380dbb09349c5385554'],
authorDate: new Date(1580811030000),
authorName: 'John Doe',
authorEmail: 'john.doe@mail.com',
commitDate: new Date(1580811031000),
}]);
});
test('multiple parent commits', function () {
const GIT_OUTPUT_MULTIPLE_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1
John Doe
john.doe@mail.com
1580811030
1580811031
8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217
This is a commit message.\x00`;
assert.deepEqual(parseGitCommits(GIT_OUTPUT_MULTIPLE_PARENTS), [{
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: ['8e5a374372b8393906c7e380dbb09349c5385554', 'df27d8c75b129ab9b178b386077da2822101b217'],
authorDate: new Date(1580811030000),
authorName: 'John Doe',
authorEmail: 'john.doe@mail.com',
commitDate: new Date(1580811031000),
}]);
});
test('no parent commits', function () {
const GIT_OUTPUT_NO_PARENTS = `52c293a05038d865604c2284aa8698bd087915a1
John Doe
john.doe@mail.com
1580811030
1580811031
This is a commit message.\x00`;
assert.deepEqual(parseGitCommits(GIT_OUTPUT_NO_PARENTS), [{
hash: '52c293a05038d865604c2284aa8698bd087915a1',
message: 'This is a commit message.',
parents: [],
authorDate: new Date(1580811030000),
authorName: 'John Doe',
authorEmail: 'john.doe@mail.com',
commitDate: new Date(1580811031000),
}]);
});
});
suite('parseLsTree', function () {
test('sample', function () {
const input = `040000 tree 0274a81f8ee9ca3669295dc40f510bd2021d0043 - .vscode
100644 blob 1d487c1817262e4f20efbfa1d04c18f51b0046f6 491570 Screen Shot 2018-06-01 at 14.48.05.png
100644 blob 686c16e4f019b734655a2576ce8b98749a9ffdb9 764420 Screen Shot 2018-06-07 at 20.04.59.png
100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 4 boom.txt
100644 blob 86dc360dd25f13fa50ffdc8259e9653921f4f2b7 11 boomcaboom.txt
100644 blob a68b14060589b16d7ac75f67b905c918c03c06eb 24 file.js
100644 blob f7bcfb05af46850d780f88c069edcd57481d822d 201 file.md
100644 blob ab8b86114a051f6490f1ec5e3141b9a632fb46b5 8 hello.js
100644 blob 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 4 what.js
100644 blob be859e3f412fa86513cd8bebe8189d1ea1a3e46d 24 what.txt
100644 blob 56ec42c9dc6fcf4534788f0fe34b36e09f37d085 261186 what.txt2`;
const output = parseLsTree(input);
assert.deepEqual(output, [
{ mode: '040000', type: 'tree', object: '0274a81f8ee9ca3669295dc40f510bd2021d0043', size: '-', file: '.vscode' },
{ mode: '100644', type: 'blob', object: '1d487c1817262e4f20efbfa1d04c18f51b0046f6', size: '491570', file: 'Screen Shot 2018-06-01 at 14.48.05.png' },
{ mode: '100644', type: 'blob', object: '686c16e4f019b734655a2576ce8b98749a9ffdb9', size: '764420', file: 'Screen Shot 2018-06-07 at 20.04.59.png' },
{ mode: '100644', type: 'blob', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', size: '4', file: 'boom.txt' },
{ mode: '100644', type: 'blob', object: '86dc360dd25f13fa50ffdc8259e9653921f4f2b7', size: '11', file: 'boomcaboom.txt' },
{ mode: '100644', type: 'blob', object: 'a68b14060589b16d7ac75f67b905c918c03c06eb', size: '24', file: 'file.js' },
{ mode: '100644', type: 'blob', object: 'f7bcfb05af46850d780f88c069edcd57481d822d', size: '201', file: 'file.md' },
{ mode: '100644', type: 'blob', object: 'ab8b86114a051f6490f1ec5e3141b9a632fb46b5', size: '8', file: 'hello.js' },
{ mode: '100644', type: 'blob', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', size: '4', file: 'what.js' },
{ mode: '100644', type: 'blob', object: 'be859e3f412fa86513cd8bebe8189d1ea1a3e46d', size: '24', file: 'what.txt' },
{ mode: '100644', type: 'blob', object: '56ec42c9dc6fcf4534788f0fe34b36e09f37d085', size: '261186', file: 'what.txt2' }
]);
});
});
suite('parseLsFiles', function () {
test('sample', function () {
const input = `100644 7a73a41bfdf76d6f793007240d80983a52f15f97 0 .vscode/settings.json
100644 1d487c1817262e4f20efbfa1d04c18f51b0046f6 0 Screen Shot 2018-06-01 at 14.48.05.png
100644 686c16e4f019b734655a2576ce8b98749a9ffdb9 0 Screen Shot 2018-06-07 at 20.04.59.png
100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 boom.txt
100644 86dc360dd25f13fa50ffdc8259e9653921f4f2b7 0 boomcaboom.txt
100644 a68b14060589b16d7ac75f67b905c918c03c06eb 0 file.js
100644 f7bcfb05af46850d780f88c069edcd57481d822d 0 file.md
100644 ab8b86114a051f6490f1ec5e3141b9a632fb46b5 0 hello.js
100644 257cc5642cb1a054f08cc83f2d943e56fd3ebe99 0 what.js
100644 be859e3f412fa86513cd8bebe8189d1ea1a3e46d 0 what.txt
100644 56ec42c9dc6fcf4534788f0fe34b36e09f37d085 0 what.txt2`;
const output = parseLsFiles(input);
assert.deepEqual(output, [
{ mode: '100644', object: '7a73a41bfdf76d6f793007240d80983a52f15f97', stage: '0', file: '.vscode/settings.json' },
{ mode: '100644', object: '1d487c1817262e4f20efbfa1d04c18f51b0046f6', stage: '0', file: 'Screen Shot 2018-06-01 at 14.48.05.png' },
{ mode: '100644', object: '686c16e4f019b734655a2576ce8b98749a9ffdb9', stage: '0', file: 'Screen Shot 2018-06-07 at 20.04.59.png' },
{ mode: '100644', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', stage: '0', file: 'boom.txt' },
{ mode: '100644', object: '86dc360dd25f13fa50ffdc8259e9653921f4f2b7', stage: '0', file: 'boomcaboom.txt' },
{ mode: '100644', object: 'a68b14060589b16d7ac75f67b905c918c03c06eb', stage: '0', file: 'file.js' },
{ mode: '100644', object: 'f7bcfb05af46850d780f88c069edcd57481d822d', stage: '0', file: 'file.md' },
{ mode: '100644', object: 'ab8b86114a051f6490f1ec5e3141b9a632fb46b5', stage: '0', file: 'hello.js' },
{ mode: '100644', object: '257cc5642cb1a054f08cc83f2d943e56fd3ebe99', stage: '0', file: 'what.js' },
{ mode: '100644', object: 'be859e3f412fa86513cd8bebe8189d1ea1a3e46d', stage: '0', file: 'what.txt' },
{ mode: '100644', object: '56ec42c9dc6fcf4534788f0fe34b36e09f37d085', stage: '0', file: 'what.txt2' },
]);
});
});
suite('splitInChunks', () => {
test('unit tests', function () {
assert.deepEqual(
[...splitInChunks(['hello', 'there', 'cool', 'stuff'], 6)],
[['hello'], ['there'], ['cool'], ['stuff']]
);
assert.deepEqual(
[...splitInChunks(['hello', 'there', 'cool', 'stuff'], 10)],
[['hello', 'there'], ['cool', 'stuff']]
);
assert.deepEqual(
[...splitInChunks(['hello', 'there', 'cool', 'stuff'], 12)],
[['hello', 'there'], ['cool', 'stuff']]
);
assert.deepEqual(
[...splitInChunks(['hello', 'there', 'cool', 'stuff'], 14)],
[['hello', 'there', 'cool'], ['stuff']]
);
assert.deepEqual(
[...splitInChunks(['hello', 'there', 'cool', 'stuff'], 2000)],
[['hello', 'there', 'cool', 'stuff']]
);
assert.deepEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 1)],
[['0'], ['01'], ['012'], ['0'], ['01'], ['012'], ['0'], ['01'], ['012']]
);
assert.deepEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 2)],
[['0'], ['01'], ['012'], ['0'], ['01'], ['012'], ['0'], ['01'], ['012']]
);
assert.deepEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 3)],
[['0', '01'], ['012'], ['0', '01'], ['012'], ['0', '01'], ['012']]
);
assert.deepEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 4)],
[['0', '01'], ['012', '0'], ['01'], ['012', '0'], ['01'], ['012']]
);
assert.deepEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 5)],
[['0', '01'], ['012', '0'], ['01', '012'], ['0', '01'], ['012']]
);
assert.deepEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 6)],
[['0', '01', '012'], ['0', '01', '012'], ['0', '01', '012']]
);
assert.deepEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 7)],
[['0', '01', '012', '0'], ['01', '012', '0'], ['01', '012']]
);
assert.deepEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 8)],
[['0', '01', '012', '0'], ['01', '012', '0', '01'], ['012']]
);
assert.deepEqual(
[...splitInChunks(['0', '01', '012', '0', '01', '012', '0', '01', '012'], 9)],
[['0', '01', '012', '0', '01'], ['012', '0', '01', '012']]
);
});
});
});

View File

@ -0,0 +1,40 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const testRunner = require('vscode/lib/testrunner');
const options: any = {
ui: 'tdd',
useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'),
timeout: 60000
};
// These integration tests is being run in multiple environments (electron, web, remote)
// so we need to set the suite name based on the environment as the suite name is used
// for the test results file name
let suite = '';
if (process.env.VSCODE_BROWSER) {
suite = `${process.env.VSCODE_BROWSER} Browser Integration Git Tests`;
} else if (process.env.REMOTE_VSCODE) {
suite = 'Remote Integration Git Tests';
} else {
suite = 'Integration Git Tests';
}
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
options.reporter = 'mocha-multi-reporters';
options.reporterOptions = {
reporterEnabled: 'spec, mocha-junit-reporter',
mochaJunitReporterReporterOptions: {
testsuitesTitle: `${suite} ${process.platform}`,
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
}
};
}
testRunner.configure(options);
export = testRunner;

View File

@ -0,0 +1,127 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import * as assert from 'assert';
import { workspace, commands, window, Uri, WorkspaceEdit, Range, TextDocument, extensions } from 'vscode';
import * as cp from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { GitExtension, API, Repository, Status } from '../api/git';
import { eventToPromise } from '../util';
suite('git smoke test', function () {
const cwd = fs.realpathSync(workspace.workspaceFolders![0].uri.fsPath);
function file(relativePath: string) {
return path.join(cwd, relativePath);
}
function uri(relativePath: string) {
return Uri.file(file(relativePath));
}
async function open(relativePath: string) {
const doc = await workspace.openTextDocument(uri(relativePath));
await window.showTextDocument(doc);
return doc;
}
async function type(doc: TextDocument, text: string) {
const edit = new WorkspaceEdit();
const end = doc.lineAt(doc.lineCount - 1).range.end;
edit.replace(doc.uri, new Range(end, end), text);
await workspace.applyEdit(edit);
}
let git: API;
let repository: Repository;
suiteSetup(async function () {
fs.writeFileSync(file('app.js'), 'hello', 'utf8');
fs.writeFileSync(file('index.pug'), 'hello', 'utf8');
cp.execSync('git init', { cwd });
cp.execSync('git config user.name testuser', { cwd });
cp.execSync('git config user.email monacotools@microsoft.com', { cwd });
cp.execSync('git add .', { cwd });
cp.execSync('git commit -m "initial commit"', { cwd });
// make sure git is activated
const ext = extensions.getExtension<GitExtension>('vscode.git');
await ext?.activate();
git = ext!.exports.getAPI(1);
if (git.repositories.length === 0) {
await eventToPromise(git.onDidOpenRepository);
}
assert.equal(git.repositories.length, 1);
assert.equal(fs.realpathSync(git.repositories[0].rootUri.fsPath), cwd);
repository = git.repositories[0];
});
test('reflects working tree changes', async function () {
await commands.executeCommand('workbench.view.scm');
const appjs = await open('app.js');
await type(appjs, ' world');
await appjs.save();
await repository.status();
assert.equal(repository.state.workingTreeChanges.length, 1);
repository.state.workingTreeChanges.some(r => r.uri.path === appjs.uri.path && r.status === Status.MODIFIED);
fs.writeFileSync(file('newfile.txt'), '');
const newfile = await open('newfile.txt');
await type(newfile, 'hey there');
await newfile.save();
await repository.status();
assert.equal(repository.state.workingTreeChanges.length, 2);
repository.state.workingTreeChanges.some(r => r.uri.path === appjs.uri.path && r.status === Status.MODIFIED);
repository.state.workingTreeChanges.some(r => r.uri.path === newfile.uri.path && r.status === Status.UNTRACKED);
});
test('opens diff editor', async function () {
const appjs = uri('app.js');
await commands.executeCommand('git.openChange', appjs);
assert(window.activeTextEditor);
assert.equal(window.activeTextEditor!.document.uri.path, appjs.path);
// TODO: how do we really know this is a diff editor?
});
test('stages correctly', async function () {
const appjs = uri('app.js');
const newfile = uri('newfile.txt');
await commands.executeCommand('git.stage', appjs);
assert.equal(repository.state.workingTreeChanges.length, 1);
repository.state.workingTreeChanges.some(r => r.uri.path === newfile.path && r.status === Status.UNTRACKED);
assert.equal(repository.state.indexChanges.length, 1);
repository.state.indexChanges.some(r => r.uri.path === appjs.path && r.status === Status.INDEX_MODIFIED);
await commands.executeCommand('git.unstage', appjs);
assert.equal(repository.state.workingTreeChanges.length, 2);
repository.state.workingTreeChanges.some(r => r.uri.path === appjs.path && r.status === Status.MODIFIED);
repository.state.workingTreeChanges.some(r => r.uri.path === newfile.path && r.status === Status.UNTRACKED);
});
test('stages, commits changes and verifies outgoing change', async function () {
const appjs = uri('app.js');
const newfile = uri('newfile.txt');
await commands.executeCommand('git.stage', appjs);
await repository.commit('second commit');
assert.equal(repository.state.workingTreeChanges.length, 1);
repository.state.workingTreeChanges.some(r => r.uri.path === newfile.path && r.status === Status.UNTRACKED);
assert.equal(repository.state.indexChanges.length, 0);
await commands.executeCommand('git.stageAll', appjs);
await repository.commit('third commit');
assert.equal(repository.state.workingTreeChanges.length, 0);
assert.equal(repository.state.indexChanges.length, 0);
});
});

View File

@ -0,0 +1,264 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { CancellationToken, ConfigurationChangeEvent, Disposable, env, Event, EventEmitter, ThemeIcon, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider, Uri, workspace } from 'vscode';
import { Model } from './model';
import { Repository, Resource } from './repository';
import { debounce } from './decorators';
import { emojify, ensureEmojis } from './emoji';
const localize = nls.loadMessageBundle();
export class GitTimelineItem extends TimelineItem {
static is(item: TimelineItem): item is GitTimelineItem {
return item instanceof GitTimelineItem;
}
readonly ref: string;
readonly previousRef: string;
readonly message: string;
constructor(
ref: string,
previousRef: string,
message: string,
timestamp: number,
id: string,
contextValue: string
) {
const index = message.indexOf('\n');
const label = index !== -1 ? `${message.substring(0, index)} \u2026` : message;
super(label, timestamp);
this.ref = ref;
this.previousRef = previousRef;
this.message = message;
this.id = id;
this.contextValue = contextValue;
}
get shortRef() {
return this.shortenRef(this.ref);
}
get shortPreviousRef() {
return this.shortenRef(this.previousRef);
}
private shortenRef(ref: string): string {
if (ref === '' || ref === '~' || ref === 'HEAD') {
return ref;
}
return ref.endsWith('^') ? `${ref.substr(0, 8)}^` : ref.substr(0, 8);
}
}
export class GitTimelineProvider implements TimelineProvider {
private _onDidChange = new EventEmitter<TimelineChangeEvent>();
get onDidChange(): Event<TimelineChangeEvent> {
return this._onDidChange.event;
}
readonly id = 'git-history';
readonly label = localize('git.timeline.source', 'Git History');
private readonly disposable: Disposable;
private providerDisposable: Disposable | undefined;
private repo: Repository | undefined;
private repoDisposable: Disposable | undefined;
private repoStatusDate: Date | undefined;
constructor(private readonly model: Model) {
this.disposable = Disposable.from(
model.onDidOpenRepository(this.onRepositoriesChanged, this),
workspace.onDidChangeConfiguration(this.onConfigurationChanged, this)
);
if (model.repositories.length) {
this.ensureProviderRegistration();
}
}
dispose() {
this.providerDisposable?.dispose();
this.disposable.dispose();
}
async provideTimeline(uri: Uri, options: TimelineOptions, _token: CancellationToken): Promise<Timeline> {
// console.log(`GitTimelineProvider.provideTimeline: uri=${uri} state=${this._model.state}`);
const repo = this.model.getRepository(uri);
if (!repo) {
this.repoDisposable?.dispose();
this.repoStatusDate = undefined;
this.repo = undefined;
return { items: [] };
}
if (this.repo?.root !== repo.root) {
this.repoDisposable?.dispose();
this.repo = repo;
this.repoStatusDate = new Date();
this.repoDisposable = Disposable.from(
repo.onDidChangeRepository(uri => this.onRepositoryChanged(repo, uri)),
repo.onDidRunGitStatus(() => this.onRepositoryStatusChanged(repo))
);
}
const config = workspace.getConfiguration('git.timeline');
// TODO@eamodio: Ensure that the uri is a file -- if not we could get the history of the repo?
let limit: number | undefined;
if (options.limit !== undefined && typeof options.limit !== 'number') {
try {
const result = await this.model.git.exec(repo.root, ['rev-list', '--count', `${options.limit.id}..`, '--', uri.fsPath]);
if (!result.exitCode) {
// Ask for 2 more (1 for the limit commit and 1 for the next commit) than so we can determine if there are more commits
limit = Number(result.stdout) + 2;
}
}
catch {
limit = undefined;
}
} else {
// If we are not getting everything, ask for 1 more than so we can determine if there are more commits
limit = options.limit === undefined ? undefined : options.limit + 1;
}
await ensureEmojis();
const commits = await repo.logFile(uri, {
maxEntries: limit,
hash: options.cursor,
// sortByAuthorDate: true
});
const paging = commits.length ? {
cursor: limit === undefined ? undefined : (commits.length >= limit ? commits[commits.length - 1]?.hash : undefined)
} : undefined;
// If we asked for an extra commit, strip it off
if (limit !== undefined && commits.length >= limit) {
commits.splice(commits.length - 1, 1);
}
const dateFormatter = new Intl.DateTimeFormat(env.language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
const dateType = config.get<'committed' | 'authored'>('date');
const showAuthor = config.get<boolean>('showAuthor');
const items = commits.map<GitTimelineItem>((c, i) => {
const date = dateType === 'authored' ? c.authorDate : c.commitDate;
const message = emojify(c.message);
const item = new GitTimelineItem(c.hash, commits[i + 1]?.hash ?? `${c.hash}^`, message, date?.getTime() ?? 0, c.hash, 'git:file:commit');
item.iconPath = new (ThemeIcon as any)('git-commit');
if (showAuthor) {
item.description = c.authorName;
}
item.detail = `${c.authorName} (${c.authorEmail}) — ${c.hash.substr(0, 8)}\n${dateFormatter.format(date)}\n\n${message}`;
item.command = {
title: 'Open Comparison',
command: 'git.timeline.openDiff',
arguments: [item, uri, this.id]
};
return item;
});
if (options.cursor === undefined) {
const you = localize('git.timeline.you', 'You');
const index = repo.indexGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath);
if (index) {
const date = this.repoStatusDate ?? new Date();
const item = new GitTimelineItem('~', 'HEAD', localize('git.timeline.stagedChanges', 'Staged Changes'), date.getTime(), 'index', 'git:file:index');
// TODO@eamodio: Replace with a better icon -- reflecting its status maybe?
item.iconPath = new (ThemeIcon as any)('git-commit');
item.description = '';
item.detail = localize('git.timeline.detail', '{0} — {1}\n{2}\n\n{3}', you, localize('git.index', 'Index'), dateFormatter.format(date), Resource.getStatusText(index.type));
item.command = {
title: 'Open Comparison',
command: 'git.timeline.openDiff',
arguments: [item, uri, this.id]
};
items.splice(0, 0, item);
}
const working = repo.workingTreeGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath);
if (working) {
const date = new Date();
const item = new GitTimelineItem('', index ? '~' : 'HEAD', localize('git.timeline.uncommitedChanges', 'Uncommited Changes'), date.getTime(), 'working', 'git:file:working');
// TODO@eamodio: Replace with a better icon -- reflecting its status maybe?
item.iconPath = new (ThemeIcon as any)('git-commit');
item.description = '';
item.detail = localize('git.timeline.detail', '{0} — {1}\n{2}\n\n{3}', you, localize('git.workingTree', 'Working Tree'), dateFormatter.format(date), Resource.getStatusText(working.type));
item.command = {
title: 'Open Comparison',
command: 'git.timeline.openDiff',
arguments: [item, uri, this.id]
};
items.splice(0, 0, item);
}
}
return {
items: items,
paging: paging
};
}
private ensureProviderRegistration() {
if (this.providerDisposable === undefined) {
this.providerDisposable = workspace.registerTimelineProvider(['file', 'git', 'vscode-remote', 'gitlens-git'], this);
}
}
private onConfigurationChanged(e: ConfigurationChangeEvent) {
if (e.affectsConfiguration('git.timeline.date') || e.affectsConfiguration('git.timeline.showAuthor')) {
this.fireChanged();
}
}
private onRepositoriesChanged(_repo: Repository) {
// console.log(`GitTimelineProvider.onRepositoriesChanged`);
this.ensureProviderRegistration();
// TODO@eamodio: Being naive for now and just always refreshing each time there is a new repository
this.fireChanged();
}
private onRepositoryChanged(_repo: Repository, _uri: Uri) {
// console.log(`GitTimelineProvider.onRepositoryChanged: uri=${uri.toString(true)}`);
this.fireChanged();
}
private onRepositoryStatusChanged(_repo: Repository) {
// console.log(`GitTimelineProvider.onRepositoryStatusChanged`);
// This is less than ideal, but for now just save the last time a status was run and use that as the timestamp for staged items
this.repoStatusDate = new Date();
this.fireChanged();
}
@debounce(500)
private fireChanged() {
this._onDidChange.fire(undefined);
}
}

View File

@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/vs/vscode.d.ts'/>
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
/// <reference path="../../../types/lib.textEncoder.d.ts" />

View File

@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri } from 'vscode';
export interface GitUriParams {
path: string;
ref: string;
submoduleOf?: string;
}
export function isGitUri(uri: Uri): boolean {
return /^git$/.test(uri.scheme);
}
export function fromGitUri(uri: Uri): GitUriParams {
return JSON.parse(uri.query);
}
export interface GitUriOptions {
replaceFileExtension?: boolean;
submoduleOf?: string;
}
// As a mitigation for extensions like ESLint showing warnings and errors
// for git URIs, let's change the file extension of these uris to .git,
// when `replaceFileExtension` is true.
export function toGitUri(uri: Uri, ref: string, options: GitUriOptions = {}): Uri {
const params: GitUriParams = {
path: uri.fsPath,
ref
};
if (options.submoduleOf) {
params.submoduleOf = options.submoduleOf;
}
let path = uri.path;
if (options.replaceFileExtension) {
path = `${path}.git`;
} else if (options.submoduleOf) {
path = `${path}.diff`;
}
return uri.with({
scheme: 'git',
path,
query: JSON.stringify(params)
});
}

View File

@ -0,0 +1,416 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, Disposable, EventEmitter } from 'vscode';
import { dirname, sep } from 'path';
import { Readable } from 'stream';
import { promises as fs, createReadStream } from 'fs';
import * as byline from 'byline';
export function log(...args: any[]): void {
console.log.apply(console, ['git:', ...args]);
}
export interface IDisposable {
dispose(): void;
}
export function dispose<T extends IDisposable>(disposables: T[]): T[] {
disposables.forEach(d => d.dispose());
return [];
}
export function toDisposable(dispose: () => void): IDisposable {
return { dispose };
}
export function combinedDisposable(disposables: IDisposable[]): IDisposable {
return toDisposable(() => dispose(disposables));
}
export const EmptyDisposable = toDisposable(() => null);
export function fireEvent<T>(event: Event<T>): Event<T> {
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => event(_ => (listener as any).call(thisArgs), null, disposables);
}
export function mapEvent<I, O>(event: Event<I>, map: (i: I) => O): Event<O> {
return (listener: (e: O) => any, thisArgs?: any, disposables?: Disposable[]) => event(i => listener.call(thisArgs, map(i)), null, disposables);
}
export function filterEvent<T>(event: Event<T>, filter: (e: T) => boolean): Event<T> {
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => event(e => filter(e) && listener.call(thisArgs, e), null, disposables);
}
export function anyEvent<T>(...events: Event<T>[]): Event<T> {
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
const result = combinedDisposable(events.map(event => event(i => listener.call(thisArgs, i))));
if (disposables) {
disposables.push(result);
}
return result;
};
}
export function done<T>(promise: Promise<T>): Promise<void> {
return promise.then<void>(() => undefined);
}
export function onceEvent<T>(event: Event<T>): Event<T> {
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
const result = event(e => {
result.dispose();
return listener.call(thisArgs, e);
}, null, disposables);
return result;
};
}
export function debounceEvent<T>(event: Event<T>, delay: number): Event<T> {
return (listener: (e: T) => any, thisArgs?: any, disposables?: Disposable[]) => {
let timer: NodeJS.Timer;
return event(e => {
clearTimeout(timer);
timer = setTimeout(() => listener.call(thisArgs, e), delay);
}, null, disposables);
};
}
export function eventToPromise<T>(event: Event<T>): Promise<T> {
return new Promise<T>(c => onceEvent(event)(c));
}
export function once(fn: (...args: any[]) => any): (...args: any[]) => any {
let didRun = false;
return (...args) => {
if (didRun) {
return;
}
return fn(...args);
};
}
export function assign<T>(destination: T, ...sources: any[]): T {
for (const source of sources) {
Object.keys(source).forEach(key => (destination as any)[key] = source[key]);
}
return destination;
}
export function uniqBy<T>(arr: T[], fn: (el: T) => string): T[] {
const seen = Object.create(null);
return arr.filter(el => {
const key = fn(el);
if (seen[key]) {
return false;
}
seen[key] = true;
return true;
});
}
export function groupBy<T>(arr: T[], fn: (el: T) => string): { [key: string]: T[] } {
return arr.reduce((result, el) => {
const key = fn(el);
result[key] = [...(result[key] || []), el];
return result;
}, Object.create(null));
}
export async function mkdirp(path: string, mode?: number): Promise<boolean> {
const mkdir = async () => {
try {
await fs.mkdir(path, mode);
} catch (err) {
if (err.code === 'EEXIST') {
const stat = await fs.stat(path);
if (stat.isDirectory()) {
return;
}
throw new Error(`'${path}' exists and is not a directory.`);
}
throw err;
}
};
// is root?
if (path === dirname(path)) {
return true;
}
try {
await mkdir();
} catch (err) {
if (err.code !== 'ENOENT') {
throw err;
}
await mkdirp(dirname(path), mode);
await mkdir();
}
return true;
}
export function uniqueFilter<T>(keyFn: (t: T) => string): (t: T) => boolean {
const seen: { [key: string]: boolean; } = Object.create(null);
return element => {
const key = keyFn(element);
if (seen[key]) {
return false;
}
seen[key] = true;
return true;
};
}
export function find<T>(array: T[], fn: (t: T) => boolean): T | undefined {
let result: T | undefined = undefined;
array.some(e => {
if (fn(e)) {
result = e;
return true;
}
return false;
});
return result;
}
export async function grep(filename: string, pattern: RegExp): Promise<boolean> {
return new Promise<boolean>((c, e) => {
const fileStream = createReadStream(filename, { encoding: 'utf8' });
const stream = byline(fileStream);
stream.on('data', (line: string) => {
if (pattern.test(line)) {
fileStream.close();
c(true);
}
});
stream.on('error', e);
stream.on('end', () => c(false));
});
}
export function readBytes(stream: Readable, bytes: number): Promise<Buffer> {
return new Promise<Buffer>((complete, error) => {
let done = false;
let buffer = Buffer.allocUnsafe(bytes);
let bytesRead = 0;
stream.on('data', (data: Buffer) => {
let bytesToRead = Math.min(bytes - bytesRead, data.length);
data.copy(buffer, bytesRead, 0, bytesToRead);
bytesRead += bytesToRead;
if (bytesRead === bytes) {
(stream as any).destroy(); // Will trigger the close event eventually
}
});
stream.on('error', (e: Error) => {
if (!done) {
done = true;
error(e);
}
});
stream.on('close', () => {
if (!done) {
done = true;
complete(buffer.slice(0, bytesRead));
}
});
});
}
export const enum Encoding {
UTF8 = 'utf8',
UTF16be = 'utf16be',
UTF16le = 'utf16le'
}
export function detectUnicodeEncoding(buffer: Buffer): Encoding | null {
if (buffer.length < 2) {
return null;
}
const b0 = buffer.readUInt8(0);
const b1 = buffer.readUInt8(1);
if (b0 === 0xFE && b1 === 0xFF) {
return Encoding.UTF16be;
}
if (b0 === 0xFF && b1 === 0xFE) {
return Encoding.UTF16le;
}
if (buffer.length < 3) {
return null;
}
const b2 = buffer.readUInt8(2);
if (b0 === 0xEF && b1 === 0xBB && b2 === 0xBF) {
return Encoding.UTF8;
}
return null;
}
function isWindowsPath(path: string): boolean {
return /^[a-zA-Z]:\\/.test(path);
}
export function isDescendant(parent: string, descendant: string): boolean {
if (parent === descendant) {
return true;
}
if (parent.charAt(parent.length - 1) !== sep) {
parent += sep;
}
// Windows is case insensitive
if (isWindowsPath(parent)) {
parent = parent.toLowerCase();
descendant = descendant.toLowerCase();
}
return descendant.startsWith(parent);
}
export function pathEquals(a: string, b: string): boolean {
// Windows is case insensitive
if (isWindowsPath(a)) {
a = a.toLowerCase();
b = b.toLowerCase();
}
return a === b;
}
export function* splitInChunks(array: string[], maxChunkLength: number): IterableIterator<string[]> {
let current: string[] = [];
let length = 0;
for (const value of array) {
let newLength = length + value.length;
if (newLength > maxChunkLength && current.length > 0) {
yield current;
current = [];
newLength = value.length;
}
current.push(value);
length = newLength;
}
if (current.length > 0) {
yield current;
}
}
interface ILimitedTaskFactory<T> {
factory: () => Promise<T>;
c: (value: T | Promise<T>) => void;
e: (error?: any) => void;
}
export class Limiter<T> {
private runningPromises: number;
private maxDegreeOfParalellism: number;
private outstandingPromises: ILimitedTaskFactory<T>[];
constructor(maxDegreeOfParalellism: number) {
this.maxDegreeOfParalellism = maxDegreeOfParalellism;
this.outstandingPromises = [];
this.runningPromises = 0;
}
queue(factory: () => Promise<T>): Promise<T> {
return new Promise<T>((c, e) => {
this.outstandingPromises.push({ factory, c, e });
this.consume();
});
}
private consume(): void {
while (this.outstandingPromises.length && this.runningPromises < this.maxDegreeOfParalellism) {
const iLimitedTask = this.outstandingPromises.shift()!;
this.runningPromises++;
const promise = iLimitedTask.factory();
promise.then(iLimitedTask.c, iLimitedTask.e);
promise.then(() => this.consumed(), () => this.consumed());
}
}
private consumed(): void {
this.runningPromises--;
if (this.outstandingPromises.length > 0) {
this.consume();
}
}
}
type Completion<T> = { success: true, value: T } | { success: false, err: any };
export class PromiseSource<T> {
private _onDidComplete = new EventEmitter<Completion<T>>();
private _promise: Promise<T> | undefined;
get promise(): Promise<T> {
if (this._promise) {
return this._promise;
}
return eventToPromise(this._onDidComplete.event).then(completion => {
if (completion.success) {
return completion.value;
} else {
throw completion.err;
}
});
}
resolve(value: T): void {
if (!this._promise) {
this._promise = Promise.resolve(value);
this._onDidComplete.fire({ success: true, value });
}
}
reject(err: any): void {
if (!this._promise) {
this._promise = Promise.reject(err);
this._onDidComplete.fire({ success: false, err });
}
}
}

View File

@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, EventEmitter, Uri } from 'vscode';
import { join } from 'path';
import * as fs from 'fs';
import { IDisposable } from './util';
export interface IFileWatcher extends IDisposable {
readonly event: Event<Uri>;
}
export function watch(location: string): IFileWatcher {
const dotGitWatcher = fs.watch(location);
const onDotGitFileChangeEmitter = new EventEmitter<Uri>();
dotGitWatcher.on('change', (_, e) => onDotGitFileChangeEmitter.fire(Uri.file(join(location, e as string))));
dotGitWatcher.on('error', err => console.error(err));
return new class implements IFileWatcher {
event = onDotGitFileChangeEmitter.event;
dispose() { dotGitWatcher.close(); }
};
}