Merge from vscode merge-base (#22780)

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

This reverts commit 47a1745180.

* Fix notebook download task

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

View File

@@ -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) {