Merge from vscode merge-base (#22780)

* Revert "Revert "Merge from vscode merge-base (#22769)" (#22779)"

This reverts commit 47a1745180.

* Fix notebook download task

* Remove done call from extensions-ci
This commit is contained in:
Karl Burtram
2023-04-19 21:48:46 -07:00
committed by GitHub
parent decbe8dded
commit e7d3d047ec
2389 changed files with 92155 additions and 42602 deletions

View File

@@ -3,18 +3,23 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event, EventEmitter, SourceControlActionButton, Uri, workspace } from 'vscode';
import * as nls from 'vscode-nls';
import { Command, Disposable, Event, EventEmitter, SourceControlActionButton, Uri, workspace } from 'vscode';
import { ApiRepository } from './api/api1';
import { Branch, Status } from './api/git';
import { IPostCommitCommandsProviderRegistry } from './postCommitCommands';
import { Repository, Operation } from './repository';
import { dispose } from './util';
import { Branch } from './api/git';
const localize = nls.loadMessageBundle();
interface ActionButtonState {
readonly HEAD: Branch | undefined;
readonly isSyncRunning: boolean;
readonly repositoryHasNoChanges: boolean;
readonly isCommitInProgress: boolean;
readonly isMergeInProgress: boolean;
readonly isRebaseInProgress: boolean;
readonly isSyncInProgress: boolean;
readonly repositoryHasChangesToCommit: boolean;
}
export class ActionButtonCommand {
@@ -32,81 +37,263 @@ export class ActionButtonCommand {
private disposables: Disposable[] = [];
constructor(readonly repository: Repository) {
this._state = { HEAD: undefined, isSyncRunning: false, repositoryHasNoChanges: false };
constructor(
readonly repository: Repository,
readonly postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry) {
this._state = {
HEAD: undefined,
isCommitInProgress: false,
isMergeInProgress: false,
isRebaseInProgress: false,
isSyncInProgress: false,
repositoryHasChangesToCommit: false
};
repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables);
repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables);
this.disposables.push(postCommitCommandsProviderRegistry.onDidChangePostCommitCommandsProviders(() => this._onDidChange.fire()));
const root = Uri.file(repository.root);
this.disposables.push(workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('git.enableSmartCommit', root) ||
e.affectsConfiguration('git.smartCommitChanges', root) ||
e.affectsConfiguration('git.suggestSmartCommit', root)) {
this.onDidChangeSmartCommitSettings();
}
if (e.affectsConfiguration('git.branchProtection', root) ||
e.affectsConfiguration('git.branchProtectionPrompt', root) ||
e.affectsConfiguration('git.postCommitCommand', root) ||
e.affectsConfiguration('git.showActionButton', root)) {
this._onDidChange.fire();
}
}));
}
get button(): SourceControlActionButton | undefined {
if (!this.state.HEAD || !this.state.HEAD.name || !this.state.HEAD.commit) { return undefined; }
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<string>('showUnpublishedCommitsButton', 'whenEmpty');
const postCommitCommand = config.get<string>('postCommitCommand');
const noPostCommitCommand = postCommitCommand !== 'sync' && postCommitCommand !== 'push';
if (!this.state.HEAD) { return undefined; }
let actionButton: SourceControlActionButton | undefined;
if (showActionButton === 'always' || (showActionButton === 'whenEmpty' && this.state.repositoryHasNoChanges && noPostCommitCommand)) {
if (this.state.HEAD.upstream) {
if (this.state.HEAD.ahead) {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const rebaseWhenSync = config.get<string>('rebaseWhenSync');
const ahead = `${this.state.HEAD.ahead}$(arrow-up)`;
const behind = this.state.HEAD.behind ? `${this.state.HEAD.behind}$(arrow-down) ` : '';
const icon = this.state.isSyncRunning ? '$(sync~spin)' : '$(sync)';
if (this.state.repositoryHasChangesToCommit) {
// Commit Changes (enabled)
actionButton = this.getCommitActionButton();
}
actionButton = {
command: {
command: this.state.isSyncRunning ? '' : rebaseWhenSync ? 'git.syncRebase' : 'git.sync',
title: localize('scm button sync title', "{0} {1}{2}", icon, behind, ahead),
tooltip: this.state.isSyncRunning ?
localize('syncing changes', "Synchronizing Changes...")
: this.repository.syncTooltip,
arguments: [this.repository.sourceControl],
},
description: localize('scm button sync description', "{0} Sync Changes {1}{2}", icon, behind, ahead)
};
// Commit Changes (enabled) -> Publish Branch -> Sync Changes -> Commit Changes (disabled)
return actionButton ?? this.getPublishBranchActionButton() ?? this.getSyncChangesActionButton() ?? this.getCommitActionButton();
}
private getCommitActionButton(): SourceControlActionButton | undefined {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<{ commit: boolean }>('showActionButton', { commit: true });
// The button is disabled
if (!showActionButton.commit) { return undefined; }
return {
command: this.getCommitActionButtonPrimaryCommand(),
secondaryCommands: this.getCommitActionButtonSecondaryCommands(),
enabled: (this.state.repositoryHasChangesToCommit || this.state.isRebaseInProgress) && !this.state.isCommitInProgress && !this.state.isMergeInProgress
};
}
private getCommitActionButtonPrimaryCommand(): Command {
// Rebase Continue
if (this.state.isRebaseInProgress) {
return {
command: 'git.commit',
title: localize('scm button continue title', "{0} Continue", '$(check)'),
tooltip: this.state.isCommitInProgress ? localize('scm button continuing tooltip', "Continuing Rebase...") : localize('scm button continue tooltip', "Continue Rebase"),
arguments: [this.repository.sourceControl, '']
};
}
// Commit
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const postCommitCommand = config.get<string>('postCommitCommand');
// Branch protection
const isBranchProtected = this.repository.isBranchProtected();
const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!;
const alwaysPrompt = isBranchProtected && branchProtectionPrompt === 'alwaysPrompt';
const alwaysCommitToNewBranch = isBranchProtected && branchProtectionPrompt === 'alwaysCommitToNewBranch';
// Icon
const icon = alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined;
let commandArg = '';
let title = localize('scm button commit title', "{0} Commit", icon ?? '$(check)');
let tooltip = this.state.isCommitInProgress ? localize('scm button committing tooltip', "Committing Changes...") : localize('scm button commit tooltip', "Commit Changes");
// Title, tooltip
switch (postCommitCommand) {
case 'push': {
commandArg = 'git.push';
title = localize('scm button commit and push title', "{0} Commit & Push", icon ?? '$(arrow-up)');
if (alwaysCommitToNewBranch) {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing to new branch and pushing tooltip', "Committing to New Branch & Pushing Changes...") :
localize('scm button commit to new branch and push tooltip', "Commit to New Branch & Push Changes");
} else {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing and pushing tooltip', "Committing & Pushing Changes...") :
localize('scm button commit and push tooltip', "Commit & Push Changes");
}
} else {
actionButton = {
command: {
command: this.state.isSyncRunning ? '' : 'git.publish',
title: localize('scm button publish title', "$(cloud-upload) Publish Branch"),
tooltip: this.state.isSyncRunning ?
localize('scm button publish branch running', "Publishing Branch...") :
localize('scm button publish branch', "Publish Branch"),
arguments: [this.repository.sourceControl],
}
};
break;
}
case 'sync': {
commandArg = 'git.sync';
title = localize('scm button commit and sync title', "{0} Commit & Sync", icon ?? '$(sync)');
if (alwaysCommitToNewBranch) {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing to new branch and synching tooltip', "Committing to New Branch & Synching Changes...") :
localize('scm button commit to new branch and sync tooltip', "Commit to New Branch & Sync Changes");
} else {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing and synching tooltip', "Committing & Synching Changes...") :
localize('scm button commit and sync tooltip', "Commit & Sync Changes");
}
break;
}
default: {
if (alwaysCommitToNewBranch) {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing to new branch tooltip', "Committing Changes to New Branch...") :
localize('scm button commit to new branch tooltip', "Commit Changes to New Branch");
}
break;
}
}
return actionButton;
return { command: 'git.commit', title, tooltip, arguments: [this.repository.sourceControl, commandArg] };
}
private getCommitActionButtonSecondaryCommands(): Command[][] {
const commandGroups: Command[][] = [];
if (!this.state.isRebaseInProgress) {
for (const provider of this.postCommitCommandsProviderRegistry.getPostCommitCommandsProviders()) {
const commands = provider.getCommands(new ApiRepository(this.repository));
commandGroups.push((commands ?? []).map(c => {
return {
command: 'git.commit',
title: c.title,
arguments: [this.repository.sourceControl, c.command]
};
}));
}
if (commandGroups.length > 0) {
commandGroups[0].splice(0, 0, { command: 'git.commit', title: localize('scm secondary button commit', "Commit") });
}
}
return commandGroups;
}
private getPublishBranchActionButton(): SourceControlActionButton | undefined {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<{ publish: boolean }>('showActionButton', { publish: true });
// Branch does have an upstream, commit/merge/rebase is in progress, or the button is disabled
if (this.state.HEAD?.upstream || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.publish) { return undefined; }
return {
command: {
command: 'git.publish',
title: localize({ key: 'scm publish branch action button title', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }, "{0} Publish Branch", '$(cloud-upload)'),
tooltip: this.state.isSyncInProgress ?
localize({ key: 'scm button publish branch running', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }, "Publishing Branch...") :
localize({ key: 'scm button publish branch', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }, "Publish Branch"),
arguments: [this.repository.sourceControl],
},
enabled: !this.state.isSyncInProgress
};
}
private getSyncChangesActionButton(): SourceControlActionButton | undefined {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<{ sync: boolean }>('showActionButton', { sync: true });
const branchIsAheadOrBehind = (this.state.HEAD?.behind ?? 0) > 0 || (this.state.HEAD?.ahead ?? 0) > 0;
// Branch does not have an upstream, branch is not ahead/behind the remote branch, commit/merge/rebase is in progress, or the button is disabled
if (!this.state.HEAD?.upstream || !branchIsAheadOrBehind || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.sync) { return undefined; }
const ahead = this.state.HEAD.ahead ? ` ${this.state.HEAD.ahead}$(arrow-up)` : '';
const behind = this.state.HEAD.behind ? ` ${this.state.HEAD.behind}$(arrow-down)` : '';
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(sync)';
return {
command: {
command: 'git.sync',
title: `${icon}${behind}${ahead}`,
tooltip: this.state.isSyncInProgress ?
localize('syncing changes', "Synchronizing Changes...")
: this.repository.syncTooltip,
arguments: [this.repository.sourceControl],
},
description: localize('scm button sync description', "{0} Sync Changes{1}{2}", icon, behind, ahead),
enabled: !this.state.isSyncInProgress
};
}
private onDidChangeOperations(): void {
const isSyncRunning = this.repository.operations.isRunning(Operation.Sync) ||
const isCommitInProgress =
this.repository.operations.isRunning(Operation.Commit) ||
this.repository.operations.isRunning(Operation.RebaseContinue);
const isSyncInProgress =
this.repository.operations.isRunning(Operation.Sync) ||
this.repository.operations.isRunning(Operation.Push) ||
this.repository.operations.isRunning(Operation.Pull);
this.state = { ...this.state, isSyncRunning };
this.state = { ...this.state, isCommitInProgress, isSyncInProgress };
}
private onDidChangeSmartCommitSettings(): void {
this.state = {
...this.state,
repositoryHasChangesToCommit: this.repositoryHasChangesToCommit()
};
}
private onDidRunGitStatus(): void {
this.state = {
...this.state,
HEAD: this.repository.HEAD,
repositoryHasNoChanges:
this.repository.indexGroup.resourceStates.length === 0 &&
this.repository.mergeGroup.resourceStates.length === 0 &&
this.repository.untrackedGroup.resourceStates.length === 0 &&
this.repository.workingTreeGroup.resourceStates.length === 0
isMergeInProgress: this.repository.mergeGroup.resourceStates.length !== 0,
isRebaseInProgress: !!this.repository.rebaseCommit,
repositoryHasChangesToCommit: this.repositoryHasChangesToCommit()
};
}
private repositoryHasChangesToCommit(): boolean {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;
const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges', 'all');
const resources = [...this.repository.indexGroup.resourceStates];
if (
// Smart commit enabled (all)
(enableSmartCommit && smartCommitChanges === 'all') ||
// Smart commit disabled, smart suggestion enabled
(!enableSmartCommit && suggestSmartCommit)
) {
resources.push(...this.repository.workingTreeGroup.resourceStates);
}
// Smart commit enabled (tracked only)
if (enableSmartCommit && smartCommitChanges === 'tracked') {
resources.push(...this.repository.workingTreeGroup.resourceStates.filter(r => r.type !== Status.UNTRACKED));
}
return resources.length !== 0;
}
dispose(): void {
this.disposables = dispose(this.disposables);
}

View File

@@ -5,8 +5,8 @@
import { Model } from '../model';
import { Repository as BaseRepository, Resource } from '../repository';
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, ICloneOptions } from './git'; // {{SQL CARBON EDIT}} add ICloneOptions
import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode';
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, ICloneOptions, PostCommitCommandsProvider } from './git'; // {{SQL CARBON EDIT}} add ICloneOptions
import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode'; // {{SQL CARBON EDIT}} Add cancellationToken
import { combinedDisposable, mapEvent } from '../util';
import { toGitUri } from '../uri';
import { GitExtensionImpl } from './extension';
@@ -57,157 +57,157 @@ export class ApiRepositoryUIState implements RepositoryUIState {
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);
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) { }
constructor(readonly repository: BaseRepository) { }
apply(patch: string, reverse?: boolean): Promise<void> {
return this._repository.apply(patch, reverse);
return this.repository.apply(patch, reverse);
}
getConfigs(): Promise<{ key: string; value: string }[]> {
return this._repository.getConfigs();
return this.repository.getConfigs();
}
getConfig(key: string): Promise<string> {
return this._repository.getConfig(key);
return this.repository.getConfig(key);
}
setConfig(key: string, value: string): Promise<string> {
return this._repository.setConfig(key, value);
return this.repository.setConfig(key, value);
}
getGlobalConfig(key: string): Promise<string> {
return this._repository.getGlobalConfig(key);
return this.repository.getGlobalConfig(key);
}
getObjectDetails(treeish: string, path: string): Promise<{ mode: string; object: string; size: number }> {
return this._repository.getObjectDetails(treeish, path);
return this.repository.getObjectDetails(treeish, path);
}
detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string }> {
return this._repository.detectObjectType(object);
return this.repository.detectObjectType(object);
}
buffer(ref: string, filePath: string): Promise<Buffer> {
return this._repository.buffer(ref, filePath);
return this.repository.buffer(ref, filePath);
}
show(ref: string, path: string): Promise<string> {
return this._repository.show(ref, path);
return this.repository.show(ref, path);
}
getCommit(ref: string): Promise<Commit> {
return this._repository.getCommit(ref);
return this.repository.getCommit(ref);
}
add(paths: string[]) {
return this._repository.add(paths.map(p => Uri.file(p)));
return this.repository.add(paths.map(p => Uri.file(p)));
}
revert(paths: string[]) {
return this._repository.revert(paths.map(p => Uri.file(p)));
return this.repository.revert(paths.map(p => Uri.file(p)));
}
clean(paths: string[]) {
return this._repository.clean(paths.map(p => Uri.file(p)));
return this.repository.clean(paths.map(p => Uri.file(p)));
}
diff(cached?: boolean) {
return this._repository.diff(cached);
return this.repository.diff(cached);
}
diffWithHEAD(): Promise<Change[]>;
diffWithHEAD(path: string): Promise<string>;
diffWithHEAD(path?: string): Promise<string | Change[]> {
return this._repository.diffWithHEAD(path);
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);
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);
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);
return this.repository.diffIndexWith(ref, path);
}
diffBlobs(object1: string, object2: string): Promise<string> {
return this._repository.diffBlobs(object1, object2);
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);
return this.repository.diffBetween(ref1, ref2, path);
}
hashObject(data: string): Promise<string> {
return this._repository.hashObject(data);
return this.repository.hashObject(data);
}
createBranch(name: string, checkout: boolean, ref?: string | undefined): Promise<void> {
return this._repository.branch(name, checkout, ref);
return this.repository.branch(name, checkout, ref);
}
deleteBranch(name: string, force?: boolean): Promise<void> {
return this._repository.deleteBranch(name, force);
return this.repository.deleteBranch(name, force);
}
getBranch(name: string): Promise<Branch> {
return this._repository.getBranch(name);
return this.repository.getBranch(name);
}
getBranches(query: BranchQuery): Promise<Ref[]> {
return this._repository.getBranches(query);
return this.repository.getBranches(query);
}
setBranchUpstream(name: string, upstream: string): Promise<void> {
return this._repository.setBranchUpstream(name, upstream);
return this.repository.setBranchUpstream(name, upstream);
}
getMergeBase(ref1: string, ref2: string): Promise<string> {
return this._repository.getMergeBase(ref1, ref2);
return this.repository.getMergeBase(ref1, ref2);
}
tag(name: string, upstream: string): Promise<void> {
return this._repository.tag(name, upstream);
return this.repository.tag(name, upstream);
}
deleteTag(name: string): Promise<void> {
return this._repository.deleteTag(name);
return this.repository.deleteTag(name);
}
status(): Promise<void> {
return this._repository.status();
return this.repository.status();
}
checkout(treeish: string): Promise<void> {
return this._repository.checkout(treeish);
return this.repository.checkout(treeish);
}
addRemote(name: string, url: string): Promise<void> {
return this._repository.addRemote(name, url);
return this.repository.addRemote(name, url);
}
removeRemote(name: string): Promise<void> {
return this._repository.removeRemote(name);
return this.repository.removeRemote(name);
}
renameRemote(name: string, newName: string): Promise<void> {
return this._repository.renameRemote(name, newName);
return this.repository.renameRemote(name, newName);
}
fetch(arg0?: FetchOptions | string | undefined,
@@ -216,30 +216,30 @@ export class ApiRepository implements Repository {
prune?: boolean | undefined
): Promise<void> {
if (arg0 !== undefined && typeof arg0 !== 'string') {
return this._repository.fetch(arg0);
return this.repository.fetch(arg0);
}
return this._repository.fetch({ remote: arg0, ref, depth, prune });
return this.repository.fetch({ remote: arg0, ref, depth, prune });
}
pull(unshallow?: boolean): Promise<void> {
return this._repository.pull(undefined, unshallow);
return this.repository.pull(undefined, unshallow);
}
push(remoteName?: string, branchName?: string, setUpstream: boolean = false, force?: ForcePushMode): Promise<void> {
return this._repository.pushTo(remoteName, branchName, setUpstream, force);
return this.repository.pushTo(remoteName, branchName, setUpstream, force);
}
blame(path: string): Promise<string> {
return this._repository.blame(path);
return this.repository.blame(path);
}
log(options?: LogOptions): Promise<Commit[]> {
return this._repository.log(options);
return this.repository.log(options);
}
commit(message: string, opts?: CommitOptions): Promise<void> {
return this._repository.commit(message, opts);
return this.repository.commit(message, opts);
}
}
@@ -323,6 +323,10 @@ export class ApiImpl implements API {
return this._model.registerCredentialsProvider(provider);
}
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable {
return this._model.registerPostCommitCommandsProvider(provider);
}
registerPushErrorHandler(handler: PushErrorHandler): Disposable {
return this._model.registerPushErrorHandler(handler);
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri, Event, Disposable, ProviderResult, CancellationToken, Progress } from 'vscode'; // {{SQL CARBON EDIT}} add CancellationToken
import { Uri, Event, Disposable, ProviderResult, CancellationToken, Progress, Command } from 'vscode'; // {{SQL CARBON EDIT}} add CancellationToken
export { ProviderResult } from 'vscode';
export interface Git {
@@ -137,6 +137,9 @@ export interface CommitOptions {
empty?: boolean;
noVerify?: boolean;
requireUserConfig?: boolean;
useEditor?: boolean;
verbose?: boolean;
postCommitCommand?: string;
}
export interface FetchOptions {
@@ -251,6 +254,10 @@ export interface CredentialsProvider {
getCredentials(host: Uri): ProviderResult<Credentials>;
}
export interface PostCommitCommandsProvider {
getCommands(repository: Repository): Command[];
}
export interface PushErrorHandler {
handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean>;
}
@@ -287,6 +294,7 @@ export interface API {
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable;
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable;
registerPushErrorHandler(handler: PushErrorHandler): Disposable;
}
@@ -298,7 +306,7 @@ export interface GitExtension {
/**
* Returns a specific API version.
*
* Throws error if git extension is disabled. You can listed to the
* Throws error if git extension is disabled. You can listen to the
* [GitExtension.onDidChangeEnablement](#GitExtension.onDidChangeEnablement) event
* to know when the extension becomes enabled/disabled.
*
@@ -344,6 +352,7 @@ export const enum GitErrorCodes {
PatchDoesNotApply = 'PatchDoesNotApply',
NoPathFound = 'NoPathFound',
UnknownPath = 'UnknownPath',
EmptyCommitMessage = 'EmptyCommitMessage'
}
// {{SQL CARBON EDIT}} move ICloneOptions from git.ts to here since it's used in clone()

View File

@@ -3,31 +3,31 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { window, InputBoxOptions, Uri, OutputChannel, Disposable, workspace } from 'vscode';
import { IDisposable, EmptyDisposable, toDisposable, logTimestamp } from './util';
import { window, InputBoxOptions, Uri, Disposable, workspace } from 'vscode';
import { IDisposable, EmptyDisposable, toDisposable } from './util';
import * as path from 'path';
import { IIPCHandler, IIPCServer, createIPCServer } from './ipc/ipcServer';
import { IIPCHandler, IIPCServer } from './ipc/ipcServer';
import { CredentialsProvider, Credentials } from './api/git';
import { ITerminalEnvironmentProvider } from './terminal';
export class Askpass implements IIPCHandler {
export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider {
private env: { [key: string]: string };
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(`${logTimestamp()} [error] Failed to create git askpass IPC: ${err}`);
return new Askpass();
}
}
private constructor(private ipc?: IIPCServer) {
constructor(private ipc?: IIPCServer) {
if (ipc) {
this.disposable = ipc.registerHandler('askpass', this);
}
this.env = {
GIT_ASKPASS: path.join(__dirname, this.ipc ? 'askpass.sh' : 'askpass-empty.sh'),
VSCODE_GIT_ASKPASS_NODE: process.execPath,
VSCODE_GIT_ASKPASS_EXTRA_ARGS: (process.versions['electron'] && process.versions['microsoft-build']) ? '--ms-enable-electron-run-as-node' : '',
VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js'),
};
}
async handle({ request, host }: { request: string; host: string }): Promise<string> {
@@ -73,25 +73,13 @@ export class Askpass implements IIPCHandler {
}
getEnv(): { [key: string]: string } {
if (!this.ipc) {
return {
GIT_ASKPASS: path.join(__dirname, 'askpass-empty.sh')
};
}
let env: { [key: string]: string } = {
...this.ipc.getEnv(),
VSCODE_GIT_ASKPASS_NODE: process.execPath,
VSCODE_GIT_ASKPASS_EXTRA_ARGS: (process.versions['electron'] && process.versions['microsoft-build']) ? '--ms-enable-electron-run-as-node' : '',
VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js')
};
const config = workspace.getConfiguration('git');
if (config.get<boolean>('useIntegratedAskPass')) {
env.GIT_ASKPASS = path.join(__dirname, 'askpass.sh');
}
return config.get<boolean>('useIntegratedAskPass') ? this.env : {};
}
return env;
getTerminalEnv(): { [key: string]: string } {
const config = workspace.getConfiguration('git');
return config.get<boolean>('useIntegratedAskPass') && config.get<boolean>('terminalAuthentication') ? this.env : {};
}
registerCredentialsProvider(provider: CredentialsProvider): Disposable {

View File

@@ -5,17 +5,18 @@
import * as os from 'os';
import * as path from 'path';
import { Command, commands, Disposable, LineChange, MessageOptions, OutputChannel, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider } from 'vscode';
import { Command, commands, Disposable, LineChange, MessageOptions, Position, ProgressLocation, QuickPickItem, Range, SourceControlResourceState, TextDocumentShowOptions, TextEditor, Uri, ViewColumn, window, workspace, WorkspaceEdit, WorkspaceFolder, TimelineItem, env, Selection, TextDocumentContentProvider, InputBoxValidationSeverity, TabInputText, TabInputTextMerge } from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import * as nls from 'vscode-nls';
import { uniqueNamesGenerator, adjectives, animals, colors, NumberDictionary } from '@joaomoreno/unique-names-generator';
import { Branch, ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourcePublisher } from './api/git';
import { Git, Stash } from './git';
import { Model } from './model';
import { Repository, Resource, ResourceGroupType } from './repository';
import { applyLineChanges, getModifiedRange, intersectDiffWithRange, invertLineChange, toLineRanges } from './staging';
import { fromGitUri, toGitUri, isGitUri } from './uri';
import { grep, isDescendant, logTimestamp, pathEquals, relativePath } from './util';
import { Log, LogLevel } from './log';
import { fromGitUri, toGitUri, isGitUri, toMergeUris } from './uri';
import { grep, isDescendant, pathEquals, relativePath } from './util';
import { LogLevel, OutputChannelLogger } from './log';
import { GitTimelineItem } from './timelineProvider';
import { ApiRepository } from './api/api1';
import { pickRemoteSource } from './remoteSource';
@@ -25,24 +26,26 @@ const localize = nls.loadMessageBundle();
class CheckoutItem implements QuickPickItem {
protected get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); }
get label(): string { return this.ref.name || this.shortCommit; }
get label(): string { return `${this.repository.isBranchProtected(this.ref.name ?? '') ? '$(lock)' : '$(git-branch)'} ${this.ref.name || this.shortCommit}`; }
get description(): string { return this.shortCommit; }
get refName(): string | undefined { return this.ref.name; }
constructor(protected ref: Ref) { }
constructor(protected repository: Repository, protected ref: Ref) { }
async run(repository: Repository, opts?: { detached?: boolean }): Promise<void> {
async run(opts?: { detached?: boolean }): Promise<void> {
const ref = this.ref.name;
if (!ref) {
return;
}
await repository.checkout(ref, opts);
await this.repository.checkout(ref, opts);
}
}
class CheckoutTagItem extends CheckoutItem {
override get label(): string { return `$(tag) ${this.ref.name || this.shortCommit}`; }
override get description(): string {
return localize('tag at', "Tag at {0}", this.shortCommit);
}
@@ -50,21 +53,22 @@ class CheckoutTagItem extends CheckoutItem {
class CheckoutRemoteHeadItem extends CheckoutItem {
override get label(): string { return `$(cloud) ${this.ref.name || this.shortCommit}`; }
override get description(): string {
return localize('remote branch at', "Remote branch at {0}", this.shortCommit);
}
override async run(repository: Repository, opts?: { detached?: boolean }): Promise<void> {
override async run(opts?: { detached?: boolean }): Promise<void> {
if (!this.ref.name) {
return;
}
const branches = await repository.findTrackingBranches(this.ref.name);
const branches = await this.repository.findTrackingBranches(this.ref.name);
if (branches.length > 0) {
await repository.checkout(branches[0].name!, opts);
await this.repository.checkout(branches[0].name!, opts);
} else {
await repository.checkoutTracking(this.ref.name, opts);
await this.repository.checkoutTracking(this.ref.name, opts);
}
}
}
@@ -137,6 +141,7 @@ class HEADItem implements QuickPickItem {
get label(): string { return 'HEAD'; }
get description(): string { return (this.repository.HEAD && this.repository.HEAD.commit || '').substr(0, 8); }
get alwaysShow(): boolean { return true; }
get refName(): string { return 'HEAD'; }
}
class AddRemoteItem implements QuickPickItem {
@@ -217,7 +222,7 @@ function createCheckoutItems(repository: Repository): CheckoutItem[] {
checkoutTypes = checkoutTypeConfig;
}
const processors = checkoutTypes.map(getCheckoutProcessor)
const processors = checkoutTypes.map(type => getCheckoutProcessor(repository, type))
.filter(p => !!p) as CheckoutProcessor[];
for (const ref of repository.refs) {
@@ -232,8 +237,8 @@ function createCheckoutItems(repository: Repository): CheckoutItem[] {
class CheckoutProcessor {
private refs: Ref[] = [];
get items(): CheckoutItem[] { return this.refs.map(r => new this.ctor(r)); }
constructor(private type: RefType, private ctor: { new(ref: Ref): CheckoutItem }) { }
get items(): CheckoutItem[] { return this.refs.map(r => new this.ctor(this.repository, r)); }
constructor(private repository: Repository, private type: RefType, private ctor: { new(repository: Repository, ref: Ref): CheckoutItem }) { }
onRef(ref: Ref): void {
if (ref.type === this.type) {
@@ -242,14 +247,14 @@ class CheckoutProcessor {
}
}
function getCheckoutProcessor(type: string): CheckoutProcessor | undefined {
function getCheckoutProcessor(repository: Repository, type: string): CheckoutProcessor | undefined {
switch (type) {
case 'local':
return new CheckoutProcessor(RefType.Head, CheckoutItem);
return new CheckoutProcessor(repository, RefType.Head, CheckoutItem);
case 'remote':
return new CheckoutProcessor(RefType.RemoteHead, CheckoutRemoteHeadItem);
return new CheckoutProcessor(repository, RefType.RemoteHead, CheckoutRemoteHeadItem);
case 'tags':
return new CheckoutProcessor(RefType.Tag, CheckoutTagItem);
return new CheckoutProcessor(repository, RefType.Tag, CheckoutTagItem);
}
return undefined;
@@ -310,7 +315,7 @@ export class CommandCenter {
constructor(
private git: Git,
private model: Model,
private outputChannel: OutputChannel,
private outputChannelLogger: OutputChannelLogger,
private telemetryReporter: TelemetryReporter
) {
this.disposables = Commands.map(({ commandId, key, method, options }) => {
@@ -328,11 +333,25 @@ export class CommandCenter {
@command('git.setLogLevel')
async setLogLevel(): Promise<void> {
const createItem = (logLevel: LogLevel) => ({
label: LogLevel[logLevel],
logLevel,
description: Log.logLevel === logLevel ? localize('current', "Current") : undefined
});
const createItem = (logLevel: LogLevel) => {
let description: string | undefined;
const defaultDescription = localize('default', "Default");
const currentDescription = localize('current', "Current");
if (logLevel === this.outputChannelLogger.defaultLogLevel && logLevel === this.outputChannelLogger.currentLogLevel) {
description = `${defaultDescription} & ${currentDescription} `;
} else if (logLevel === this.outputChannelLogger.defaultLogLevel) {
description = defaultDescription;
} else if (logLevel === this.outputChannelLogger.currentLogLevel) {
description = currentDescription;
}
return {
label: LogLevel[logLevel],
logLevel,
description
};
};
const items = [
createItem(LogLevel.Trace),
@@ -352,8 +371,7 @@ export class CommandCenter {
return;
}
Log.logLevel = choice.logLevel;
this.outputChannel.appendLine(localize('changed', "{0} Log level changed to: {1}", logTimestamp(), LogLevel[Log.logLevel]));
this.outputChannelLogger.currentLogLevel = choice.logLevel;
}
@command('git.refresh', { repository: true })
@@ -390,6 +408,55 @@ export class CommandCenter {
}
}
@command('_git.openMergeEditor')
async openMergeEditor(uri: unknown) {
if (!(uri instanceof Uri)) {
return;
}
const repo = this.model.getRepository(uri);
if (!repo) {
return;
}
const isRebasing = Boolean(repo.rebaseCommit);
type InputData = { uri: Uri; title?: string; detail?: string; description?: string };
const mergeUris = toMergeUris(uri);
const ours: InputData = { uri: mergeUris.ours, title: localize('Yours', 'Yours') };
const theirs: InputData = { uri: mergeUris.theirs, title: localize('Theirs', 'Theirs') };
try {
const [head, rebaseOrMergeHead] = await Promise.all([
repo.getCommit('HEAD'),
isRebasing ? repo.getCommit('REBASE_HEAD') : repo.getCommit('MERGE_HEAD')
]);
// ours (current branch and commit)
ours.detail = head.refNames.map(s => s.replace(/^HEAD ->/, '')).join(', ');
ours.description = '$(git-commit) ' + head.hash.substring(0, 7);
// theirs
theirs.detail = rebaseOrMergeHead.refNames.join(', ');
theirs.description = '$(git-commit) ' + rebaseOrMergeHead.hash.substring(0, 7);
} catch (error) {
// not so bad, can continue with just uris
console.error('FAILED to read HEAD, MERGE_HEAD commits');
console.error(error);
}
const options = {
base: mergeUris.base,
input1: isRebasing ? ours : theirs,
input2: isRebasing ? theirs : ours,
output: uri
};
await commands.executeCommand(
'_open.mergeEditor',
options
);
}
async cloneRepository(url?: string, parentPath?: string, options: { recursive?: boolean } = {}): Promise<void> {
if (!url || typeof url !== 'string') {
url = await pickRemoteSource({
@@ -401,7 +468,8 @@ export class CommandCenter {
if (!url) {
/* __GDPR__
"clone" : {
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
"owner": "lszomoru",
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_URL' });
@@ -426,7 +494,8 @@ export class CommandCenter {
if (!uris || uris.length === 0) {
/* __GDPR__
"clone" : {
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
"owner": "lszomoru",
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' });
@@ -484,8 +553,9 @@ export class CommandCenter {
/* __GDPR__
"clone" : {
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }
"owner": "lszomoru",
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" },
"openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true, "comment": "Indicates whether the folder is opened following the clone operation" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: action === PostCloneAction.Open || action === PostCloneAction.OpenNewWindow ? 1 : 0 });
@@ -503,7 +573,8 @@ export class CommandCenter {
if (/already exists and is not an empty directory/.test(err && err.stderr || '')) {
/* __GDPR__
"clone" : {
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
"owner": "lszomoru",
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' });
@@ -512,7 +583,8 @@ export class CommandCenter {
} else {
/* __GDPR__
"clone" : {
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
"owner": "lszomoru",
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The outcome of the git operation" }
}
*/
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' });
@@ -820,14 +892,14 @@ export class CommandCenter {
@command('git.stage')
async stage(...resourceStates: SourceControlResourceState[]): Promise<void> {
this.outputChannel.appendLine(`${logTimestamp()} git.stage ${resourceStates.length}`);
this.outputChannelLogger.logDebug(`git.stage ${resourceStates.length} `);
resourceStates = resourceStates.filter(s => !!s);
if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
const resource = this.getSCMResource();
this.outputChannel.appendLine(`${logTimestamp()} git.stage.getSCMResource ${resource ? resource.resourceUri.toString() : null}`);
this.outputChannelLogger.logDebug(`git.stage.getSCMResource ${resource ? resource.resourceUri.toString() : null} `);
if (!resource) {
return;
@@ -870,7 +942,7 @@ export class CommandCenter {
const untracked = selection.filter(s => s.resourceGroupType === ResourceGroupType.Untracked);
const scmResources = [...workingTree, ...untracked, ...resolved, ...unresolved];
this.outputChannel.appendLine(`${logTimestamp()} git.stage.scmResources ${scmResources.length}`);
this.outputChannelLogger.logDebug(`git.stage.scmResources ${scmResources.length} `);
if (!scmResources.length) {
return;
}
@@ -1020,6 +1092,47 @@ export class CommandCenter {
await this._stageChanges(textEditor, selectedChanges);
}
@command('git.acceptMerge')
async acceptMerge(uri: Uri | unknown): Promise<void> {
if (!(uri instanceof Uri)) {
return;
}
const repository = this.model.getRepository(uri);
if (!repository) {
console.log(`FAILED to accept merge because uri ${uri.toString()} doesn't belong to any repository`);
return;
}
const { activeTab } = window.tabGroups.activeTabGroup;
if (!activeTab) {
return;
}
// make sure to save the merged document
const doc = workspace.textDocuments.find(doc => doc.uri.toString() === uri.toString());
if (!doc) {
console.log(`FAILED to accept merge because uri ${uri.toString()} doesn't match a document`);
return;
}
if (doc.isDirty) {
await doc.save();
}
// find the merge editor tabs for the resource in question and close them all
let didCloseTab = false;
const mergeEditorTabs = window.tabGroups.all.map(group => group.tabs.filter(tab => tab.input instanceof TabInputTextMerge && tab.input.result.toString() === uri.toString())).flat();
if (mergeEditorTabs.includes(activeTab)) {
didCloseTab = await window.tabGroups.close(mergeEditorTabs, true);
}
// Only stage if the merge editor has been successfully closed. That means all conflicts have been
// handled or unhandled conflicts are OK by the user.
if (didCloseTab) {
await repository.add([uri]);
await commands.executeCommand('workbench.view.scm');
}
}
private async _stageChanges(textEditor: TextEditor, changes: LineChange[]): Promise<void> {
const modifiedDocument = textEditor.document;
const modifiedUri = modifiedDocument.uri;
@@ -1339,7 +1452,7 @@ export class CommandCenter {
private async smartCommit(
repository: Repository,
getCommitMessage: () => Promise<string | undefined>,
opts?: CommitOptions
opts: CommitOptions
): Promise<boolean> {
const config = workspace.getConfiguration('git', Uri.file(repository.root));
let promptToSaveFilesBeforeCommit = config.get<'always' | 'staged' | 'never'>('promptToSaveFilesBeforeCommit');
@@ -1385,14 +1498,8 @@ export class CommandCenter {
}
}
if (!opts) {
opts = { all: noStagedChanges };
} else if (!opts.all && noStagedChanges && !opts.empty) {
opts = { ...opts, all: true };
}
// no changes, and the user has not configured to commit all in this case
if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit && !opts.empty) {
if (!noUnstagedChanges && noStagedChanges && !enableSmartCommit && !opts.empty && !opts.all) {
const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;
if (!suggestSmartCommit) {
@@ -1416,6 +1523,12 @@ export class CommandCenter {
}
}
if (opts.all === undefined) {
opts = { ...opts, all: noStagedChanges };
} else if (!opts.all && noStagedChanges && !opts.empty) {
opts = { ...opts, all: true };
}
// enable signing of commits if configured
opts.signCommit = enableCommitSigning;
@@ -1423,6 +1536,14 @@ export class CommandCenter {
opts.signoff = true;
}
if (config.get<boolean>('useEditorAsCommitInput')) {
opts.useEditor = true;
if (config.get<boolean>('verboseCommit')) {
opts.verbose = true;
}
}
const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges');
if (
@@ -1437,6 +1558,8 @@ export class CommandCenter {
// amend allows changing only the commit message
&& !opts.amend
&& !opts.empty
// rebase not in progress
&& repository.rebaseCommit === undefined
) {
const commitAnyway = localize('commit anyway', "Create Empty Commit");
const answer = await window.showInformationMessage(localize('no changes', "There are no changes to commit."), commitAnyway);
@@ -1468,9 +1591,9 @@ export class CommandCenter {
}
}
let message = await getCommitMessage();
const message = await getCommitMessage();
if (!message && !opts.amend) {
if (!message && !opts.amend && !opts.useEditor) {
return false;
}
@@ -1482,29 +1605,55 @@ export class CommandCenter {
opts.all = 'tracked';
}
// Branch protection
const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!;
if (repository.isBranchProtected() && (branchProtectionPrompt === 'alwaysPrompt' || branchProtectionPrompt === 'alwaysCommitToNewBranch')) {
const commitToNewBranch = localize('commit to branch', "Commit to a New Branch");
let pick: string | undefined = commitToNewBranch;
if (branchProtectionPrompt === 'alwaysPrompt') {
const message = localize('confirm branch protection commit', "You are trying to commit to a protected branch and you might not have permission to push your commits to the remote.\n\nHow would you like to proceed?");
const commit = localize('commit changes', "Commit Anyway");
pick = await window.showWarningMessage(message, { modal: true }, commitToNewBranch, commit);
}
if (!pick) {
return false;
} else if (pick === commitToNewBranch) {
const branchName = await this.promptForBranchName(repository);
if (!branchName) {
return false;
}
await repository.branch(branchName, true);
}
}
await repository.commit(message, opts);
const postCommitCommand = config.get<'none' | 'push' | 'sync'>('postCommitCommand');
switch (postCommitCommand) {
case 'push':
await this._push(repository, { pushType: PushType.Push, silent: true });
break;
case 'sync':
await this.sync(repository);
break;
// Execute post commit command
if (opts.postCommitCommand?.length) {
await commands.executeCommand(
opts.postCommitCommand,
new ApiRepository(repository));
}
return true;
}
private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
private async commitWithAnyInput(repository: Repository, opts: CommitOptions): Promise<void> {
const message = repository.inputBox.value;
const root = Uri.file(repository.root);
const config = workspace.getConfiguration('git', root);
const getCommitMessage = async () => {
let _message: string | undefined = message;
if (!_message) {
let value: string | undefined = undefined;
if (!_message && !config.get<boolean>('useEditorAsCommitInput')) {
const value: string | undefined = undefined;
if (opts && opts.amend && repository.HEAD && repository.HEAD.commit) {
return undefined;
@@ -1538,8 +1687,8 @@ export class CommandCenter {
}
@command('git.commit', { repository: true })
async commit(repository: Repository): Promise<void> {
await this.commitWithAnyInput(repository);
async commit(repository: Repository, postCommitCommand?: string): Promise<void> {
await this.commitWithAnyInput(repository, { postCommitCommand });
}
@command('git.commitStaged', { repository: true })
@@ -1572,13 +1721,58 @@ export class CommandCenter {
await this.commitWithAnyInput(repository, { all: true, amend: true });
}
@command('git.commitMessageAccept')
async commitMessageAccept(arg?: Uri): Promise<void> {
if (!arg) { return; }
// Close the tab
this._closeEditorTab(arg);
}
@command('git.commitMessageDiscard')
async commitMessageDiscard(arg?: Uri): Promise<void> {
if (!arg) { return; }
// Clear the contents of the editor
const editors = window.visibleTextEditors
.filter(e => e.document.languageId === 'git-commit' && e.document.uri.toString() === arg.toString());
if (editors.length !== 1) { return; }
const commitMsgEditor = editors[0];
const commitMsgDocument = commitMsgEditor.document;
const editResult = await commitMsgEditor.edit(builder => {
const firstLine = commitMsgDocument.lineAt(0);
const lastLine = commitMsgDocument.lineAt(commitMsgDocument.lineCount - 1);
builder.delete(new Range(firstLine.range.start, lastLine.range.end));
});
if (!editResult) { return; }
// Save the document
const saveResult = await commitMsgDocument.save();
if (!saveResult) { return; }
// Close the tab
this._closeEditorTab(arg);
}
private _closeEditorTab(uri: Uri): void {
const tabToClose = window.tabGroups.all.map(g => g.tabs).flat()
.filter(t => t.input instanceof TabInputText && t.input.uri.toString() === uri.toString());
window.tabGroups.close(tabToClose);
}
private async _commitEmpty(repository: Repository, noVerify?: boolean): Promise<void> {
const root = Uri.file(repository.root);
const config = workspace.getConfiguration('git', root);
const shouldPrompt = config.get<boolean>('confirmEmptyCommits') === true;
if (shouldPrompt) {
const message = localize('confirm emtpy commit', "Are you sure you want to create an empty commit?");
const message = localize('confirm empty commit', "Are you sure you want to create an empty commit?");
const yes = localize('yes', "Yes");
const neverAgain = localize('yes never again', "Yes, Don't Show Again");
const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);
@@ -1725,7 +1919,7 @@ export class CommandCenter {
const item = choice as CheckoutItem;
try {
await item.run(repository, opts);
await item.run(opts);
} catch (err) {
if (err.gitErrorCode !== GitErrorCodes.DirtyWorkTree) {
throw err;
@@ -1737,10 +1931,10 @@ export class CommandCenter {
if (choice === force) {
await this.cleanAll(repository);
await item.run(repository, opts);
await item.run(opts);
} else if (choice === stash) {
await this.stash(repository);
await item.run(repository, opts);
await item.run(opts);
await this.stashPopLatest(repository);
}
}
@@ -1759,34 +1953,100 @@ export class CommandCenter {
await this._branch(repository, undefined, true);
}
private async promptForBranchName(defaultName?: string, initialValue?: string): Promise<string> {
private generateRandomBranchName(repository: Repository, separator: string): string {
const config = workspace.getConfiguration('git');
const branchRandomNameDictionary = config.get<string[]>('branchRandomName.dictionary')!;
const dictionaries: string[][] = [];
for (const dictionary of branchRandomNameDictionary) {
if (dictionary.toLowerCase() === 'adjectives') {
dictionaries.push(adjectives);
}
if (dictionary.toLowerCase() === 'animals') {
dictionaries.push(animals);
}
if (dictionary.toLowerCase() === 'colors') {
dictionaries.push(colors);
}
if (dictionary.toLowerCase() === 'numbers') {
dictionaries.push(NumberDictionary.generate({ length: 3 }));
}
}
if (dictionaries.length === 0) {
return '';
}
// 5 attempts to generate a random branch name
for (let index = 0; index < 5; index++) {
const randomName = uniqueNamesGenerator({
dictionaries,
length: dictionaries.length,
separator
});
// Check for local ref conflict
if (!repository.refs.find(r => r.type === RefType.Head && r.name === randomName)) {
return randomName;
}
}
return '';
}
private async promptForBranchName(repository: Repository, defaultName?: string, initialValue?: string): Promise<string> {
const config = workspace.getConfiguration('git');
const branchPrefix = config.get<string>('branchPrefix')!;
const branchWhitespaceChar = config.get<string>('branchWhitespaceChar')!;
const branchValidationRegex = config.get<string>('branchValidationRegex')!;
const sanitize = (name: string) => name ?
name.trim().replace(/^-+/, '').replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, branchWhitespaceChar)
: name;
const rawBranchName = defaultName || await window.showInputBox({
placeHolder: localize('branch name', "Branch name"),
prompt: localize('provide branch name', "Please provide a new branch name"),
value: initialValue,
ignoreFocusOut: true,
validateInput: (name: string) => {
const validateName = new RegExp(branchValidationRegex);
if (validateName.test(sanitize(name))) {
return null;
}
let rawBranchName = defaultName;
return localize('branch name format invalid', "Branch name needs to match regex: {0}", branchValidationRegex);
if (!rawBranchName) {
// Branch name
if (!initialValue) {
const branchRandomNameEnabled = config.get<boolean>('branchRandomName.enable', false);
initialValue = `${branchPrefix}${branchRandomNameEnabled ? this.generateRandomBranchName(repository, branchWhitespaceChar) : ''}`;
}
});
// Branch name selection
const initialValueSelection: [number, number] | undefined =
initialValue.startsWith(branchPrefix) ? [branchPrefix.length, initialValue.length] : undefined;
rawBranchName = await window.showInputBox({
placeHolder: localize('branch name', "Branch name"),
prompt: localize('provide branch name', "Please provide a new branch name"),
value: initialValue,
valueSelection: initialValueSelection,
ignoreFocusOut: true,
validateInput: (name: string) => {
const validateName = new RegExp(branchValidationRegex);
const sanitizedName = sanitize(name);
if (validateName.test(sanitizedName)) {
// If the sanitized name that we will use is different than what is
// in the input box, show an info message to the user informing them
// the branch name that will be used.
return name === sanitizedName
? null
: {
message: localize('branch name does not match sanitized', "The new branch will be '{0}'", sanitizedName),
severity: InputBoxValidationSeverity.Info
};
}
return localize('branch name format invalid', "Branch name needs to match regex: {0}", branchValidationRegex);
}
});
}
return sanitize(rawBranchName || '');
}
private async _branch(repository: Repository, defaultName?: string, from = false): Promise<void> {
const branchName = await this.promptForBranchName(defaultName);
const branchName = await this.promptForBranchName(repository, defaultName);
if (!branchName) {
return;
@@ -1803,7 +2063,9 @@ export class CommandCenter {
return;
}
target = choice.label;
if (choice.refName) {
target = choice.refName;
}
}
await repository.branch(branchName, true, target);
@@ -1849,7 +2111,7 @@ export class CommandCenter {
@command('git.renameBranch', { repository: true })
async renameBranch(repository: Repository): Promise<void> {
const currentBranchName = repository.HEAD && repository.HEAD.name;
const branchName = await this.promptForBranchName(undefined, currentBranchName);
const branchName = await this.promptForBranchName(repository, undefined, currentBranchName);
if (!branchName) {
return;
@@ -2315,17 +2577,16 @@ export class CommandCenter {
}
}
if (rebase) {
await repository.syncRebase(HEAD);
} else {
await repository.sync(HEAD);
}
await repository.sync(HEAD, rebase);
}
@command('git.sync', { repository: true })
async sync(repository: Repository): Promise<void> {
const config = workspace.getConfiguration('git', Uri.file(repository.root));
const rebase = config.get<boolean>('rebaseWhenSync', false) === true;
try {
await this._sync(repository, false);
await this._sync(repository, rebase);
} catch (err) {
if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
return;
@@ -2338,13 +2599,16 @@ export class CommandCenter {
@command('git._syncAll')
async syncAll(): Promise<void> {
await Promise.all(this.model.repositories.map(async repository => {
const config = workspace.getConfiguration('git', Uri.file(repository.root));
const rebase = config.get<boolean>('rebaseWhenSync', false) === true;
const HEAD = repository.HEAD;
if (!HEAD || !HEAD.upstream) {
return;
}
await repository.sync(HEAD);
await repository.sync(HEAD, rebase);
}));
}
@@ -2466,6 +2730,21 @@ export class CommandCenter {
await commands.executeCommand('revealInExplorer', resourceState.resourceUri);
}
@command('git.revealFileInOS.linux')
@command('git.revealFileInOS.mac')
@command('git.revealFileInOS.windows')
async revealFileInOS(resourceState: SourceControlResourceState): Promise<void> {
if (!resourceState) {
return;
}
if (!(resourceState.resourceUri instanceof Uri)) {
return;
}
await commands.executeCommand('revealFileInOS', resourceState.resourceUri);
}
private async _stash(repository: Repository, includeUntracked = false): Promise<void> {
const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0
&& (!includeUntracked || repository.untrackedGroup.resourceStates.length === 0);
@@ -2755,13 +3034,7 @@ export class CommandCenter {
@command('git.closeAllDiffEditors', { repository: true })
closeDiffEditors(repository: Repository): void {
const resources = [
...repository.indexGroup.resourceStates.map(r => r.resourceUri.fsPath),
...repository.workingTreeGroup.resourceStates.map(r => r.resourceUri.fsPath),
...repository.untrackedGroup.resourceStates.map(r => r.resourceUri.fsPath)
];
repository.closeDiffEditors(resources, resources, true);
repository.closeDiffEditors(undefined, undefined, true);
}
private createCommand(id: string, key: string, method: Function, options: ScmCommandOptions): (...args: any[]) => any {
@@ -2794,7 +3067,8 @@ export class CommandCenter {
/* __GDPR__
"git.command" : {
"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
"owner": "lszomoru",
"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "The command id of the command being executed" }
}
*/
this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });
@@ -2805,12 +3079,12 @@ export class CommandCenter {
};
let message: string;
let type: 'error' | 'warning' = 'error';
let type: 'error' | 'warning' | 'information' = 'error';
const choices = new Map<string, () => void>();
const openOutputChannelChoice = localize('open git log', "Open Git Log");
const outputChannel = this.outputChannel as OutputChannel;
choices.set(openOutputChannelChoice, () => outputChannel.show());
const outputChannelLogger = this.outputChannelLogger;
choices.set(openOutputChannelChoice, () => outputChannelLogger.showOutputChannel());
const showCommandOutputChoice = localize('show command output', "Show Command Output");
if (err.stderr) {
@@ -2868,6 +3142,12 @@ export class CommandCenter {
message = localize('missing user info', "Make sure you configure your 'user.name' and 'user.email' in git.");
choices.set(localize('learn more', "Learn More"), () => commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-setup-git')));
break;
case GitErrorCodes.EmptyCommitMessage:
message = localize('empty commit', "Commit operation was cancelled due to empty commit message.");
choices.clear();
type = 'information';
options.modal = false;
break;
default: {
const hint = (err.stderr || err.message || String(err))
.replace(/^error: /mi, '')
@@ -2889,17 +3169,25 @@ export class CommandCenter {
return;
}
let result: string | undefined;
const allChoices = Array.from(choices.keys());
const result = type === 'error'
? await window.showErrorMessage(message, options, ...allChoices)
: await window.showWarningMessage(message, options, ...allChoices);
switch (type) {
case 'error':
result = await window.showErrorMessage(message, options, ...allChoices);
break;
case 'warning':
result = await window.showWarningMessage(message, options, ...allChoices);
break;
case 'information':
result = await window.showInformationMessage(message, options, ...allChoices);
break;
}
if (result) {
const resultFn = choices.get(result);
if (resultFn) {
resultFn();
}
resultFn?.();
}
});
};
@@ -2913,10 +3201,10 @@ export class CommandCenter {
private getSCMResource(uri?: Uri): Resource | undefined {
uri = uri ? uri : (window.activeTextEditor && window.activeTextEditor.document.uri);
this.outputChannel.appendLine(`${logTimestamp()} git.getSCMResource.uri ${uri && uri.toString()}`);
this.outputChannelLogger.logDebug(`git.getSCMResource.uri ${uri && uri.toString()}`);
for (const r of this.model.repositories.map(r => r.root)) {
this.outputChannel.appendLine(`${logTimestamp()} repo root ${r}`);
this.outputChannelLogger.logDebug(`repo root ${r}`);
}
if (!uri) {

View File

@@ -106,7 +106,7 @@ class GitDecorationProvider implements FileDecorationProvider {
}
private onDidRunGitStatus(): void {
let newDecorations = new Map<string, FileDecoration>();
const newDecorations = new Map<string, FileDecoration>();
this.collectSubmoduleDecorationData(newDecorations);
this.collectDecorationData(this.repository.indexGroup, newDecorations);

View File

@@ -50,7 +50,7 @@ const JSCHARDET_TO_ICONV_ENCODINGS: { [name: string]: string } = {
};
export function detectEncoding(buffer: Buffer): string | null {
let result = detectEncodingByBOM(buffer);
const result = detectEncodingByBOM(buffer);
if (result) {
return result;

View File

@@ -0,0 +1 @@
#!/bin/sh

View File

@@ -0,0 +1,21 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IPCClient } from './ipc/ipcClient';
function fatal(err: any): void {
console.error(err);
process.exit(1);
}
function main(argv: string[]): void {
const ipcClient = new IPCClient('git-editor');
const commitMessagePath = argv[argv.length - 1];
ipcClient.call({ commitMessagePath }).then(() => {
setTimeout(() => process.exit(0), 0);
}).catch(err => fatal(err));
}
main(process.argv);

View File

@@ -0,0 +1,4 @@
#!/bin/sh
ELECTRON_RUN_AS_NODE="1" \
"$VSCODE_GIT_EDITOR_NODE" "$VSCODE_GIT_EDITOR_MAIN" $VSCODE_GIT_EDITOR_EXTRA_ARGS "$@"

View File

@@ -354,7 +354,7 @@ function sanitizePath(path: string): string {
return path.replace(/^([a-z]):\\/i, (_, letter) => `${letter.toUpperCase()}:\\`);
}
const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%B';
const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B';
/*export interface ICloneOptions { {{SQL CARBON EDIT}} moved to git.d.ts
readonly parentPath: string;
@@ -406,7 +406,7 @@ export class Git {
}
async clone(url: string, options: ICloneOptions, cancellationToken?: CancellationToken): Promise<string> {
let baseFolderName = decodeURI(url).replace(/[\/]+$/, '').replace(/^.*[\/\\]/, '').replace(/\.git$/, '') || 'repository';
const baseFolderName = decodeURI(url).replace(/[\/]+$/, '').replace(/^.*[\/\\]/, '').replace(/\.git$/, '') || 'repository';
let folderName = baseFolderName;
let folderPath = path.join(options.parentPath, folderName);
let count = 1;
@@ -447,7 +447,7 @@ export class Git {
};
try {
let command = ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath, '--progress'];
const command = ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath, '--progress'];
if (options.recursive) {
command.push('--recursive');
}
@@ -475,13 +475,14 @@ export class Git {
const repoPath = path.normalize(result.stdout.trimLeft().replace(/[\r\n]+$/, ''));
if (isWindows) {
// On Git 2.25+ if you call `rev-parse --show-toplevel` on a mapped drive, instead of getting the mapped drive path back, you get the UNC path for the mapped drive.
// So we will try to normalize it back to the mapped drive path, if possible
// On Git 2.25+ if you call `rev-parse --show-toplevel` on a mapped drive, instead of getting the mapped
// drive path back, you get the UNC path for the mapped drive. So we will try to normalize it back to the
// mapped drive path, if possible
const repoUri = Uri.file(repoPath);
const pathUri = Uri.file(repositoryPath);
if (repoUri.authority.length !== 0 && pathUri.authority.length === 0) {
// eslint-disable-next-line code-no-look-behind-regex
let match = /(?<=^\/?)([a-zA-Z])(?=:\/)/.exec(pathUri.path);
const match = /(?<=^\/?)([a-zA-Z])(?=:\/)/.exec(pathUri.path);
if (match !== null) {
const [, letter] = match;
@@ -504,6 +505,13 @@ export class Git {
return path.normalize(pathUri.fsPath);
}
// On Windows, there are cases in which the normalized path for a mapped folder contains a trailing `\`
// character (ex: \\server\folder\) due to the implementation of `path.normalize()`. This behaviour is
// by design as documented in https://github.com/nodejs/node/issues/1765.
if (repoUri.authority.length !== 0) {
return repoPath.replace(/\\$/, '');
}
}
return repoPath;
@@ -556,9 +564,7 @@ export class Git {
private async _exec(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
const child = this.spawn(args, options);
if (options.onSpawn) {
options.onSpawn(child);
}
options.onSpawn?.(child);
if (options.input) {
child.stdin!.end(options.input, 'utf8');
@@ -660,6 +666,7 @@ export interface Commit {
authorName?: string;
authorEmail?: string;
commitDate?: Date;
refNames: string[];
}
export class GitStatusParser {
@@ -790,10 +797,10 @@ export function parseGitmodules(raw: string): Submodule[] {
return result;
}
const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm;
const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm;
export function parseGitCommits(data: string): Commit[] {
let commits: Commit[] = [];
const commits: Commit[] = [];
let ref;
let authorName;
@@ -801,6 +808,7 @@ export function parseGitCommits(data: string): Commit[] {
let authorDate;
let commitDate;
let parents;
let refNames;
let message;
let match;
@@ -810,7 +818,7 @@ export function parseGitCommits(data: string): Commit[] {
break;
}
[, ref, authorName, authorEmail, authorDate, commitDate, parents, message] = match;
[, ref, authorName, authorEmail, authorDate, commitDate, parents, refNames, message] = match;
if (message[message.length - 1] === '\n') {
message = message.substr(0, message.length - 1);
@@ -825,6 +833,7 @@ export function parseGitCommits(data: string): Commit[] {
authorName: ` ${authorName}`.substr(1),
authorEmail: ` ${authorEmail}`.substr(1),
commitDate: new Date(Number(commitDate) * 1000),
refNames: refNames.split(',').map(s => s.trim())
});
} while (true);
@@ -1397,20 +1406,37 @@ export class Repository {
}
async commit(message: string | undefined, opts: CommitOptions = Object.create(null)): Promise<void> {
const args = ['commit', '--quiet', '--allow-empty-message'];
const args = ['commit', '--quiet'];
const options: SpawnOptions = {};
if (message) {
options.input = message;
args.push('--allow-empty-message', '--file', '-');
}
if (opts.verbose) {
args.push('--verbose');
}
if (opts.all) {
args.push('--all');
}
if (opts.amend && message) {
if (opts.amend) {
args.push('--amend');
}
if (opts.amend && !message) {
args.push('--amend', '--no-edit');
} else {
args.push('--file', '-');
if (!opts.useEditor) {
if (!message) {
if (opts.amend) {
args.push('--no-edit');
} else {
options.input = '';
args.push('--file', '-');
}
}
args.push('--allow-empty-message');
}
if (opts.signoff) {
@@ -1435,7 +1461,7 @@ export class Repository {
}
try {
await this.exec(args, !opts.amend || message ? { input: message || '' } : {});
await this.exec(args, options);
} catch (commitErr) {
await this.handleCommitError(commitErr);
}
@@ -1449,7 +1475,7 @@ export class Repository {
const args = ['rebase', '--continue'];
try {
await this.exec(args);
await this.exec(args, { env: { GIT_EDITOR: 'true' } });
} catch (commitErr) {
await this.handleCommitError(commitErr);
}
@@ -1459,6 +1485,9 @@ export class Repository {
if (/not possible because you have unmerged files/.test(commitErr.stderr || '')) {
commitErr.gitErrorCode = GitErrorCodes.UnmergedChanges;
throw commitErr;
} else if (/Aborting commit due to empty commit message/.test(commitErr.stderr || '')) {
commitErr.gitErrorCode = GitErrorCodes.EmptyCommitMessage;
throw commitErr;
}
try {
@@ -1540,7 +1569,7 @@ export class Repository {
}
async deleteTag(name: string): Promise<void> {
let args = ['tag', '-d', name];
const args = ['tag', '-d', name];
await this.exec(args);
}
@@ -1764,12 +1793,12 @@ export class Repository {
} catch (err) {
if (/^error: failed to push some refs to\b/m.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.PushRejected;
} else if (/Permission.*denied/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.PermissionDenied;
} else if (/Could not read from remote repository/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
} else if (/^fatal: The current branch .* has no upstream branch/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.NoUpstreamBranch;
} else if (/Permission.*denied/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.PermissionDenied;
}
throw err;

View File

@@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { TabInputText, Uri, window, workspace } from 'vscode';
import { IIPCHandler, IIPCServer } from './ipc/ipcServer';
import { ITerminalEnvironmentProvider } from './terminal';
import { EmptyDisposable, IDisposable } from './util';
interface GitEditorRequest {
commitMessagePath?: string;
}
export class GitEditor implements IIPCHandler, ITerminalEnvironmentProvider {
private env: { [key: string]: string };
private disposable: IDisposable = EmptyDisposable;
constructor(ipc?: IIPCServer) {
if (ipc) {
this.disposable = ipc.registerHandler('git-editor', this);
}
this.env = {
GIT_EDITOR: `"${path.join(__dirname, ipc ? 'git-editor.sh' : 'git-editor-empty.sh')}"`,
VSCODE_GIT_EDITOR_NODE: process.execPath,
VSCODE_GIT_EDITOR_EXTRA_ARGS: (process.versions['electron'] && process.versions['microsoft-build']) ? '--ms-enable-electron-run-as-node' : '',
VSCODE_GIT_EDITOR_MAIN: path.join(__dirname, 'git-editor-main.js')
};
}
async handle({ commitMessagePath }: GitEditorRequest): Promise<any> {
if (commitMessagePath) {
const uri = Uri.file(commitMessagePath);
const doc = await workspace.openTextDocument(uri);
await window.showTextDocument(doc, { preview: false });
return new Promise((c) => {
const onDidClose = window.tabGroups.onDidChangeTabs(async (tabs) => {
if (tabs.closed.some(t => t.input instanceof TabInputText && t.input.uri.toString() === uri.toString())) {
onDidClose.dispose();
return c(true);
}
});
});
}
}
getEnv(): { [key: string]: string } {
const config = workspace.getConfiguration('git');
return config.get<boolean>('useEditorAsCommitInput') ? this.env : {};
}
getTerminalEnv(): { [key: string]: string } {
const config = workspace.getConfiguration('git');
return config.get<boolean>('useEditorAsCommitInput') && config.get<boolean>('terminalGitEditor') ? this.env : {};
}
dispose(): void {
this.disposable.dispose();
}
}

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vscode';
import { ITerminalEnvironmentProvider } from '../terminal';
import { toDisposable } from '../util';
import * as path from 'path';
import * as http from 'http';
@@ -27,7 +28,7 @@ export interface IIPCHandler {
handle(request: any): Promise<any>;
}
export async function createIPCServer(context?: string): Promise<IIPCServer> {
export async function createIPCServer(context?: string): Promise<IPCServer> {
const server = http.createServer();
const hash = crypto.createHash('sha1');
@@ -65,7 +66,7 @@ export interface IIPCServer extends Disposable {
registerHandler(name: string, handler: IIPCHandler): Disposable;
}
class IPCServer implements IIPCServer, Disposable {
export class IPCServer implements IIPCServer, ITerminalEnvironmentProvider, Disposable {
private handlers = new Map<string, IIPCHandler>();
get ipcHandlePath(): string { return this._ipcHandlePath; }
@@ -110,6 +111,10 @@ class IPCServer implements IIPCServer, Disposable {
return { VSCODE_GIT_IPC_HANDLE: this.ipcHandlePath };
}
getTerminalEnv(): { [key: string]: string } {
return { VSCODE_GIT_IPC_HANDLE: this.ipcHandlePath };
}
dispose(): void {
this.handlers.clear();
this.server.close();

View File

@@ -3,7 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, EventEmitter } from 'vscode';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { commands, Disposable, Event, EventEmitter, OutputChannel, window, workspace } from 'vscode';
import { dispose } from './util';
/**
* The severity level of a log message
@@ -18,33 +22,94 @@ export enum LogLevel {
Off = 7
}
let _logLevel: LogLevel = LogLevel.Info;
const _onDidChangeLogLevel = new EventEmitter<LogLevel>();
/**
* Output channel logger
*/
export class OutputChannelLogger {
export const Log = {
/**
* Current logging level.
*/
get logLevel(): LogLevel {
return _logLevel;
},
private _onDidChangeLogLevel = new EventEmitter<LogLevel>();
readonly onDidChangeLogLevel: Event<LogLevel> = this._onDidChangeLogLevel.event;
/**
* Current logging level.
*/
set logLevel(logLevel: LogLevel) {
if (_logLevel === logLevel) {
private _currentLogLevel!: LogLevel;
get currentLogLevel(): LogLevel {
return this._currentLogLevel;
}
set currentLogLevel(value: LogLevel) {
if (this._currentLogLevel === value) {
return;
}
_logLevel = logLevel;
_onDidChangeLogLevel.fire(logLevel);
},
this._currentLogLevel = value;
this._onDidChangeLogLevel.fire(value);
/**
* An [event](#Event) that fires when the log level has changed.
*/
get onDidChangeLogLevel(): Event<LogLevel> {
return _onDidChangeLogLevel.event;
this.log(localize('gitLogLevel', "Log level: {0}", LogLevel[value]));
}
};
private _defaultLogLevel!: LogLevel;
get defaultLogLevel(): LogLevel {
return this._defaultLogLevel;
}
private _outputChannel: OutputChannel;
private _disposables: Disposable[] = [];
constructor() {
// Output channel
this._outputChannel = window.createOutputChannel('Git');
commands.registerCommand('git.showOutput', () => this.showOutputChannel());
this._disposables.push(this._outputChannel);
this._disposables.push(workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('git.logLevel')) {
this.onLogLevelChange();
}
}));
this.onLogLevelChange();
}
private onLogLevelChange(): void {
const config = workspace.getConfiguration('git');
const logLevel: keyof typeof LogLevel = config.get('logLevel', 'Info');
this.currentLogLevel = this._defaultLogLevel = LogLevel[logLevel] ?? LogLevel.Info;
}
log(message: string, logLevel?: LogLevel): void {
if (logLevel && logLevel < this._currentLogLevel) {
return;
}
this._outputChannel.appendLine(`[${new Date().toISOString()}]${logLevel ? ` [${LogLevel[logLevel].toLowerCase()}]` : ''} ${message}`);
}
logCritical(message: string): void {
this.log(message, LogLevel.Critical);
}
logDebug(message: string): void {
this.log(message, LogLevel.Debug);
}
logError(message: string): void {
this.log(message, LogLevel.Error);
}
logInfo(message: string): void {
this.log(message, LogLevel.Info);
}
logTrace(message: string): void {
this.log(message, LogLevel.Trace);
}
logWarning(message: string): void {
this.log(message, LogLevel.Warning);
}
showOutputChannel(): void {
this._outputChannel.show();
}
dispose(): void {
this._disposables = dispose(this._disposables);
}
}

View File

@@ -6,14 +6,14 @@
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ExtensionContext, workspace, window, Disposable, commands, OutputChannel } from 'vscode';
import { ExtensionContext, workspace, Disposable, commands } from 'vscode';
import { findGit, Git } 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, logTimestamp } from './util';
import { toDisposable, filterEvent, eventToPromise } from './util';
import TelemetryReporter from '@vscode/extension-telemetry';
import { GitExtension } from './api/git';
import { GitProtocolHandler } from './protocolHandler';
@@ -24,6 +24,9 @@ import * as os from 'os';
import { GitTimelineProvider } from './timelineProvider';
import { registerAPICommands } from './api/api1';
import { TerminalEnvironmentManager } from './terminal';
import { OutputChannelLogger } from './log';
import { createIPCServer, IPCServer } from './ipc/ipcServer';
import { GitEditor } from './gitEditor';
const deactivateTasks: { (): Promise<any> }[] = [];
@@ -33,7 +36,7 @@ export async function deactivate(): Promise<any> {
}
}
async function createModel(context: ExtensionContext, outputChannel: OutputChannel, telemetryReporter: TelemetryReporter, disposables: Disposable[]): Promise<Model> {
async function createModel(context: ExtensionContext, outputChannelLogger: OutputChannelLogger, telemetryReporter: TelemetryReporter, disposables: Disposable[]): Promise<Model> {
const pathValue = workspace.getConfiguration('git').get<string | string[]>('path');
let pathHints = Array.isArray(pathValue) ? pathValue : pathValue ? [pathValue] : [];
@@ -46,7 +49,7 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann
}
const info = await findGit(pathHints, gitPath => {
outputChannel.appendLine(localize('validating', "{0} Validating found git in: {1}", logTimestamp(), gitPath));
outputChannelLogger.logInfo(localize('validating', "Validating found git in: {0}", gitPath));
if (excludes.length === 0) {
return true;
}
@@ -54,18 +57,30 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann
const normalized = path.normalize(gitPath).replace(/[\r\n]+$/, '');
const skip = excludes.some(e => normalized.startsWith(e));
if (skip) {
outputChannel.appendLine(localize('skipped', "{0} Skipped found git in: {1}", logTimestamp(), gitPath));
outputChannelLogger.logInfo(localize('skipped', "Skipped found git in: {0}", gitPath));
}
return !skip;
});
const askpass = await Askpass.create(outputChannel, context.storagePath);
let ipcServer: IPCServer | undefined = undefined;
try {
ipcServer = await createIPCServer(context.storagePath);
} catch (err) {
outputChannelLogger.logError(`Failed to create git IPC: ${err}`);
}
const askpass = new Askpass(ipcServer);
disposables.push(askpass);
const environment = askpass.getEnv();
const terminalEnvironmentManager = new TerminalEnvironmentManager(context, environment);
const gitEditor = new GitEditor(ipcServer);
disposables.push(gitEditor);
const environment = { ...askpass.getEnv(), ...gitEditor.getEnv(), ...ipcServer?.getEnv() };
const terminalEnvironmentManager = new TerminalEnvironmentManager(context, [askpass, gitEditor, ipcServer]);
disposables.push(terminalEnvironmentManager);
outputChannelLogger.logInfo(localize('using git', "Using git {0} from {1}", info.version, info.path));
const git = new Git({
gitPath: info.path,
@@ -73,7 +88,7 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann
version: info.version,
env: environment,
});
const model = new Model(git, askpass, context.globalState, outputChannel, telemetryReporter);
const model = new Model(git, askpass, context.globalState, outputChannelLogger, telemetryReporter);
disposables.push(model);
const onRepository = () => commands.executeCommand('setContext', 'gitOpenRepositoryCount', `${model.repositories.length}`);
@@ -81,8 +96,6 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann
model.onDidCloseRepository(onRepository, null, disposables);
onRepository();
outputChannel.appendLine(localize('using git', "{0} Using git {1} from {2}", logTimestamp(), info.version, info.path));
const onOutput = (str: string) => {
const lines = str.split(/\r?\n/mg);
@@ -90,12 +103,12 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann
lines.pop();
}
outputChannel.appendLine(`${logTimestamp()} ${lines.join('\n')}`);
outputChannelLogger.log(lines.join('\n'));
};
git.onOutput.addListener('log', onOutput);
disposables.push(toDisposable(() => git.onOutput.removeListener('log', onOutput)));
const cc = new CommandCenter(git, model, outputChannel, telemetryReporter);
const cc = new CommandCenter(git, model, outputChannelLogger, telemetryReporter);
disposables.push(
cc,
new GitFileSystemProvider(model),
@@ -104,6 +117,9 @@ async function createModel(context: ExtensionContext, outputChannel: OutputChann
new GitTimelineProvider(model, cc)
);
// const postCommitCommandsProvider = new GitPostCommitCommandsProvider(); {{SQL CARBON TODO}} lewissanchez - Do we need this?
// model.registerPostCommitCommandsProvider(postCommitCommandsProvider); {{SQL CARBON TODO}} lewissanchez - Do we need this?
// checkGitVersion(info); {{SQL CARBON EDIT}} Don't check git version
return model;
@@ -162,9 +178,8 @@ export async function _activate(context: ExtensionContext): Promise<GitExtension
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 outputChannelLogger = new OutputChannelLogger();
disposables.push(outputChannelLogger);
const { name, version, aiKey } = require('../package.json') as { name: string; version: string; aiKey: string };
const telemetryReporter = new TelemetryReporter(name, version, aiKey);
@@ -178,12 +193,12 @@ export async function _activate(context: ExtensionContext): Promise<GitExtension
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));
eventToPromise(onEnabled).then(async () => result.model = await createModel(context, outputChannelLogger, telemetryReporter, disposables));
return result;
}
try {
const model = await createModel(context, outputChannel, telemetryReporter, disposables);
const model = await createModel(context, outputChannelLogger, telemetryReporter, disposables);
return new GitExtensionImpl(model);
} catch (err) {
if (!/Git installation not found/.test(err.message || '')) {
@@ -194,7 +209,9 @@ export async function _activate(context: ExtensionContext): Promise<GitExtension
// outputChannel.appendLine(err.message); {{SQL CARBON EDIT}} turn-off Git missing prompt
/* __GDPR__
"git.missing" : {}
"git.missing" : {
"owner": "lszomoru"
}
*/
telemetryReporter.sendTelemetryEvent('git.missing');

View File

@@ -3,22 +3,23 @@
* Licensed under the Source EULA. 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 { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, commands } from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import { Repository, RepositoryState } from './repository';
import { memoize, sequentialize, debounce } from './decorators';
import { dispose, anyEvent, filterEvent, isDescendant, pathEquals, toDisposable, eventToPromise, logTimestamp } from './util';
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, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher } from './api/git';
import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider } from './api/git';
import { Askpass } from './askpass';
import { IPushErrorHandlerRegistry } from './pushError';
import { ApiRepository } from './api/api1';
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
import { Log, LogLevel } from './log';
import { OutputChannelLogger } from './log';
import { IPostCommitCommandsProviderRegistry } from './postCommitCommands';
const localize = nls.loadMessageBundle();
@@ -50,7 +51,7 @@ interface OpenRepository extends Disposable {
repository: Repository;
}
export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerRegistry {
export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry {
private _onDidOpenRepository = new EventEmitter<Repository>();
readonly onDidOpenRepository: Event<Repository> = this._onDidOpenRepository.event;
@@ -105,12 +106,17 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR
private _onDidRemoveRemoteSourcePublisher = new EventEmitter<RemoteSourcePublisher>();
readonly onDidRemoveRemoteSourcePublisher = this._onDidRemoveRemoteSourcePublisher.event;
private postCommitCommandsProviders = new Set<PostCommitCommandsProvider>();
private _onDidChangePostCommitCommandsProviders = new EventEmitter<void>();
readonly onDidChangePostCommitCommandsProviders = this._onDidChangePostCommitCommandsProviders.event;
private showRepoOnHomeDriveRootWarning = true;
private pushErrorHandlers = new Set<PushErrorHandler>();
private disposables: Disposable[] = [];
constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private outputChannel: OutputChannel, private telemetryReporter: TelemetryReporter) {
constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private outputChannelLogger: OutputChannelLogger, private telemetryReporter: TelemetryReporter) {
workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables);
window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables);
@@ -143,11 +149,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR
private async scanWorkspaceFolders(): Promise<void> {
const config = workspace.getConfiguration('git');
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
// Log repository scan settings
if (Log.logLevel <= LogLevel.Trace) {
this.outputChannel.appendLine(`${logTimestamp()} Trace: autoRepositoryDetection="${autoRepositoryDetection}"`);
}
this.outputChannelLogger.logTrace(`[swsf] Scan workspace sub folders. autoRepositoryDetection=${autoRepositoryDetection}`);
if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'subFolders') {
return;
@@ -155,6 +157,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR
await Promise.all((workspace.workspaceFolders || []).map(async folder => {
const root = folder.uri.fsPath;
this.outputChannelLogger.logTrace(`[swsf] Workspace folder: ${root}`);
// Workspace folder children
const repositoryScanMaxDepth = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get<number>('repositoryScanMaxDepth', 1);
@@ -164,19 +167,25 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR
// Repository scan folders
const scanPaths = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get<string[]>('scanRepositories') || [];
this.outputChannelLogger.logTrace(`[swsf] Workspace scan settings: repositoryScanMaxDepth=${repositoryScanMaxDepth}; repositoryScanIgnoredFolders=[${repositoryScanIgnoredFolders.join(', ')}]; scanRepositories=[${scanPaths.join(', ')}]`);
for (const scanPath of scanPaths) {
if (scanPath === '.git') {
this.outputChannelLogger.logTrace('[swsf] \'.git\' not supported in \'git.scanRepositories\' setting.');
continue;
}
if (path.isAbsolute(scanPath)) {
console.warn(localize('not supported', "Absolute paths not supported in 'git.scanRepositories' setting."));
const notSupportedMessage = localize('not supported', "Absolute paths not supported in 'git.scanRepositories' setting.");
this.outputChannelLogger.logWarning(notSupportedMessage);
console.warn(notSupportedMessage);
continue;
}
subfolders.add(path.join(root, scanPath));
}
this.outputChannelLogger.logTrace(`[swsf] Workspace scan sub folders: [${[...subfolders].join(', ')}]`);
await Promise.all([...subfolders].map(f => this.openRepository(f)));
}));
}
@@ -247,6 +256,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR
.filter(r => !(workspace.workspaceFolders || []).some(f => isDescendant(f.uri.fsPath, r!.repository.root))) as OpenRepository[];
openRepositoriesToDispose.forEach(r => r.dispose());
this.outputChannelLogger.logTrace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
await Promise.all(possibleRepositoryFolders.map(p => this.openRepository(p.uri.fsPath)));
}
@@ -260,17 +270,20 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR
.filter(({ root }) => workspace.getConfiguration('git', root).get<boolean>('enabled') !== true)
.map(({ repository }) => repository);
this.outputChannelLogger.logTrace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
possibleRepositoryFolders.forEach(p => this.openRepository(p.uri.fsPath));
openRepositoriesToDispose.forEach(r => r.dispose());
}
private async onDidChangeVisibleTextEditors(editors: readonly TextEditor[]): Promise<void> {
if (!workspace.isTrusted) {
this.outputChannelLogger.logTrace('[svte] Workspace is not trusted.');
return;
}
const config = workspace.getConfiguration('git');
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
this.outputChannelLogger.logTrace(`[svte] Scan visible text editors. autoRepositoryDetection=${autoRepositoryDetection}`);
if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors') {
return;
@@ -286,16 +299,20 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR
const repository = this.getRepository(uri);
if (repository) {
this.outputChannelLogger.logTrace(`[svte] Repository for editor resource ${uri.fsPath} already exists: ${repository.root}`);
return;
}
this.outputChannelLogger.logTrace(`[svte] Open repository for editor resource ${uri.fsPath}`);
await this.openRepository(path.dirname(uri.fsPath));
}));
}
@sequentialize
async openRepository(repoPath: string): Promise<void> {
this.outputChannelLogger.logTrace(`Opening repository: ${repoPath}`);
if (this.getRepository(repoPath)) {
this.outputChannelLogger.logTrace(`Repository for path ${repoPath} already exists`);
return;
}
@@ -303,6 +320,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR
const enabled = config.get<boolean>('enabled') === true;
if (!enabled) {
this.outputChannelLogger.logTrace('Git is not enabled');
return;
}
@@ -312,6 +330,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR
fs.accessSync(path.join(repoPath, 'HEAD'), fs.constants.F_OK);
const result = await this.git.exec(repoPath, ['-C', repoPath, 'rev-parse', '--show-cdup']);
if (result.stderr.trim() === '' && result.stdout.trim() === '') {
this.outputChannelLogger.logTrace(`Bare repository: ${repoPath}`);
return;
}
} catch {
@@ -326,12 +345,15 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR
// case insensitive file systems
// https://github.com/microsoft/vscode/issues/33498
const repositoryRoot = Uri.file(rawRoot).fsPath;
this.outputChannelLogger.logTrace(`Repository root: ${repositoryRoot}`);
if (this.getRepository(repositoryRoot)) {
this.outputChannelLogger.logTrace(`Repository for path ${repositoryRoot} already exists`);
return;
}
if (this.shouldRepositoryBeIgnored(rawRoot)) {
this.outputChannelLogger.logTrace(`Repository for path ${repositoryRoot} is ignored`);
return;
}
@@ -347,20 +369,19 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR
this.showRepoOnHomeDriveRootWarning = false;
}
this.outputChannelLogger.logTrace(`Repository for path ${repositoryRoot} is on the root of the HOMEDRIVE`);
return;
}
}
const dotGit = await this.git.getRepositoryDotGit(repositoryRoot);
const repository = new Repository(this.git.open(repositoryRoot, dotGit), this, this, this.globalState, this.outputChannel, this.telemetryReporter);
const repository = new Repository(this.git.open(repositoryRoot, dotGit), this, this, this, this.globalState, this.outputChannelLogger, this.telemetryReporter);
this.open(repository);
repository.status(); // do not await this, we want SCM to know about the repo asap
} catch (ex) {
// noop
if (Log.logLevel <= LogLevel.Trace) {
this.outputChannel.appendLine(`${logTimestamp()} Trace: Opening repository for path='${repoPath}' failed; ex=${ex}`);
}
this.outputChannelLogger.logTrace(`Opening repository for path='${repoPath}' failed; ex=${ex}`);
}
}
@@ -386,7 +407,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR
}
private open(repository: Repository): void {
this.outputChannel.appendLine(`${logTimestamp()} Open repository: ${repository.root}`);
this.outputChannelLogger.logInfo(`Open repository: ${repository.root}`);
const onDidDisappearRepository = filterEvent(repository.onDidChangeState, state => state === RepositoryState.Disposed);
const disappearListener = onDidDisappearRepository(() => dispose());
@@ -443,7 +464,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR
return;
}
this.outputChannel.appendLine(`${logTimestamp()} Close repository: ${repository.root}`);
this.outputChannelLogger.logInfo(`Close repository: ${repository.root}`);
openRepository.dispose();
}
@@ -491,6 +512,10 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR
return this.openRepositories.filter(r => r.repository === hint)[0];
}
if (hint instanceof ApiRepository) {
return this.openRepositories.filter(r => r.repository === hint.repository)[0];
}
if (typeof hint === 'string') {
hint = Uri.file(hint);
}
@@ -567,6 +592,20 @@ export class Model implements IRemoteSourcePublisherRegistry, IPushErrorHandlerR
return [...this.remoteSourcePublishers.values()];
}
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable {
this.postCommitCommandsProviders.add(provider);
this._onDidChangePostCommitCommandsProviders.fire();
return toDisposable(() => {
this.postCommitCommandsProviders.delete(provider);
this._onDidChangePostCommitCommandsProviders.fire();
});
}
getPostCommitCommandsProviders(): PostCommitCommandsProvider[] {
return [...this.postCommitCommandsProviders.values()];
}
registerCredentialsProvider(provider: CredentialsProvider): Disposable {
return this.askpass.registerCredentialsProvider(provider);
}

View File

@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { Command, Disposable, Event } from 'vscode';
import { PostCommitCommandsProvider } from './api/git';
export interface IPostCommitCommandsProviderRegistry {
readonly onDidChangePostCommitCommandsProviders: Event<void>;
getPostCommitCommandsProviders(): PostCommitCommandsProvider[];
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable;
}
const localize = nls.loadMessageBundle();
export class GitPostCommitCommandsProvider implements PostCommitCommandsProvider {
getCommands(): Command[] {
return [
{
command: 'git.push',
title: localize('scm secondary button commit and push', "Commit & Push")
},
{
command: 'git.sync',
title: localize('scm secondary button commit and sync', "Commit & Sync")
},
];
}
}

View File

@@ -38,7 +38,15 @@ export class GitProtocolHandler implements UriHandler {
let cloneUri: Uri;
try {
cloneUri = Uri.parse(Array.isArray(data.url) ? data.url[0] : data.url, true);
let rawUri = Array.isArray(data.url) ? data.url[0] : data.url;
// Handle SSH Uri
// Ex: git@github.com:microsoft/vscode.git
rawUri = rawUri.replace(/^(git@[^\/:]+)(:)/i, 'ssh://$1/');
cloneUri = Uri.parse(rawUri, true);
// Validate against supported schemes
if (!schemes.has(cloneUri.scheme.toLowerCase())) {
throw new Error('Unsupported scheme.');
}

View File

@@ -5,7 +5,8 @@
import * as fs from 'fs';
import * as path from 'path';
import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands, Tab, TabInputTextDiff, TabInputNotebookDiff, RelativePattern } from 'vscode';
import * as picomatch from 'picomatch';
import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands, Tab, TabInputTextDiff, TabInputNotebookDiff, RelativePattern } from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import * as nls from 'vscode-nls';
import { Branch, Change, ForcePushMode, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions, BranchQuery, FetchOptions } from './api/git';
@@ -14,13 +15,14 @@ import { debounce, memoize, throttle } from './decorators';
import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions } from './git';
import { StatusBarCommands } from './statusbar';
import { toGitUri } from './uri';
import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, logTimestamp, onceEvent, pathEquals, relativePath } from './util';
import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util';
import { IFileWatcher, watch } from './watch';
import { Log, LogLevel } from './log';
import { LogLevel, OutputChannelLogger } from './log';
import { IPushErrorHandlerRegistry } from './pushError';
import { ApiRepository } from './api/api1';
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
import { ActionButtonCommand } from './actionButton';
import { IPostCommitCommandsProviderRegistry } from './postCommitCommands';
const timeout = (millis: number) => new Promise(c => setTimeout(c, millis));
@@ -296,6 +298,10 @@ export class Resource implements SourceControlResourceState {
const command = this._commandResolver.resolveChangeCommand(this);
await commands.executeCommand<void>(command.command, ...(command.arguments || []));
}
clone() {
return new Resource(this._commandResolver, this._resourceGroupType, this._resourceUri, this._type, this._useIcons, this._renameResourceUri);
}
}
export const enum Operation {
@@ -450,6 +456,13 @@ class ProgressManager {
const onDidChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git', Uri.file(this.repository.root)));
onDidChange(_ => this.updateEnablement());
this.updateEnablement();
this.repository.onDidChangeOperations(() => {
const commitInProgress = this.repository.operations.isRunning(Operation.Commit);
this.repository.sourceControl.inputBox.enabled = !commitInProgress;
commands.executeCommand('setContext', 'commitInProgress', commitInProgress);
});
}
private updateEnablement(): void {
@@ -504,10 +517,10 @@ class FileEventLogger {
constructor(
private onWorkspaceWorkingTreeFileChange: Event<Uri>,
private onDotGitFileChange: Event<Uri>,
private outputChannel: OutputChannel
private outputChannelLogger: OutputChannelLogger
) {
this.logLevelDisposable = Log.onDidChangeLogLevel(this.onDidChangeLogLevel, this);
this.onDidChangeLogLevel(Log.logLevel);
this.logLevelDisposable = outputChannelLogger.onDidChangeLogLevel(this.onDidChangeLogLevel, this);
this.onDidChangeLogLevel(outputChannelLogger.currentLogLevel);
}
private onDidChangeLogLevel(level: LogLevel): void {
@@ -518,8 +531,8 @@ class FileEventLogger {
}
this.eventDisposable = combinedDisposable([
this.onWorkspaceWorkingTreeFileChange(uri => this.outputChannel.appendLine(`${logTimestamp()} [debug] [wt] Change: ${uri.fsPath}`)),
this.onDotGitFileChange(uri => this.outputChannel.appendLine(`${logTimestamp()} [debug] [.git] Change: ${uri.fsPath}`))
this.onWorkspaceWorkingTreeFileChange(uri => this.outputChannelLogger.logDebug(`[wt] Change: ${uri.fsPath}`)),
this.onDotGitFileChange(uri => this.outputChannelLogger.logDebug(`[.git] Change: ${uri.fsPath}`))
]);
}
@@ -539,14 +552,14 @@ class DotGitWatcher implements IFileWatcher {
constructor(
private repository: Repository,
private outputChannel: OutputChannel
private outputChannelLogger: OutputChannelLogger
) {
const rootWatcher = watch(repository.dotGit.path);
this.disposables.push(rootWatcher);
// Ignore changes to the "index.lock" file, and watchman fsmonitor hook (https://git-scm.com/docs/githooks#_fsmonitor_watchman) cookie files.
// Watchman creates a cookie file inside the git directory whenever a query is run (https://facebook.github.io/watchman/docs/cookies.html).
const filteredRootWatcher = filterEvent(rootWatcher.event, uri => !/\/\.git(\/index\.lock)?$|\/\.watchman-cookie-/.test(uri.path));
const filteredRootWatcher = filterEvent(rootWatcher.event, uri => uri.scheme === 'file' && !/\/\.git(\/index\.lock)?$|\/\.watchman-cookie-/.test(uri.path));
this.event = anyEvent(filteredRootWatcher, this.emitter.event);
repository.onDidRunGitStatus(this.updateTransientWatchers, this, this.disposables);
@@ -570,9 +583,7 @@ class DotGitWatcher implements IFileWatcher {
this.transientDisposables.push(upstreamWatcher);
upstreamWatcher.event(this.emitter.fire, this.emitter, this.transientDisposables);
} catch (err) {
if (Log.logLevel <= LogLevel.Error) {
this.outputChannel.appendLine(`${logTimestamp()} Warning: Failed to watch ref '${upstreamPath}', is most likely packed.`);
}
this.outputChannelLogger.logWarning(`Failed to watch ref '${upstreamPath}', is most likely packed.`);
}
}
@@ -605,11 +616,20 @@ class ResourceCommandResolver {
const title = this.getTitle(resource);
if (!resource.leftUri) {
return {
command: 'vscode.open',
title: localize('open', "Open"),
arguments: [resource.rightUri, { override: resource.type === Status.BOTH_MODIFIED ? false : undefined }, title]
};
const bothModified = resource.type === Status.BOTH_MODIFIED;
if (resource.rightUri && workspace.getConfiguration('git').get<boolean>('mergeEditor', false) && (bothModified || resource.type === Status.BOTH_ADDED)) {
return {
command: '_git.openMergeEditor',
title: localize('open.merge', "Open Merge"),
arguments: [resource.rightUri]
};
} else {
return {
command: 'vscode.open',
title: localize('open', "Open"),
arguments: [resource.rightUri, { override: bothModified ? false : undefined }, title]
};
}
} else {
return {
command: 'vscode.diff',
@@ -849,6 +869,7 @@ export class Repository implements Disposable {
private isRepositoryHuge: false | { limit: number } = false;
private didWarnAboutLimit = false;
private isBranchProtectedMatcher: picomatch.Matcher | undefined;
private resourceCommandResolver = new ResourceCommandResolver(this);
private disposables: Disposable[] = [];
@@ -856,8 +877,9 @@ export class Repository implements Disposable {
private readonly repository: BaseRepository,
private pushErrorHandlerRegistry: IPushErrorHandlerRegistry,
remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry,
postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry,
globalState: Memento,
outputChannel: OutputChannel,
outputChannelLogger: OutputChannelLogger,
private telemetryReporter: TelemetryReporter
) {
const repositoryWatcher = workspace.createFileSystemWatcher(new RelativePattern(Uri.file(repository.root), '**'));
@@ -869,13 +891,11 @@ export class Repository implements Disposable {
let onRepositoryDotGitFileChange: Event<Uri>;
try {
const dotGitFileWatcher = new DotGitWatcher(this, outputChannel);
const dotGitFileWatcher = new DotGitWatcher(this, outputChannelLogger);
onRepositoryDotGitFileChange = dotGitFileWatcher.event;
this.disposables.push(dotGitFileWatcher);
} catch (err) {
if (Log.logLevel <= LogLevel.Error) {
outputChannel.appendLine(`${logTimestamp()} Failed to watch path:'${this.dotGit.path}' or commonPath:'${this.dotGit.commonPath}', reverting to legacy API file watched. Some events might be lost.\n${err.stack || err}`);
}
outputChannelLogger.logError(`Failed to watch path:'${this.dotGit.path}' or commonPath:'${this.dotGit.commonPath}', reverting to legacy API file watched. Some events might be lost.\n${err.stack || err}`);
onRepositoryDotGitFileChange = filterEvent(onRepositoryFileChange, uri => /\.git($|\/)/.test(uri.path));
}
@@ -889,7 +909,7 @@ export class Repository implements Disposable {
// Relevate repository changes should trigger virtual document change events
onRepositoryDotGitFileChange(this._onDidChangeRepository.fire, this._onDidChangeRepository, this.disposables);
this.disposables.push(new FileEventLogger(onRepositoryWorkingTreeFileChange, onRepositoryDotGitFileChange, outputChannel));
this.disposables.push(new FileEventLogger(onRepositoryWorkingTreeFileChange, onRepositoryDotGitFileChange, outputChannelLogger));
const root = Uri.file(repository.root);
this._sourceControl = scm.createSourceControl('git', 'Git', root);
@@ -916,13 +936,19 @@ export class Repository implements Disposable {
onConfigListener(updateIndexGroupVisibility, this, this.disposables);
updateIndexGroupVisibility();
workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('git.mergeEditor')) {
this.mergeGroup.resourceStates = this.mergeGroup.resourceStates.map(r => r.clone());
}
}, undefined, this.disposables);
filterEvent(workspace.onDidChangeConfiguration, e =>
e.affectsConfiguration('git.branchSortOrder', root)
e.affectsConfiguration('git.branchProtection', root)
|| e.affectsConfiguration('git.branchSortOrder', root)
|| e.affectsConfiguration('git.untrackedChanges', root)
|| e.affectsConfiguration('git.ignoreSubmodules', root)
|| e.affectsConfiguration('git.openDiffOnClick', root)
|| e.affectsConfiguration('git.rebaseWhenSync', root)
|| e.affectsConfiguration('git.showUnpublishedCommitsButton', root)
|| e.affectsConfiguration('git.showActionButton', root)
)(this.updateModelState, this, this.disposables);
const updateInputBoxVisibility = () => {
@@ -963,12 +989,16 @@ export class Repository implements Disposable {
}
}, null, this.disposables);
const onDidChangeBranchProtection = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.branchProtection', root));
onDidChangeBranchProtection(this.updateBranchProtectionMatcher, this, this.disposables);
this.updateBranchProtectionMatcher();
const statusBar = new StatusBarCommands(this, remoteSourcePublisherRegistry);
this.disposables.push(statusBar);
statusBar.onDidChange(() => this._sourceControl.statusBarCommands = statusBar.commands, null, this.disposables);
this._sourceControl.statusBarCommands = statusBar.commands;
const actionButton = new ActionButtonCommand(this);
const actionButton = new ActionButtonCommand(this, postCommitCommandsProviderRegistry);
this.disposables.push(actionButton);
actionButton.onDidChange(() => this._sourceControl.actionButton = actionButton.button);
this._sourceControl.actionButton = actionButton.button;
@@ -1014,7 +1044,7 @@ export class Repository implements Disposable {
}
let lineNumber = 0;
let start = 0, end;
let start = 0;
let match: RegExpExecArray | null;
const regex = /\r?\n/g;
@@ -1023,7 +1053,7 @@ export class Repository implements Disposable {
lineNumber++;
}
end = match ? match.index : text.length;
const end = match ? match.index : text.length;
const line = text.substring(start, end);
@@ -1267,7 +1297,7 @@ export class Repository implements Disposable {
});
}
closeDiffEditors(indexResources: string[], workingTreeResources: string[], ignoreSetting: boolean = false): void {
closeDiffEditors(indexResources: string[] | undefined, workingTreeResources: string[] | undefined, ignoreSetting: boolean = false): void {
const config = workspace.getConfiguration('git', Uri.file(this.root));
if (!config.get<boolean>('closeDiffOnOperation', false) && !ignoreSetting) { return; }
@@ -1276,11 +1306,11 @@ export class Repository implements Disposable {
for (const tab of window.tabGroups.all.map(g => g.tabs).flat()) {
const { input } = tab;
if (input instanceof TabInputTextDiff || input instanceof TabInputNotebookDiff) {
if (input.modified.scheme === 'git' && indexResources.some(r => pathEquals(r, input.modified.fsPath))) {
if (input.modified.scheme === 'git' && (indexResources === undefined || indexResources.some(r => pathEquals(r, input.modified.fsPath)))) {
// Index
diffEditorTabsToClose.push(tab);
}
if (input.modified.scheme === 'file' && input.original.scheme === 'git' && workingTreeResources.some(r => pathEquals(r, input.modified.fsPath))) {
if (input.modified.scheme === 'file' && input.original.scheme === 'git' && (workingTreeResources === undefined || workingTreeResources.some(r => pathEquals(r, input.modified.fsPath)))) {
// Working Tree
diffEditorTabsToClose.push(tab);
}
@@ -1386,15 +1416,15 @@ export class Repository implements Disposable {
}
@throttle
async fetchAll(): Promise<void> {
await this._fetch({ all: true });
async fetchAll(cancellationToken?: CancellationToken): Promise<void> {
await this._fetch({ all: true, cancellationToken });
}
async fetch(options: FetchOptions): Promise<void> {
await this._fetch(options);
}
private async _fetch(options: { remote?: string; ref?: string; all?: boolean; prune?: boolean; depth?: number; silent?: boolean } = {}): Promise<void> {
private async _fetch(options: { remote?: string; ref?: string; all?: boolean; prune?: boolean; depth?: number; silent?: boolean; cancellationToken?: CancellationToken } = {}): Promise<void> {
if (!options.prune) {
const config = workspace.getConfiguration('git', Uri.file(this.root));
const prune = config.get<boolean>('pruneOnFetch');
@@ -1439,7 +1469,7 @@ export class Repository implements Disposable {
// When fetchOnPull is enabled, fetch all branches when pulling
if (fetchOnPull) {
await this.repository.fetch({ all: true });
await this.fetchAll();
}
if (await this.checkIfMaybeRebased(this.HEAD?.name)) {
@@ -1479,13 +1509,8 @@ export class Repository implements Disposable {
}
@throttle
sync(head: Branch): Promise<void> {
return this._sync(head, false);
}
@throttle
async syncRebase(head: Branch): Promise<void> {
return this._sync(head, true);
sync(head: Branch, rebase: boolean): Promise<void> {
return this._sync(head, rebase);
}
private async _sync(head: Branch, rebase: boolean): Promise<void> {
@@ -1510,7 +1535,7 @@ export class Repository implements Disposable {
const fn = async (cancellationToken?: CancellationToken) => {
// When fetchOnPull is enabled, fetch all branches when pulling
if (fetchOnPull) {
await this.repository.fetch({ all: true, cancellationToken });
await this.fetchAll(cancellationToken);
}
if (await this.checkIfMaybeRebased(this.HEAD?.name)) {
@@ -1868,9 +1893,10 @@ export class Repository implements Disposable {
if (didHitLimit) {
/* __GDPR__
"statusLimit" : {
"ignoreSubmodules": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"limit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"statusLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true }
"owner": "lszomoru",
"ignoreSubmodules": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Setting indicating whether submodules are ignored" },
"limit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Setting indicating the limit of status entries" },
"statusLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of status entries" }
}
*/
this.telemetryReporter.sendTelemetryEvent('statusLimit', { ignoreSubmodules: String(ignoreSubmodules) }, { limit, statusLength });
@@ -2002,6 +2028,9 @@ export class Repository implements Disposable {
// set count badge
this.setCountBadge();
// set mergeChanges context
commands.executeCommand('setContext', 'git.mergeChanges', merge.map(item => item.resourceUri.toString()));
this._onDidChangeStatus.fire();
this._sourceControl.commitTemplate = await this.getInputTemplate();
@@ -2188,6 +2217,21 @@ export class Repository implements Disposable {
}
}
private updateBranchProtectionMatcher(): void {
const scopedConfig = workspace.getConfiguration('git', Uri.file(this.repository.root));
const branchProtectionGlobs = scopedConfig.get<string[]>('branchProtection')!.map(bp => bp.trim()).filter(bp => bp !== '');
if (branchProtectionGlobs.length === 0) {
this.isBranchProtectedMatcher = undefined;
} else {
this.isBranchProtectedMatcher = picomatch(branchProtectionGlobs);
}
}
public isBranchProtected(name: string = this.HEAD?.name ?? ''): boolean {
return this.isBranchProtectedMatcher ? this.isBranchProtectedMatcher(name) : false;
}
dispose(): void {
this.disposables = dispose(this.disposables);
}

View File

@@ -9,7 +9,7 @@ export function applyLineChanges(original: TextDocument, modified: TextDocument,
const result: string[] = [];
let currentLine = 0;
for (let diff of diffs) {
for (const diff of diffs) {
const isInsertion = diff.originalEndLineNumber === 0;
const isDeletion = diff.modifiedEndLineNumber === 0;

View File

@@ -24,7 +24,8 @@ class CheckoutStatusBar {
get command(): Command | undefined {
const rebasing = !!this.repository.rebaseCommit;
const title = `$(git-branch) ${this.repository.headLabel}${rebasing ? ` (${localize('rebasing', 'Rebasing')})` : ''}`;
const isBranchProtected = this.repository.isBranchProtected();
const title = `${isBranchProtected ? '$(lock)' : '$(git-branch)'} ${this.repository.headLabel}${rebasing ? ` (${localize('rebasing', 'Rebasing')})` : ''}`;
return {
command: 'git.checkout',
@@ -144,10 +145,7 @@ class SyncStatusBar {
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';
command = 'git.sync';
tooltip = this.repository.syncTooltip;
} else {
icon = '$(cloud-upload)';

View File

@@ -6,27 +6,15 @@
import { ExtensionContext, workspace } from 'vscode';
import { filterEvent, IDisposable } from './util';
export interface ITerminalEnvironmentProvider {
getTerminalEnv(): { [key: string]: string };
}
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 }) {
constructor(private readonly context: ExtensionContext, private readonly envProviders: (ITerminalEnvironmentProvider | undefined)[]) {
this.disposable = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git'))
(this.refresh, this);
@@ -35,7 +23,19 @@ export class TerminalEnvironmentManager {
private refresh(): void {
const config = workspace.getConfiguration('git', null);
this.enabled = config.get<boolean>('enabled', true) && config.get('terminalAuthentication', true);
this.context.environmentVariableCollection.clear();
if (!config.get<boolean>('enabled', true)) {
return;
}
for (const envProvider of this.envProviders) {
const terminalEnv = envProvider?.getTerminalEnv() ?? {};
for (const name of Object.keys(terminalEnv)) {
this.context.environmentVariableCollection.replace(name, terminalEnv[name]);
}
}
}
dispose(): void {

View File

@@ -205,6 +205,7 @@ john.doe@mail.com
1580811030
1580811031
8e5a374372b8393906c7e380dbb09349c5385554
main,branch
This is a commit message.\x00`;
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_SINGLE_PARENT), [{
@@ -215,6 +216,7 @@ This is a commit message.\x00`;
authorName: 'John Doe',
authorEmail: 'john.doe@mail.com',
commitDate: new Date(1580811031000),
refNames: ['main', 'branch'],
}]);
});
@@ -225,6 +227,7 @@ john.doe@mail.com
1580811030
1580811031
8e5a374372b8393906c7e380dbb09349c5385554 df27d8c75b129ab9b178b386077da2822101b217
main
This is a commit message.\x00`;
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_MULTIPLE_PARENTS), [{
@@ -235,6 +238,7 @@ This is a commit message.\x00`;
authorName: 'John Doe',
authorEmail: 'john.doe@mail.com',
commitDate: new Date(1580811031000),
refNames: ['main'],
}]);
});
@@ -245,6 +249,7 @@ john.doe@mail.com
1580811030
1580811031
main
This is a commit message.\x00`;
assert.deepStrictEqual(parseGitCommits(GIT_OUTPUT_NO_PARENTS), [{
@@ -255,6 +260,7 @@ This is a commit message.\x00`;
authorName: 'John Doe',
authorEmail: 'john.doe@mail.com',
commitDate: new Date(1580811031000),
refNames: ['main'],
}]);
});
});

View File

@@ -51,3 +51,14 @@ export function toGitUri(uri: Uri, ref: string, options: GitUriOptions = {}): Ur
query: JSON.stringify(params)
});
}
/**
* Assuming `uri` is being merged it creates uris for `base`, `ours`, and `theirs`
*/
export function toMergeUris(uri: Uri): { base: Uri; ours: Uri; theirs: Uri } {
return {
base: toGitUri(uri, ':1'),
ours: toGitUri(uri, ':2'),
theirs: toGitUri(uri, ':3'),
};
}

View File

@@ -16,10 +16,6 @@ export function log(...args: any[]): void {
console.log.apply(console, ['git:', ...args]);
}
export function logTimestamp(): string {
return `[${new Date().toISOString()}]`;
}
export interface IDisposable {
dispose(): void;
}
@@ -55,9 +51,7 @@ 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);
}
disposables?.push(result);
return result;
};
@@ -93,7 +87,7 @@ export function eventToPromise<T>(event: Event<T>): Promise<T> {
}
export function once(fn: (...args: any[]) => any): (...args: any[]) => any {
let didRun = false;
const didRun = false;
return (...args) => {
if (didRun) {
@@ -223,11 +217,11 @@ export async function grep(filename: string, pattern: RegExp): Promise<boolean>
export function readBytes(stream: Readable, bytes: number): Promise<Buffer> {
return new Promise<Buffer>((complete, error) => {
let done = false;
let buffer = Buffer.allocUnsafe(bytes);
const buffer = Buffer.allocUnsafe(bytes);
let bytesRead = 0;
stream.on('data', (data: Buffer) => {
let bytesToRead = Math.min(bytes - bytesRead, data.length);
const bytesToRead = Math.min(bytes - bytesRead, data.length);
data.copy(buffer, bytesRead, 0, bytesToRead);
bytesRead += bytesToRead;