mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-30 09:35:39 -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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user