mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-22 09:35:37 -05:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
13
extensions/git/src/api/git.d.ts
vendored
13
extensions/git/src/api/git.d.ts
vendored
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
1
extensions/git/src/git-editor-empty.sh
Executable file
1
extensions/git/src/git-editor-empty.sh
Executable file
@@ -0,0 +1 @@
|
||||
#!/bin/sh
|
||||
21
extensions/git/src/git-editor-main.ts
Normal file
21
extensions/git/src/git-editor-main.ts
Normal 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);
|
||||
4
extensions/git/src/git-editor.sh
Executable file
4
extensions/git/src/git-editor.sh
Executable 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 "$@"
|
||||
@@ -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;
|
||||
|
||||
63
extensions/git/src/gitEditor.ts
Normal file
63
extensions/git/src/gitEditor.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
32
extensions/git/src/postCommitCommands.ts
Normal file
32
extensions/git/src/postCommitCommands.ts
Normal 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")
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'],
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user