eae5d8c807
These conflicts will be resolved in the following commits. We do it this way so that PR review is possible.
2879 lines
94 KiB
TypeScript
2879 lines
94 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { 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 TelemetryReporter from 'vscode-extension-telemetry';
|
|
import * as nls from 'vscode-nls';
|
|
import { Branch, ForcePushMode, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourceProvider } 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, pathEquals } from './util';
|
|
import { Log, LogLevel } from './log';
|
|
import { GitTimelineItem } from './timelineProvider';
|
|
import { ApiRepository } from './api/api1';
|
|
import { pickRemoteSource } from './remoteSource';
|
|
|
|
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 description(): string { return this.shortCommit; }
|
|
|
|
constructor(protected ref: Ref) { }
|
|
|
|
async run(repository: Repository, opts?: { detached?: boolean }): Promise<void> {
|
|
const ref = this.ref.name;
|
|
|
|
if (!ref) {
|
|
return;
|
|
}
|
|
|
|
await repository.checkout(ref, opts);
|
|
}
|
|
}
|
|
|
|
class CheckoutTagItem extends CheckoutItem {
|
|
|
|
get description(): string {
|
|
return localize('tag at', "Tag at {0}", this.shortCommit);
|
|
}
|
|
}
|
|
|
|
class CheckoutRemoteHeadItem extends CheckoutItem {
|
|
|
|
get description(): string {
|
|
return localize('remote branch at', "Remote branch at {0}", this.shortCommit);
|
|
}
|
|
|
|
async run(repository: Repository, opts?: { detached?: boolean }): Promise<void> {
|
|
if (!this.ref.name) {
|
|
return;
|
|
}
|
|
|
|
const branches = await repository.findTrackingBranches(this.ref.name);
|
|
|
|
if (branches.length > 0) {
|
|
await repository.checkout(branches[0].name!, opts);
|
|
} else {
|
|
await repository.checkoutTracking(this.ref.name, opts);
|
|
}
|
|
}
|
|
}
|
|
|
|
class BranchDeleteItem implements QuickPickItem {
|
|
|
|
private get shortCommit(): string { return (this.ref.commit || '').substr(0, 8); }
|
|
get branchName(): string | undefined { return this.ref.name; }
|
|
get label(): string { return this.branchName || ''; }
|
|
get description(): string { return this.shortCommit; }
|
|
|
|
constructor(private ref: Ref) { }
|
|
|
|
async run(repository: Repository, force?: boolean): Promise<void> {
|
|
if (!this.branchName) {
|
|
return;
|
|
}
|
|
await repository.deleteBranch(this.branchName, force);
|
|
}
|
|
}
|
|
|
|
class MergeItem implements QuickPickItem {
|
|
|
|
get label(): string { return this.ref.name || ''; }
|
|
get description(): string { return this.ref.name || ''; }
|
|
|
|
constructor(protected ref: Ref) { }
|
|
|
|
async run(repository: Repository): Promise<void> {
|
|
await repository.merge(this.ref.name! || this.ref.commit!);
|
|
}
|
|
}
|
|
|
|
class RebaseItem implements QuickPickItem {
|
|
|
|
get label(): string { return this.ref.name || ''; }
|
|
description: string = '';
|
|
|
|
constructor(readonly ref: Ref) { }
|
|
|
|
async run(repository: Repository): Promise<void> {
|
|
if (this.ref?.name) {
|
|
await repository.rebase(this.ref.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
class CreateBranchItem implements QuickPickItem {
|
|
get label(): string { return '$(plus) ' + localize('create branch', 'Create new branch...'); }
|
|
get description(): string { return ''; }
|
|
get alwaysShow(): boolean { return true; }
|
|
}
|
|
|
|
class CreateBranchFromItem implements QuickPickItem {
|
|
get label(): string { return '$(plus) ' + localize('create branch from', 'Create new branch from...'); }
|
|
get description(): string { return ''; }
|
|
get alwaysShow(): boolean { return true; }
|
|
}
|
|
|
|
class CheckoutDetachedItem implements QuickPickItem {
|
|
get label(): string { return '$(debug-disconnect) ' + localize('checkout detached', 'Checkout detached...'); }
|
|
get description(): string { return ''; }
|
|
get alwaysShow(): boolean { return true; }
|
|
}
|
|
|
|
class HEADItem implements QuickPickItem {
|
|
|
|
constructor(private repository: Repository) { }
|
|
|
|
get label(): string { return 'HEAD'; }
|
|
get description(): string { return (this.repository.HEAD && this.repository.HEAD.commit || '').substr(0, 8); }
|
|
get alwaysShow(): boolean { return true; }
|
|
}
|
|
|
|
class AddRemoteItem implements QuickPickItem {
|
|
|
|
constructor(private cc: CommandCenter) { }
|
|
|
|
get label(): string { return '$(plus) ' + localize('add remote', 'Add a new remote...'); }
|
|
get description(): string { return ''; }
|
|
|
|
get alwaysShow(): boolean { return true; }
|
|
|
|
async run(repository: Repository): Promise<void> {
|
|
await this.cc.addRemote(repository);
|
|
}
|
|
}
|
|
|
|
interface CommandOptions {
|
|
repository?: boolean;
|
|
diff?: boolean;
|
|
}
|
|
|
|
interface Command {
|
|
commandId: string;
|
|
key: string;
|
|
method: Function;
|
|
options: CommandOptions;
|
|
}
|
|
|
|
const Commands: Command[] = [];
|
|
|
|
function command(commandId: string, options: CommandOptions = {}): Function {
|
|
return (_target: any, key: string, descriptor: any) => {
|
|
if (!(typeof descriptor.value === 'function')) {
|
|
throw new Error('not supported');
|
|
}
|
|
|
|
Commands.push({ commandId, key, method: descriptor.value, options });
|
|
};
|
|
}
|
|
|
|
// const ImageMimetypes = [
|
|
// 'image/png',
|
|
// 'image/gif',
|
|
// 'image/jpeg',
|
|
// 'image/webp',
|
|
// 'image/tiff',
|
|
// 'image/bmp'
|
|
// ];
|
|
|
|
async function categorizeResourceByResolution(resources: Resource[]): Promise<{ merge: Resource[], resolved: Resource[], unresolved: Resource[], deletionConflicts: Resource[] }> {
|
|
const selection = resources.filter(s => s instanceof Resource) as Resource[];
|
|
const merge = selection.filter(s => s.resourceGroupType === ResourceGroupType.Merge);
|
|
const isBothAddedOrModified = (s: Resource) => s.type === Status.BOTH_MODIFIED || s.type === Status.BOTH_ADDED;
|
|
const isAnyDeleted = (s: Resource) => s.type === Status.DELETED_BY_THEM || s.type === Status.DELETED_BY_US;
|
|
const possibleUnresolved = merge.filter(isBothAddedOrModified);
|
|
const promises = possibleUnresolved.map(s => grep(s.resourceUri.fsPath, /^<{7}|^={7}|^>{7}/));
|
|
const unresolvedBothModified = await Promise.all<boolean>(promises);
|
|
const resolved = possibleUnresolved.filter((_s, i) => !unresolvedBothModified[i]);
|
|
const deletionConflicts = merge.filter(s => isAnyDeleted(s));
|
|
const unresolved = [
|
|
...merge.filter(s => !isBothAddedOrModified(s) && !isAnyDeleted(s)),
|
|
...possibleUnresolved.filter((_s, i) => unresolvedBothModified[i])
|
|
];
|
|
|
|
return { merge, resolved, unresolved, deletionConflicts };
|
|
}
|
|
|
|
function createCheckoutItems(repository: Repository): CheckoutItem[] {
|
|
const config = workspace.getConfiguration('git');
|
|
const checkoutTypeConfig = config.get<string | string[]>('checkoutType');
|
|
let checkoutTypes: string[];
|
|
|
|
if (checkoutTypeConfig === 'all' || !checkoutTypeConfig || checkoutTypeConfig.length === 0) {
|
|
checkoutTypes = ['local', 'remote', 'tags'];
|
|
} else if (typeof checkoutTypeConfig === 'string') {
|
|
checkoutTypes = [checkoutTypeConfig];
|
|
} else {
|
|
checkoutTypes = checkoutTypeConfig;
|
|
}
|
|
|
|
const processors = checkoutTypes.map(getCheckoutProcessor)
|
|
.filter(p => !!p) as CheckoutProcessor[];
|
|
|
|
for (const ref of repository.refs) {
|
|
for (const processor of processors) {
|
|
processor.onRef(ref);
|
|
}
|
|
}
|
|
|
|
return processors.reduce<CheckoutItem[]>((r, p) => r.concat(...p.items), []);
|
|
}
|
|
|
|
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 }) { }
|
|
|
|
onRef(ref: Ref): void {
|
|
if (ref.type === this.type) {
|
|
this.refs.push(ref);
|
|
}
|
|
}
|
|
}
|
|
|
|
function getCheckoutProcessor(type: string): CheckoutProcessor | undefined {
|
|
switch (type) {
|
|
case 'local':
|
|
return new CheckoutProcessor(RefType.Head, CheckoutItem);
|
|
case 'remote':
|
|
return new CheckoutProcessor(RefType.RemoteHead, CheckoutRemoteHeadItem);
|
|
case 'tags':
|
|
return new CheckoutProcessor(RefType.Tag, CheckoutTagItem);
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
function sanitizeRemoteName(name: string) {
|
|
name = name.trim();
|
|
return name && name.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$|\[|\]$/g, '-');
|
|
}
|
|
|
|
class TagItem implements QuickPickItem {
|
|
get label(): string { return this.ref.name ?? ''; }
|
|
get description(): string { return this.ref.commit?.substr(0, 8) ?? ''; }
|
|
constructor(readonly ref: Ref) { }
|
|
}
|
|
|
|
enum PushType {
|
|
Push,
|
|
PushTo,
|
|
PushFollowTags,
|
|
PushTags
|
|
}
|
|
|
|
interface PushOptions {
|
|
pushType: PushType;
|
|
forcePush?: boolean;
|
|
silent?: boolean;
|
|
|
|
pushTo?: {
|
|
remote?: string;
|
|
refspec?: string;
|
|
setUpstream?: boolean;
|
|
}
|
|
}
|
|
|
|
class CommandErrorOutputTextDocumentContentProvider implements TextDocumentContentProvider {
|
|
|
|
private items = new Map<string, string>();
|
|
|
|
set(uri: Uri, contents: string): void {
|
|
this.items.set(uri.path, contents);
|
|
}
|
|
|
|
delete(uri: Uri): void {
|
|
this.items.delete(uri.path);
|
|
}
|
|
|
|
provideTextDocumentContent(uri: Uri): string | undefined {
|
|
return this.items.get(uri.path);
|
|
}
|
|
}
|
|
|
|
export class CommandCenter {
|
|
|
|
private disposables: Disposable[];
|
|
private commandErrors = new CommandErrorOutputTextDocumentContentProvider();
|
|
|
|
constructor(
|
|
private git: Git,
|
|
private model: Model,
|
|
private outputChannel: OutputChannel,
|
|
private telemetryReporter: TelemetryReporter
|
|
) {
|
|
this.disposables = Commands.map(({ commandId, key, method, options }) => {
|
|
const command = this.createCommand(commandId, key, method, options);
|
|
|
|
if (options.diff) {
|
|
return commands.registerDiffInformationCommand(commandId, command);
|
|
} else {
|
|
return commands.registerCommand(commandId, command);
|
|
}
|
|
});
|
|
|
|
this.disposables.push(workspace.registerTextDocumentContentProvider('git-output', this.commandErrors));
|
|
}
|
|
|
|
@command('git.setLogLevel')
|
|
async setLogLevel(): Promise<void> {
|
|
const createItem = (logLevel: LogLevel) => ({
|
|
label: LogLevel[logLevel],
|
|
logLevel,
|
|
description: Log.logLevel === logLevel ? localize('current', "Current") : undefined
|
|
});
|
|
|
|
const items = [
|
|
createItem(LogLevel.Trace),
|
|
createItem(LogLevel.Debug),
|
|
createItem(LogLevel.Info),
|
|
createItem(LogLevel.Warning),
|
|
createItem(LogLevel.Error),
|
|
createItem(LogLevel.Critical),
|
|
createItem(LogLevel.Off)
|
|
];
|
|
|
|
const choice = await window.showQuickPick(items, {
|
|
placeHolder: localize('select log level', "Select log level")
|
|
});
|
|
|
|
if (!choice) {
|
|
return;
|
|
}
|
|
|
|
Log.logLevel = choice.logLevel;
|
|
this.outputChannel.appendLine(localize('changed', "Log level changed to: {0}", LogLevel[Log.logLevel]));
|
|
}
|
|
|
|
@command('git.refresh', { repository: true })
|
|
async refresh(repository: Repository): Promise<void> {
|
|
await repository.status();
|
|
}
|
|
|
|
@command('git.openResource')
|
|
async openResource(resource: Resource): Promise<void> {
|
|
const repository = this.model.getRepository(resource.resourceUri);
|
|
|
|
if (!repository) {
|
|
return;
|
|
}
|
|
|
|
await resource.open();
|
|
}
|
|
|
|
@command('git.openAllChanges', { repository: true })
|
|
async openChanges(repository: Repository): Promise<void> {
|
|
[
|
|
...repository.workingTreeGroup.resourceStates,
|
|
...repository.untrackedGroup.resourceStates,
|
|
].forEach(resource => {
|
|
commands.executeCommand(
|
|
'vscode.open',
|
|
resource.resourceUri,
|
|
{ preview: false, }
|
|
);
|
|
});
|
|
}
|
|
|
|
async cloneRepository(url?: string, parentPath?: string, options: { recursive?: boolean } = {}): Promise<void> {
|
|
if (!url || typeof url !== 'string') {
|
|
url = await pickRemoteSource(this.model, {
|
|
providerLabel: provider => localize('clonefrom', "Clone from {0}", provider.name),
|
|
urlLabel: localize('repourl', "Clone from URL")
|
|
});
|
|
}
|
|
|
|
if (!url) {
|
|
/* __GDPR__
|
|
"clone" : {
|
|
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
|
}
|
|
*/
|
|
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_URL' });
|
|
return;
|
|
}
|
|
|
|
url = url.trim().replace(/^git\s+clone\s+/, '');
|
|
|
|
if (!parentPath) {
|
|
const config = workspace.getConfiguration('git');
|
|
let defaultCloneDirectory = config.get<string>('defaultCloneDirectory') || os.homedir();
|
|
defaultCloneDirectory = defaultCloneDirectory.replace(/^~/, os.homedir());
|
|
|
|
const uris = await window.showOpenDialog({
|
|
canSelectFiles: false,
|
|
canSelectFolders: true,
|
|
canSelectMany: false,
|
|
defaultUri: Uri.file(defaultCloneDirectory),
|
|
openLabel: localize('selectFolder', "Select Repository Location")
|
|
});
|
|
|
|
if (!uris || uris.length === 0) {
|
|
/* __GDPR__
|
|
"clone" : {
|
|
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
|
}
|
|
*/
|
|
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'no_directory' });
|
|
return;
|
|
}
|
|
|
|
const uri = uris[0];
|
|
parentPath = uri.fsPath;
|
|
}
|
|
|
|
try {
|
|
const opts = {
|
|
location: ProgressLocation.Notification,
|
|
title: localize('cloning', "Cloning git repository '{0}'...", url),
|
|
cancellable: true
|
|
};
|
|
|
|
const repositoryPath = await window.withProgress(
|
|
opts,
|
|
(progress, token) => this.git.clone(url!, { parentPath: parentPath!, progress, recursive: options.recursive }, token)
|
|
);
|
|
|
|
const config = workspace.getConfiguration('git');
|
|
const openAfterClone = config.get<'always' | 'alwaysNewWindow' | 'whenNoFolderOpen' | 'prompt'>('openAfterClone');
|
|
|
|
enum PostCloneAction { Open, OpenNewWindow, AddToWorkspace }
|
|
let action: PostCloneAction | undefined = undefined;
|
|
|
|
if (openAfterClone === 'always') {
|
|
action = PostCloneAction.Open;
|
|
} else if (openAfterClone === 'alwaysNewWindow') {
|
|
action = PostCloneAction.OpenNewWindow;
|
|
} else if (openAfterClone === 'whenNoFolderOpen' && !workspace.workspaceFolders) {
|
|
action = PostCloneAction.Open;
|
|
}
|
|
|
|
if (action === undefined) {
|
|
let message = localize('proposeopen', "Would you like to open the cloned repository?");
|
|
const open = localize('openrepo', "Open");
|
|
const openNewWindow = localize('openreponew', "Open in New Window");
|
|
const choices = [open, openNewWindow];
|
|
|
|
const addToWorkspace = localize('add', "Add to Workspace");
|
|
if (workspace.workspaceFolders) {
|
|
message = localize('proposeopen2', "Would you like to open the cloned repository, or add it to the current workspace?");
|
|
choices.push(addToWorkspace);
|
|
}
|
|
|
|
const result = await window.showInformationMessage(message, ...choices);
|
|
|
|
action = result === open ? PostCloneAction.Open
|
|
: result === openNewWindow ? PostCloneAction.OpenNewWindow
|
|
: result === addToWorkspace ? PostCloneAction.AddToWorkspace : undefined;
|
|
}
|
|
|
|
/* __GDPR__
|
|
"clone" : {
|
|
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
|
"openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }
|
|
}
|
|
*/
|
|
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: action === PostCloneAction.Open || action === PostCloneAction.OpenNewWindow ? 1 : 0 });
|
|
|
|
const uri = Uri.file(repositoryPath);
|
|
|
|
if (action === PostCloneAction.Open) {
|
|
commands.executeCommand('vscode.openFolder', uri, { forceReuseWindow: true });
|
|
} else if (action === PostCloneAction.AddToWorkspace) {
|
|
workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
|
|
} else if (action === PostCloneAction.OpenNewWindow) {
|
|
commands.executeCommand('vscode.openFolder', uri, { forceNewWindow: true });
|
|
}
|
|
} catch (err) {
|
|
if (/already exists and is not an empty directory/.test(err && err.stderr || '')) {
|
|
/* __GDPR__
|
|
"clone" : {
|
|
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
|
}
|
|
*/
|
|
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'directory_not_empty' });
|
|
} else if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
|
|
return;
|
|
} else {
|
|
/* __GDPR__
|
|
"clone" : {
|
|
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
|
}
|
|
*/
|
|
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'error' });
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
@command('git.clone')
|
|
async clone(url?: string, parentPath?: string): Promise<void> {
|
|
await this.cloneRepository(url, parentPath);
|
|
}
|
|
|
|
@command('git.cloneRecursive')
|
|
async cloneRecursive(url?: string, parentPath?: string): Promise<void> {
|
|
await this.cloneRepository(url, parentPath, { recursive: true });
|
|
}
|
|
|
|
@command('git.init')
|
|
async init(skipFolderPrompt = false): Promise<void> {
|
|
let repositoryPath: string | undefined = undefined;
|
|
let askToOpen = true;
|
|
|
|
if (workspace.workspaceFolders) {
|
|
if (skipFolderPrompt && workspace.workspaceFolders.length === 1) {
|
|
repositoryPath = workspace.workspaceFolders[0].uri.fsPath;
|
|
askToOpen = false;
|
|
} else {
|
|
const placeHolder = localize('init', "Pick workspace folder to initialize git repo in");
|
|
const pick = { label: localize('choose', "Choose Folder...") };
|
|
const items: { label: string, folder?: WorkspaceFolder }[] = [
|
|
...workspace.workspaceFolders.map(folder => ({ label: folder.name, description: folder.uri.fsPath, folder })),
|
|
pick
|
|
];
|
|
const item = await window.showQuickPick(items, { placeHolder, ignoreFocusOut: true });
|
|
|
|
if (!item) {
|
|
return;
|
|
} else if (item.folder) {
|
|
repositoryPath = item.folder.uri.fsPath;
|
|
askToOpen = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!repositoryPath) {
|
|
const homeUri = Uri.file(os.homedir());
|
|
const defaultUri = workspace.workspaceFolders && workspace.workspaceFolders.length > 0
|
|
? Uri.file(workspace.workspaceFolders[0].uri.fsPath)
|
|
: homeUri;
|
|
|
|
const result = await window.showOpenDialog({
|
|
canSelectFiles: false,
|
|
canSelectFolders: true,
|
|
canSelectMany: false,
|
|
defaultUri,
|
|
openLabel: localize('init repo', "Initialize Repository")
|
|
});
|
|
|
|
if (!result || result.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const uri = result[0];
|
|
|
|
if (homeUri.toString().startsWith(uri.toString())) {
|
|
const yes = localize('create repo', "Initialize Repository");
|
|
const answer = await window.showWarningMessage(localize('are you sure', "This will create a Git repository in '{0}'. Are you sure you want to continue?", uri.fsPath), yes);
|
|
|
|
if (answer !== yes) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
repositoryPath = uri.fsPath;
|
|
|
|
if (workspace.workspaceFolders && workspace.workspaceFolders.some(w => w.uri.toString() === uri.toString())) {
|
|
askToOpen = false;
|
|
}
|
|
}
|
|
|
|
await this.git.init(repositoryPath);
|
|
|
|
let message = localize('proposeopen init', "Would you like to open the initialized repository?");
|
|
const open = localize('openrepo', "Open");
|
|
const openNewWindow = localize('openreponew', "Open in New Window");
|
|
const choices = [open, openNewWindow];
|
|
|
|
if (!askToOpen) {
|
|
return;
|
|
}
|
|
|
|
const addToWorkspace = localize('add', "Add to Workspace");
|
|
if (workspace.workspaceFolders) {
|
|
message = localize('proposeopen2 init', "Would you like to open the initialized repository, or add it to the current workspace?");
|
|
choices.push(addToWorkspace);
|
|
}
|
|
|
|
const result = await window.showInformationMessage(message, ...choices);
|
|
const uri = Uri.file(repositoryPath);
|
|
|
|
if (result === open) {
|
|
commands.executeCommand('vscode.openFolder', uri);
|
|
} else if (result === addToWorkspace) {
|
|
workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
|
|
} else if (result === openNewWindow) {
|
|
commands.executeCommand('vscode.openFolder', uri, true);
|
|
} else {
|
|
await this.model.openRepository(repositoryPath);
|
|
}
|
|
}
|
|
|
|
@command('git.openRepository', { repository: false })
|
|
async openRepository(path?: string): Promise<void> {
|
|
if (!path) {
|
|
const result = await window.showOpenDialog({
|
|
canSelectFiles: false,
|
|
canSelectFolders: true,
|
|
canSelectMany: false,
|
|
defaultUri: Uri.file(os.homedir()),
|
|
openLabel: localize('open repo', "Open Repository")
|
|
});
|
|
|
|
if (!result || result.length === 0) {
|
|
return;
|
|
}
|
|
|
|
path = result[0].fsPath;
|
|
}
|
|
|
|
await this.model.openRepository(path);
|
|
}
|
|
|
|
@command('git.close', { repository: true })
|
|
async close(repository: Repository): Promise<void> {
|
|
this.model.close(repository);
|
|
}
|
|
|
|
@command('git.openFile')
|
|
async openFile(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
|
|
const preserveFocus = arg instanceof Resource;
|
|
|
|
let uris: Uri[] | undefined;
|
|
|
|
if (arg instanceof Uri) {
|
|
if (isGitUri(arg)) {
|
|
uris = [Uri.file(fromGitUri(arg).path)];
|
|
} else if (arg.scheme === 'file') {
|
|
uris = [arg];
|
|
}
|
|
} else {
|
|
let resource = arg;
|
|
|
|
if (!(resource instanceof Resource)) {
|
|
// can happen when called from a keybinding
|
|
resource = this.getSCMResource();
|
|
}
|
|
|
|
if (resource) {
|
|
uris = ([resource, ...resourceStates] as Resource[])
|
|
.filter(r => r.type !== Status.DELETED && r.type !== Status.INDEX_DELETED)
|
|
.map(r => r.resourceUri);
|
|
} else if (window.activeTextEditor) {
|
|
uris = [window.activeTextEditor.document.uri];
|
|
}
|
|
}
|
|
|
|
if (!uris) {
|
|
return;
|
|
}
|
|
|
|
const activeTextEditor = window.activeTextEditor;
|
|
|
|
for (const uri of uris) {
|
|
const opts: TextDocumentShowOptions = {
|
|
preserveFocus,
|
|
preview: false,
|
|
viewColumn: ViewColumn.Active
|
|
};
|
|
|
|
let document;
|
|
try {
|
|
document = await workspace.openTextDocument(uri);
|
|
} catch (error) {
|
|
await commands.executeCommand('vscode.open', uri, {
|
|
...opts,
|
|
override: arg instanceof Resource && arg.type === Status.BOTH_MODIFIED ? false : undefined
|
|
});
|
|
continue;
|
|
}
|
|
|
|
// Check if active text editor has same path as other editor. we cannot compare via
|
|
// URI.toString() here because the schemas can be different. Instead we just go by path.
|
|
if (activeTextEditor && activeTextEditor.document.uri.path === uri.path) {
|
|
// preserve not only selection but also visible range
|
|
opts.selection = activeTextEditor.selection;
|
|
const previousVisibleRanges = activeTextEditor.visibleRanges;
|
|
const editor = await window.showTextDocument(document, opts);
|
|
editor.revealRange(previousVisibleRanges[0]);
|
|
} else {
|
|
await commands.executeCommand('vscode.open', uri, opts);
|
|
}
|
|
}
|
|
}
|
|
|
|
@command('git.openFile2')
|
|
async openFile2(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
|
|
this.openFile(arg, ...resourceStates);
|
|
}
|
|
|
|
@command('git.openHEADFile')
|
|
async openHEADFile(arg?: Resource | Uri): Promise<void> {
|
|
let resource: Resource | undefined = undefined;
|
|
const preview = !(arg instanceof Resource);
|
|
|
|
if (arg instanceof Resource) {
|
|
resource = arg;
|
|
} else if (arg instanceof Uri) {
|
|
resource = this.getSCMResource(arg);
|
|
} else {
|
|
resource = this.getSCMResource();
|
|
}
|
|
|
|
if (!resource) {
|
|
return;
|
|
}
|
|
|
|
const HEAD = resource.leftUri;
|
|
const basename = path.basename(resource.resourceUri.fsPath);
|
|
const title = `${basename} (HEAD)`;
|
|
|
|
if (!HEAD) {
|
|
window.showWarningMessage(localize('HEAD not available', "HEAD version of '{0}' is not available.", path.basename(resource.resourceUri.fsPath)));
|
|
return;
|
|
}
|
|
|
|
const opts: TextDocumentShowOptions = {
|
|
preview
|
|
};
|
|
|
|
return await commands.executeCommand<void>('vscode.open', HEAD, opts, title);
|
|
}
|
|
|
|
@command('git.openChange')
|
|
async openChange(arg?: Resource | Uri, ...resourceStates: SourceControlResourceState[]): Promise<void> {
|
|
let resources: Resource[] | undefined = undefined;
|
|
|
|
if (arg instanceof Uri) {
|
|
const resource = this.getSCMResource(arg);
|
|
if (resource !== undefined) {
|
|
resources = [resource];
|
|
}
|
|
} else {
|
|
let resource: Resource | undefined = undefined;
|
|
|
|
if (arg instanceof Resource) {
|
|
resource = arg;
|
|
} else {
|
|
resource = this.getSCMResource();
|
|
}
|
|
|
|
if (resource) {
|
|
resources = [...resourceStates as Resource[], resource];
|
|
}
|
|
}
|
|
|
|
if (!resources) {
|
|
return;
|
|
}
|
|
|
|
for (const resource of resources) {
|
|
await resource.openChange();
|
|
}
|
|
}
|
|
|
|
@command('git.rename', { repository: true })
|
|
async rename(repository: Repository, fromUri: Uri | undefined): Promise<void> {
|
|
fromUri = fromUri ?? window.activeTextEditor?.document.uri;
|
|
|
|
if (!fromUri) {
|
|
return;
|
|
}
|
|
|
|
const from = path.relative(repository.root, fromUri.path);
|
|
let to = await window.showInputBox({
|
|
value: from,
|
|
valueSelection: [from.length - path.basename(from).length, from.length]
|
|
});
|
|
|
|
to = to?.trim();
|
|
|
|
if (!to) {
|
|
return;
|
|
}
|
|
|
|
await repository.move(from, to);
|
|
}
|
|
|
|
@command('git.stage')
|
|
async stage(...resourceStates: SourceControlResourceState[]): Promise<void> {
|
|
this.outputChannel.appendLine(`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(`git.stage.getSCMResource ${resource ? resource.resourceUri.toString() : null}`);
|
|
|
|
if (!resource) {
|
|
return;
|
|
}
|
|
|
|
resourceStates = [resource];
|
|
}
|
|
|
|
const selection = resourceStates.filter(s => s instanceof Resource) as Resource[];
|
|
const { resolved, unresolved, deletionConflicts } = await categorizeResourceByResolution(selection);
|
|
|
|
if (unresolved.length > 0) {
|
|
const message = unresolved.length > 1
|
|
? localize('confirm stage files with merge conflicts', "Are you sure you want to stage {0} files with merge conflicts?", unresolved.length)
|
|
: localize('confirm stage file with merge conflicts', "Are you sure you want to stage {0} with merge conflicts?", path.basename(unresolved[0].resourceUri.fsPath));
|
|
|
|
const yes = localize('yes', "Yes");
|
|
const pick = await window.showWarningMessage(message, { modal: true }, yes);
|
|
|
|
if (pick !== yes) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
await this.runByRepository(deletionConflicts.map(r => r.resourceUri), async (repository, resources) => {
|
|
for (const resource of resources) {
|
|
await this._stageDeletionConflict(repository, resource);
|
|
}
|
|
});
|
|
} catch (err) {
|
|
if (/Cancelled/.test(err.message)) {
|
|
return;
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
|
|
const workingTree = selection.filter(s => s.resourceGroupType === ResourceGroupType.WorkingTree);
|
|
const untracked = selection.filter(s => s.resourceGroupType === ResourceGroupType.Untracked);
|
|
const scmResources = [...workingTree, ...untracked, ...resolved, ...unresolved];
|
|
|
|
this.outputChannel.appendLine(`git.stage.scmResources ${scmResources.length}`);
|
|
if (!scmResources.length) {
|
|
return;
|
|
}
|
|
|
|
const resources = scmResources.map(r => r.resourceUri);
|
|
await this.runByRepository(resources, async (repository, resources) => repository.add(resources));
|
|
}
|
|
|
|
@command('git.stageAll', { repository: true })
|
|
async stageAll(repository: Repository): Promise<void> {
|
|
const resources = [...repository.workingTreeGroup.resourceStates, ...repository.untrackedGroup.resourceStates];
|
|
const uris = resources.map(r => r.resourceUri);
|
|
|
|
if (uris.length > 0) {
|
|
const config = workspace.getConfiguration('git', Uri.file(repository.root));
|
|
const untrackedChanges = config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges');
|
|
await repository.add(uris, untrackedChanges === 'mixed' ? undefined : { update: true });
|
|
}
|
|
}
|
|
|
|
private async _stageDeletionConflict(repository: Repository, uri: Uri): Promise<void> {
|
|
const uriString = uri.toString();
|
|
const resource = repository.mergeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
|
|
|
|
if (!resource) {
|
|
return;
|
|
}
|
|
|
|
if (resource.type === Status.DELETED_BY_THEM) {
|
|
const keepIt = localize('keep ours', "Keep Our Version");
|
|
const deleteIt = localize('delete', "Delete File");
|
|
const result = await window.showInformationMessage(localize('deleted by them', "File '{0}' was deleted by them and modified by us.\n\nWhat would you like to do?", path.basename(uri.fsPath)), { modal: true }, keepIt, deleteIt);
|
|
|
|
if (result === keepIt) {
|
|
await repository.add([uri]);
|
|
} else if (result === deleteIt) {
|
|
await repository.rm([uri]);
|
|
} else {
|
|
throw new Error('Cancelled');
|
|
}
|
|
} else if (resource.type === Status.DELETED_BY_US) {
|
|
const keepIt = localize('keep theirs', "Keep Their Version");
|
|
const deleteIt = localize('delete', "Delete File");
|
|
const result = await window.showInformationMessage(localize('deleted by us', "File '{0}' was deleted by us and modified by them.\n\nWhat would you like to do?", path.basename(uri.fsPath)), { modal: true }, keepIt, deleteIt);
|
|
|
|
if (result === keepIt) {
|
|
await repository.add([uri]);
|
|
} else if (result === deleteIt) {
|
|
await repository.rm([uri]);
|
|
} else {
|
|
throw new Error('Cancelled');
|
|
}
|
|
}
|
|
}
|
|
|
|
@command('git.stageAllTracked', { repository: true })
|
|
async stageAllTracked(repository: Repository): Promise<void> {
|
|
const resources = repository.workingTreeGroup.resourceStates
|
|
.filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED);
|
|
const uris = resources.map(r => r.resourceUri);
|
|
|
|
await repository.add(uris);
|
|
}
|
|
|
|
@command('git.stageAllUntracked', { repository: true })
|
|
async stageAllUntracked(repository: Repository): Promise<void> {
|
|
const resources = [...repository.workingTreeGroup.resourceStates, ...repository.untrackedGroup.resourceStates]
|
|
.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED);
|
|
const uris = resources.map(r => r.resourceUri);
|
|
|
|
await repository.add(uris);
|
|
}
|
|
|
|
@command('git.stageAllMerge', { repository: true })
|
|
async stageAllMerge(repository: Repository): Promise<void> {
|
|
const resources = repository.mergeGroup.resourceStates.filter(s => s instanceof Resource) as Resource[];
|
|
const { merge, unresolved, deletionConflicts } = await categorizeResourceByResolution(resources);
|
|
|
|
try {
|
|
for (const deletionConflict of deletionConflicts) {
|
|
await this._stageDeletionConflict(repository, deletionConflict.resourceUri);
|
|
}
|
|
} catch (err) {
|
|
if (/Cancelled/.test(err.message)) {
|
|
return;
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
|
|
if (unresolved.length > 0) {
|
|
const message = unresolved.length > 1
|
|
? localize('confirm stage files with merge conflicts', "Are you sure you want to stage {0} files with merge conflicts?", merge.length)
|
|
: localize('confirm stage file with merge conflicts', "Are you sure you want to stage {0} with merge conflicts?", path.basename(merge[0].resourceUri.fsPath));
|
|
|
|
const yes = localize('yes', "Yes");
|
|
const pick = await window.showWarningMessage(message, { modal: true }, yes);
|
|
|
|
if (pick !== yes) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const uris = resources.map(r => r.resourceUri);
|
|
|
|
if (uris.length > 0) {
|
|
await repository.add(uris);
|
|
}
|
|
}
|
|
|
|
@command('git.stageChange')
|
|
async stageChange(uri: Uri, changes: LineChange[], index: number): Promise<void> {
|
|
if (!uri) {
|
|
return;
|
|
}
|
|
|
|
const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0];
|
|
|
|
if (!textEditor) {
|
|
return;
|
|
}
|
|
|
|
await this._stageChanges(textEditor, [changes[index]]);
|
|
|
|
const firstStagedLine = changes[index].modifiedStartLineNumber - 1;
|
|
textEditor.selections = [new Selection(firstStagedLine, 0, firstStagedLine, 0)];
|
|
}
|
|
|
|
@command('git.stageSelectedRanges', { diff: true })
|
|
async stageSelectedChanges(changes: LineChange[]): Promise<void> {
|
|
const textEditor = window.activeTextEditor;
|
|
|
|
if (!textEditor) {
|
|
return;
|
|
}
|
|
|
|
const modifiedDocument = textEditor.document;
|
|
const selectedLines = toLineRanges(textEditor.selections, modifiedDocument);
|
|
const selectedChanges = changes
|
|
.map(diff => selectedLines.reduce<LineChange | null>((result, range) => result || intersectDiffWithRange(modifiedDocument, diff, range), null))
|
|
.filter(d => !!d) as LineChange[];
|
|
|
|
if (!selectedChanges.length) {
|
|
return;
|
|
}
|
|
|
|
await this._stageChanges(textEditor, selectedChanges);
|
|
}
|
|
|
|
private async _stageChanges(textEditor: TextEditor, changes: LineChange[]): Promise<void> {
|
|
const modifiedDocument = textEditor.document;
|
|
const modifiedUri = modifiedDocument.uri;
|
|
|
|
if (modifiedUri.scheme !== 'file') {
|
|
return;
|
|
}
|
|
|
|
const originalUri = toGitUri(modifiedUri, '~');
|
|
const originalDocument = await workspace.openTextDocument(originalUri);
|
|
const result = applyLineChanges(originalDocument, modifiedDocument, changes);
|
|
|
|
await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
|
|
}
|
|
|
|
@command('git.revertChange')
|
|
async revertChange(uri: Uri, changes: LineChange[], index: number): Promise<void> {
|
|
if (!uri) {
|
|
return;
|
|
}
|
|
|
|
const textEditor = window.visibleTextEditors.filter(e => e.document.uri.toString() === uri.toString())[0];
|
|
|
|
if (!textEditor) {
|
|
return;
|
|
}
|
|
|
|
await this._revertChanges(textEditor, [...changes.slice(0, index), ...changes.slice(index + 1)]);
|
|
|
|
const firstStagedLine = changes[index].modifiedStartLineNumber - 1;
|
|
textEditor.selections = [new Selection(firstStagedLine, 0, firstStagedLine, 0)];
|
|
}
|
|
|
|
@command('git.revertSelectedRanges', { diff: true })
|
|
async revertSelectedRanges(changes: LineChange[]): Promise<void> {
|
|
const textEditor = window.activeTextEditor;
|
|
|
|
if (!textEditor) {
|
|
return;
|
|
}
|
|
|
|
const modifiedDocument = textEditor.document;
|
|
const selections = textEditor.selections;
|
|
const selectedChanges = changes.filter(change => {
|
|
const modifiedRange = getModifiedRange(modifiedDocument, change);
|
|
return selections.every(selection => !selection.intersection(modifiedRange));
|
|
});
|
|
|
|
if (selectedChanges.length === changes.length) {
|
|
return;
|
|
}
|
|
|
|
const selectionsBeforeRevert = textEditor.selections;
|
|
await this._revertChanges(textEditor, selectedChanges);
|
|
textEditor.selections = selectionsBeforeRevert;
|
|
}
|
|
|
|
private async _revertChanges(textEditor: TextEditor, changes: LineChange[]): Promise<void> {
|
|
const modifiedDocument = textEditor.document;
|
|
const modifiedUri = modifiedDocument.uri;
|
|
|
|
if (modifiedUri.scheme !== 'file') {
|
|
return;
|
|
}
|
|
|
|
const originalUri = toGitUri(modifiedUri, '~');
|
|
const originalDocument = await workspace.openTextDocument(originalUri);
|
|
const visibleRangesBeforeRevert = textEditor.visibleRanges;
|
|
const result = applyLineChanges(originalDocument, modifiedDocument, changes);
|
|
|
|
const edit = new WorkspaceEdit();
|
|
edit.replace(modifiedUri, new Range(new Position(0, 0), modifiedDocument.lineAt(modifiedDocument.lineCount - 1).range.end), result);
|
|
workspace.applyEdit(edit);
|
|
|
|
await modifiedDocument.save();
|
|
|
|
textEditor.revealRange(visibleRangesBeforeRevert[0]);
|
|
}
|
|
|
|
@command('git.unstage')
|
|
async unstage(...resourceStates: SourceControlResourceState[]): Promise<void> {
|
|
resourceStates = resourceStates.filter(s => !!s);
|
|
|
|
if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
|
|
const resource = this.getSCMResource();
|
|
|
|
if (!resource) {
|
|
return;
|
|
}
|
|
|
|
resourceStates = [resource];
|
|
}
|
|
|
|
const scmResources = resourceStates
|
|
.filter(s => s instanceof Resource && s.resourceGroupType === ResourceGroupType.Index) as Resource[];
|
|
|
|
if (!scmResources.length) {
|
|
return;
|
|
}
|
|
|
|
const resources = scmResources.map(r => r.resourceUri);
|
|
await this.runByRepository(resources, async (repository, resources) => repository.revert(resources));
|
|
}
|
|
|
|
@command('git.unstageAll', { repository: true })
|
|
async unstageAll(repository: Repository): Promise<void> {
|
|
await repository.revert([]);
|
|
}
|
|
|
|
@command('git.unstageSelectedRanges', { diff: true })
|
|
async unstageSelectedRanges(diffs: LineChange[]): Promise<void> {
|
|
const textEditor = window.activeTextEditor;
|
|
|
|
if (!textEditor) {
|
|
return;
|
|
}
|
|
|
|
const modifiedDocument = textEditor.document;
|
|
const modifiedUri = modifiedDocument.uri;
|
|
|
|
if (!isGitUri(modifiedUri)) {
|
|
return;
|
|
}
|
|
|
|
const { ref } = fromGitUri(modifiedUri);
|
|
|
|
if (ref !== '') {
|
|
return;
|
|
}
|
|
|
|
const originalUri = toGitUri(modifiedUri, 'HEAD');
|
|
const originalDocument = await workspace.openTextDocument(originalUri);
|
|
const selectedLines = toLineRanges(textEditor.selections, modifiedDocument);
|
|
const selectedDiffs = diffs
|
|
.map(diff => selectedLines.reduce<LineChange | null>((result, range) => result || intersectDiffWithRange(modifiedDocument, diff, range), null))
|
|
.filter(d => !!d) as LineChange[];
|
|
|
|
if (!selectedDiffs.length) {
|
|
return;
|
|
}
|
|
|
|
const invertedDiffs = selectedDiffs.map(invertLineChange);
|
|
const result = applyLineChanges(modifiedDocument, originalDocument, invertedDiffs);
|
|
|
|
await this.runByRepository(modifiedUri, async (repository, resource) => await repository.stage(resource, result));
|
|
}
|
|
|
|
@command('git.clean')
|
|
async clean(...resourceStates: SourceControlResourceState[]): Promise<void> {
|
|
resourceStates = resourceStates.filter(s => !!s);
|
|
|
|
if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
|
|
const resource = this.getSCMResource();
|
|
|
|
if (!resource) {
|
|
return;
|
|
}
|
|
|
|
resourceStates = [resource];
|
|
}
|
|
|
|
const scmResources = resourceStates.filter(s => s instanceof Resource
|
|
&& (s.resourceGroupType === ResourceGroupType.WorkingTree || s.resourceGroupType === ResourceGroupType.Untracked)) as Resource[];
|
|
|
|
if (!scmResources.length) {
|
|
return;
|
|
}
|
|
|
|
const untrackedCount = scmResources.reduce((s, r) => s + (r.type === Status.UNTRACKED ? 1 : 0), 0);
|
|
let message: string;
|
|
let yes = localize('discard', "Discard Changes");
|
|
|
|
if (scmResources.length === 1) {
|
|
if (untrackedCount > 0) {
|
|
message = localize('confirm delete', "Are you sure you want to DELETE {0}?\nThis is IRREVERSIBLE!\nThis file will be FOREVER LOST if you proceed.", path.basename(scmResources[0].resourceUri.fsPath));
|
|
yes = localize('delete file', "Delete file");
|
|
} else {
|
|
if (scmResources[0].type === Status.DELETED) {
|
|
yes = localize('restore file', "Restore file");
|
|
message = localize('confirm restore', "Are you sure you want to restore {0}?", path.basename(scmResources[0].resourceUri.fsPath));
|
|
} else {
|
|
message = localize('confirm discard', "Are you sure you want to discard changes in {0}?", path.basename(scmResources[0].resourceUri.fsPath));
|
|
}
|
|
}
|
|
} else {
|
|
if (scmResources.every(resource => resource.type === Status.DELETED)) {
|
|
yes = localize('restore files', "Restore files");
|
|
message = localize('confirm restore multiple', "Are you sure you want to restore {0} files?", scmResources.length);
|
|
} else {
|
|
message = localize('confirm discard multiple', "Are you sure you want to discard changes in {0} files?", scmResources.length);
|
|
}
|
|
|
|
if (untrackedCount > 0) {
|
|
message = `${message}\n\n${localize('warn untracked', "This will DELETE {0} untracked files!\nThis is IRREVERSIBLE!\nThese files will be FOREVER LOST.", untrackedCount)}`;
|
|
}
|
|
}
|
|
|
|
const pick = await window.showWarningMessage(message, { modal: true }, yes);
|
|
|
|
if (pick !== yes) {
|
|
return;
|
|
}
|
|
|
|
const resources = scmResources.map(r => r.resourceUri);
|
|
await this.runByRepository(resources, async (repository, resources) => repository.clean(resources));
|
|
}
|
|
|
|
@command('git.cleanAll', { repository: true })
|
|
async cleanAll(repository: Repository): Promise<void> {
|
|
let resources = repository.workingTreeGroup.resourceStates;
|
|
|
|
if (resources.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const trackedResources = resources.filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED);
|
|
const untrackedResources = resources.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED);
|
|
|
|
if (untrackedResources.length === 0) {
|
|
await this._cleanTrackedChanges(repository, resources);
|
|
} else if (resources.length === 1) {
|
|
await this._cleanUntrackedChange(repository, resources[0]);
|
|
} else if (trackedResources.length === 0) {
|
|
await this._cleanUntrackedChanges(repository, resources);
|
|
} else { // resources.length > 1 && untrackedResources.length > 0 && trackedResources.length > 0
|
|
const untrackedMessage = untrackedResources.length === 1
|
|
? localize('there are untracked files single', "The following untracked file will be DELETED FROM DISK if discarded: {0}.", path.basename(untrackedResources[0].resourceUri.fsPath))
|
|
: localize('there are untracked files', "There are {0} untracked files which will be DELETED FROM DISK if discarded.", untrackedResources.length);
|
|
|
|
const message = localize('confirm discard all 2', "{0}\n\nThis is IRREVERSIBLE, your current working set will be FOREVER LOST.", untrackedMessage, resources.length);
|
|
|
|
const yesTracked = trackedResources.length === 1
|
|
? localize('yes discard tracked', "Discard 1 Tracked File", trackedResources.length)
|
|
: localize('yes discard tracked multiple', "Discard {0} Tracked Files", trackedResources.length);
|
|
|
|
const yesAll = localize('discardAll', "Discard All {0} Files", resources.length);
|
|
const pick = await window.showWarningMessage(message, { modal: true }, yesTracked, yesAll);
|
|
|
|
if (pick === yesTracked) {
|
|
resources = trackedResources;
|
|
} else if (pick !== yesAll) {
|
|
return;
|
|
}
|
|
|
|
await repository.clean(resources.map(r => r.resourceUri));
|
|
}
|
|
}
|
|
|
|
@command('git.cleanAllTracked', { repository: true })
|
|
async cleanAllTracked(repository: Repository): Promise<void> {
|
|
const resources = repository.workingTreeGroup.resourceStates
|
|
.filter(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED);
|
|
|
|
if (resources.length === 0) {
|
|
return;
|
|
}
|
|
|
|
await this._cleanTrackedChanges(repository, resources);
|
|
}
|
|
|
|
@command('git.cleanAllUntracked', { repository: true })
|
|
async cleanAllUntracked(repository: Repository): Promise<void> {
|
|
const resources = [...repository.workingTreeGroup.resourceStates, ...repository.untrackedGroup.resourceStates]
|
|
.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED);
|
|
|
|
if (resources.length === 0) {
|
|
return;
|
|
}
|
|
|
|
if (resources.length === 1) {
|
|
await this._cleanUntrackedChange(repository, resources[0]);
|
|
} else {
|
|
await this._cleanUntrackedChanges(repository, resources);
|
|
}
|
|
}
|
|
|
|
private async _cleanTrackedChanges(repository: Repository, resources: Resource[]): Promise<void> {
|
|
const message = resources.length === 1
|
|
? localize('confirm discard all single', "Are you sure you want to discard changes in {0}?", path.basename(resources[0].resourceUri.fsPath))
|
|
: localize('confirm discard all', "Are you sure you want to discard ALL changes in {0} files?\nThis is IRREVERSIBLE!\nYour current working set will be FOREVER LOST if you proceed.", resources.length);
|
|
const yes = resources.length === 1
|
|
? localize('discardAll multiple', "Discard 1 File")
|
|
: localize('discardAll', "Discard All {0} Files", resources.length);
|
|
const pick = await window.showWarningMessage(message, { modal: true }, yes);
|
|
|
|
if (pick !== yes) {
|
|
return;
|
|
}
|
|
|
|
await repository.clean(resources.map(r => r.resourceUri));
|
|
}
|
|
|
|
private async _cleanUntrackedChange(repository: Repository, resource: Resource): Promise<void> {
|
|
const message = localize('confirm delete', "Are you sure you want to DELETE {0}?\nThis is IRREVERSIBLE!\nThis file will be FOREVER LOST if you proceed.", path.basename(resource.resourceUri.fsPath));
|
|
const yes = localize('delete file', "Delete file");
|
|
const pick = await window.showWarningMessage(message, { modal: true }, yes);
|
|
|
|
if (pick !== yes) {
|
|
return;
|
|
}
|
|
|
|
await repository.clean([resource.resourceUri]);
|
|
}
|
|
|
|
private async _cleanUntrackedChanges(repository: Repository, resources: Resource[]): Promise<void> {
|
|
const message = localize('confirm delete multiple', "Are you sure you want to DELETE {0} files?\nThis is IRREVERSIBLE!\nThese files will be FOREVER LOST if you proceed.", resources.length);
|
|
const yes = localize('delete files', "Delete Files");
|
|
const pick = await window.showWarningMessage(message, { modal: true }, yes);
|
|
|
|
if (pick !== yes) {
|
|
return;
|
|
}
|
|
|
|
await repository.clean(resources.map(r => r.resourceUri));
|
|
}
|
|
|
|
private async smartCommit(
|
|
repository: Repository,
|
|
getCommitMessage: () => Promise<string | undefined>,
|
|
opts?: CommitOptions
|
|
): Promise<boolean> {
|
|
const config = workspace.getConfiguration('git', Uri.file(repository.root));
|
|
let promptToSaveFilesBeforeCommit = config.get<'always' | 'staged' | 'never'>('promptToSaveFilesBeforeCommit');
|
|
|
|
// migration
|
|
if (promptToSaveFilesBeforeCommit as any === true) {
|
|
promptToSaveFilesBeforeCommit = 'always';
|
|
} else if (promptToSaveFilesBeforeCommit as any === false) {
|
|
promptToSaveFilesBeforeCommit = 'never';
|
|
}
|
|
|
|
const enableSmartCommit = config.get<boolean>('enableSmartCommit') === true;
|
|
const enableCommitSigning = config.get<boolean>('enableCommitSigning') === true;
|
|
let noStagedChanges = repository.indexGroup.resourceStates.length === 0;
|
|
let noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
|
|
|
|
if (promptToSaveFilesBeforeCommit !== 'never') {
|
|
let documents = workspace.textDocuments
|
|
.filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath));
|
|
|
|
if (promptToSaveFilesBeforeCommit === 'staged' || repository.indexGroup.resourceStates.length > 0) {
|
|
documents = documents
|
|
.filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath)));
|
|
}
|
|
|
|
if (documents.length > 0) {
|
|
const message = documents.length === 1
|
|
? localize('unsaved files single', "The following file has unsaved changes which won't be included in the commit if you proceed: {0}.\n\nWould you like to save it before committing?", path.basename(documents[0].uri.fsPath))
|
|
: localize('unsaved files', "There are {0} unsaved files.\n\nWould you like to save them before committing?", documents.length);
|
|
const saveAndCommit = localize('save and commit', "Save All & Commit");
|
|
const commit = localize('commit', "Commit Staged Changes");
|
|
const pick = await window.showWarningMessage(message, { modal: true }, saveAndCommit, commit);
|
|
|
|
if (pick === saveAndCommit) {
|
|
await Promise.all(documents.map(d => d.save()));
|
|
await repository.add(documents.map(d => d.uri));
|
|
|
|
noStagedChanges = repository.indexGroup.resourceStates.length === 0;
|
|
noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0;
|
|
} else if (pick !== commit) {
|
|
return false; // do not commit on cancel
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
const suggestSmartCommit = config.get<boolean>('suggestSmartCommit') === true;
|
|
|
|
if (!suggestSmartCommit) {
|
|
return false;
|
|
}
|
|
|
|
// prompt the user if we want to commit all or not
|
|
const message = localize('no staged changes', "There are no staged changes to commit.\n\nWould you like to stage all your changes and commit them directly?");
|
|
const yes = localize('yes', "Yes");
|
|
const always = localize('always', "Always");
|
|
const never = localize('never', "Never");
|
|
const pick = await window.showWarningMessage(message, { modal: true }, yes, always, never);
|
|
|
|
if (pick === always) {
|
|
config.update('enableSmartCommit', true, true);
|
|
} else if (pick === never) {
|
|
config.update('suggestSmartCommit', false, true);
|
|
return false;
|
|
} else if (pick !== yes) {
|
|
return false; // do not commit on cancel
|
|
}
|
|
}
|
|
|
|
// enable signing of commits if configured
|
|
opts.signCommit = enableCommitSigning;
|
|
|
|
if (config.get<boolean>('alwaysSignOff')) {
|
|
opts.signoff = true;
|
|
}
|
|
|
|
const smartCommitChanges = config.get<'all' | 'tracked'>('smartCommitChanges');
|
|
|
|
if (
|
|
(
|
|
// no changes
|
|
(noStagedChanges && noUnstagedChanges)
|
|
// or no staged changes and not `all`
|
|
|| (!opts.all && noStagedChanges)
|
|
// no staged changes and no tracked unstaged changes
|
|
|| (noStagedChanges && smartCommitChanges === 'tracked' && repository.workingTreeGroup.resourceStates.every(r => r.type === Status.UNTRACKED))
|
|
)
|
|
// amend allows changing only the commit message
|
|
&& !opts.amend
|
|
&& !opts.empty
|
|
) {
|
|
const commitAnyway = localize('commit anyway', "Create Empty Commit");
|
|
const answer = await window.showInformationMessage(localize('no changes', "There are no changes to commit."), commitAnyway);
|
|
|
|
if (answer !== commitAnyway) {
|
|
return false;
|
|
}
|
|
|
|
opts.empty = true;
|
|
}
|
|
|
|
if (opts.noVerify) {
|
|
if (!config.get<boolean>('allowNoVerifyCommit')) {
|
|
await window.showErrorMessage(localize('no verify commit not allowed', "Commits without verification are not allowed, please enable them with the 'git.allowNoVerifyCommit' setting."));
|
|
return false;
|
|
}
|
|
|
|
if (config.get<boolean>('confirmNoVerifyCommit')) {
|
|
const message = localize('confirm no verify commit', "You are about to commit your changes without verification, this skips pre-commit hooks and can be undesirable.\n\nAre you sure to continue?");
|
|
const yes = localize('ok', "OK");
|
|
const neverAgain = localize('never ask again', "OK, Don't Ask Again");
|
|
const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);
|
|
|
|
if (pick === neverAgain) {
|
|
config.update('confirmNoVerifyCommit', false, true);
|
|
} else if (pick !== yes) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
let message = await getCommitMessage();
|
|
|
|
if (!message && !opts.amend) {
|
|
return false;
|
|
}
|
|
|
|
if (opts.all && smartCommitChanges === 'tracked') {
|
|
opts.all = 'tracked';
|
|
}
|
|
|
|
if (opts.all && config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges') !== 'mixed') {
|
|
opts.all = 'tracked';
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private async commitWithAnyInput(repository: Repository, opts?: CommitOptions): Promise<void> {
|
|
const message = repository.inputBox.value;
|
|
const getCommitMessage = async () => {
|
|
let _message: string | undefined = message;
|
|
|
|
if (!_message) {
|
|
let value: string | undefined = undefined;
|
|
|
|
if (opts && opts.amend && repository.HEAD && repository.HEAD.commit) {
|
|
return undefined;
|
|
}
|
|
|
|
const branchName = repository.headShortName;
|
|
let placeHolder: string;
|
|
|
|
if (branchName) {
|
|
placeHolder = localize('commitMessageWithHeadLabel2', "Message (commit on '{0}')", branchName);
|
|
} else {
|
|
placeHolder = localize('commit message', "Commit message");
|
|
}
|
|
|
|
_message = await window.showInputBox({
|
|
value,
|
|
placeHolder,
|
|
prompt: localize('provide commit message', "Please provide a commit message"),
|
|
ignoreFocusOut: true
|
|
});
|
|
}
|
|
|
|
return _message;
|
|
};
|
|
|
|
const didCommit = await this.smartCommit(repository, getCommitMessage, opts);
|
|
|
|
if (message && didCommit) {
|
|
repository.inputBox.value = await repository.getInputTemplate();
|
|
}
|
|
}
|
|
|
|
@command('git.commit', { repository: true })
|
|
async commit(repository: Repository): Promise<void> {
|
|
await this.commitWithAnyInput(repository);
|
|
}
|
|
|
|
@command('git.commitStaged', { repository: true })
|
|
async commitStaged(repository: Repository): Promise<void> {
|
|
await this.commitWithAnyInput(repository, { all: false });
|
|
}
|
|
|
|
@command('git.commitStagedSigned', { repository: true })
|
|
async commitStagedSigned(repository: Repository): Promise<void> {
|
|
await this.commitWithAnyInput(repository, { all: false, signoff: true });
|
|
}
|
|
|
|
@command('git.commitStagedAmend', { repository: true })
|
|
async commitStagedAmend(repository: Repository): Promise<void> {
|
|
await this.commitWithAnyInput(repository, { all: false, amend: true });
|
|
}
|
|
|
|
@command('git.commitAll', { repository: true })
|
|
async commitAll(repository: Repository): Promise<void> {
|
|
await this.commitWithAnyInput(repository, { all: true });
|
|
}
|
|
|
|
@command('git.commitAllSigned', { repository: true })
|
|
async commitAllSigned(repository: Repository): Promise<void> {
|
|
await this.commitWithAnyInput(repository, { all: true, signoff: true });
|
|
}
|
|
|
|
@command('git.commitAllAmend', { repository: true })
|
|
async commitAllAmend(repository: Repository): Promise<void> {
|
|
await this.commitWithAnyInput(repository, { all: true, amend: true });
|
|
}
|
|
|
|
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 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);
|
|
|
|
if (pick === neverAgain) {
|
|
await config.update('confirmEmptyCommits', false, true);
|
|
} else if (pick !== yes) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
await this.commitWithAnyInput(repository, { empty: true, noVerify });
|
|
}
|
|
|
|
@command('git.commitEmpty', { repository: true })
|
|
async commitEmpty(repository: Repository): Promise<void> {
|
|
await this._commitEmpty(repository);
|
|
}
|
|
|
|
@command('git.commitNoVerify', { repository: true })
|
|
async commitNoVerify(repository: Repository): Promise<void> {
|
|
await this.commitWithAnyInput(repository, { noVerify: true });
|
|
}
|
|
|
|
@command('git.commitStagedNoVerify', { repository: true })
|
|
async commitStagedNoVerify(repository: Repository): Promise<void> {
|
|
await this.commitWithAnyInput(repository, { all: false, noVerify: true });
|
|
}
|
|
|
|
@command('git.commitStagedSignedNoVerify', { repository: true })
|
|
async commitStagedSignedNoVerify(repository: Repository): Promise<void> {
|
|
await this.commitWithAnyInput(repository, { all: false, signoff: true, noVerify: true });
|
|
}
|
|
|
|
@command('git.commitStagedAmendNoVerify', { repository: true })
|
|
async commitStagedAmendNoVerify(repository: Repository): Promise<void> {
|
|
await this.commitWithAnyInput(repository, { all: false, amend: true, noVerify: true });
|
|
}
|
|
|
|
@command('git.commitAllNoVerify', { repository: true })
|
|
async commitAllNoVerify(repository: Repository): Promise<void> {
|
|
await this.commitWithAnyInput(repository, { all: true, noVerify: true });
|
|
}
|
|
|
|
@command('git.commitAllSignedNoVerify', { repository: true })
|
|
async commitAllSignedNoVerify(repository: Repository): Promise<void> {
|
|
await this.commitWithAnyInput(repository, { all: true, signoff: true, noVerify: true });
|
|
}
|
|
|
|
@command('git.commitAllAmendNoVerify', { repository: true })
|
|
async commitAllAmendNoVerify(repository: Repository): Promise<void> {
|
|
await this.commitWithAnyInput(repository, { all: true, amend: true, noVerify: true });
|
|
}
|
|
|
|
@command('git.commitEmptyNoVerify', { repository: true })
|
|
async commitEmptyNoVerify(repository: Repository): Promise<void> {
|
|
await this._commitEmpty(repository, true);
|
|
}
|
|
|
|
@command('git.restoreCommitTemplate', { repository: true })
|
|
async restoreCommitTemplate(repository: Repository): Promise<void> {
|
|
repository.inputBox.value = await repository.getCommitTemplate();
|
|
}
|
|
|
|
@command('git.undoCommit', { repository: true })
|
|
async undoCommit(repository: Repository): Promise<void> {
|
|
const HEAD = repository.HEAD;
|
|
|
|
if (!HEAD || !HEAD.commit) {
|
|
window.showWarningMessage(localize('no more', "Can't undo because HEAD doesn't point to any commit."));
|
|
return;
|
|
}
|
|
|
|
const commit = await repository.getCommit('HEAD');
|
|
|
|
if (commit.parents.length > 1) {
|
|
const yes = localize('undo commit', "Undo merge commit");
|
|
const result = await window.showWarningMessage(localize('merge commit', "The last commit was a merge commit. Are you sure you want to undo it?"), { modal: true }, yes);
|
|
|
|
if (result !== yes) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (commit.parents.length > 0) {
|
|
await repository.reset('HEAD~');
|
|
} else {
|
|
await repository.deleteRef('HEAD');
|
|
await this.unstageAll(repository);
|
|
}
|
|
|
|
repository.inputBox.value = commit.message;
|
|
}
|
|
|
|
@command('git.checkout', { repository: true })
|
|
async checkout(repository: Repository, treeish?: string): Promise<boolean> {
|
|
return this._checkout(repository, { treeish });
|
|
}
|
|
|
|
@command('git.checkoutDetached', { repository: true })
|
|
async checkoutDetached(repository: Repository, treeish?: string): Promise<boolean> {
|
|
return this._checkout(repository, { detached: true, treeish });
|
|
}
|
|
|
|
private async _checkout(repository: Repository, opts?: { detached?: boolean, treeish?: string }): Promise<boolean> {
|
|
if (typeof opts?.treeish === 'string') {
|
|
await repository.checkout(opts?.treeish, opts);
|
|
return true;
|
|
}
|
|
|
|
const createBranch = new CreateBranchItem();
|
|
const createBranchFrom = new CreateBranchFromItem();
|
|
const checkoutDetached = new CheckoutDetachedItem();
|
|
const picks: QuickPickItem[] = [];
|
|
|
|
if (!opts?.detached) {
|
|
picks.push(createBranch, createBranchFrom, checkoutDetached);
|
|
}
|
|
|
|
picks.push(...createCheckoutItems(repository));
|
|
|
|
const quickpick = window.createQuickPick();
|
|
quickpick.items = picks;
|
|
quickpick.placeholder = opts?.detached
|
|
? localize('select a ref to checkout detached', 'Select a ref to checkout in detached mode')
|
|
: localize('select a ref to checkout', 'Select a ref to checkout');
|
|
|
|
quickpick.show();
|
|
|
|
const choice = await new Promise<QuickPickItem | undefined>(c => quickpick.onDidAccept(() => c(quickpick.activeItems[0])));
|
|
quickpick.hide();
|
|
|
|
if (!choice) {
|
|
return false;
|
|
}
|
|
|
|
if (choice === createBranch) {
|
|
await this._branch(repository, quickpick.value);
|
|
} else if (choice === createBranchFrom) {
|
|
await this._branch(repository, quickpick.value, true);
|
|
} else if (choice === checkoutDetached) {
|
|
return this._checkout(repository, { detached: true });
|
|
} else {
|
|
const item = choice as CheckoutItem;
|
|
|
|
try {
|
|
await item.run(repository, opts);
|
|
} catch (err) {
|
|
if (err.gitErrorCode !== GitErrorCodes.DirtyWorkTree) {
|
|
throw err;
|
|
}
|
|
|
|
const force = localize('force', "Force Checkout");
|
|
const stash = localize('stashcheckout', "Stash & Checkout");
|
|
const choice = await window.showWarningMessage(localize('local changes', "Your local changes would be overwritten by checkout."), { modal: true }, force, stash);
|
|
|
|
if (choice === force) {
|
|
await this.cleanAll(repository);
|
|
await item.run(repository, opts);
|
|
} else if (choice === stash) {
|
|
await this.stash(repository);
|
|
await item.run(repository, opts);
|
|
await this.stashPopLatest(repository);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
@command('git.branch', { repository: true })
|
|
async branch(repository: Repository): Promise<void> {
|
|
await this._branch(repository);
|
|
}
|
|
|
|
@command('git.branchFrom', { repository: true })
|
|
async branchFrom(repository: Repository): Promise<void> {
|
|
await this._branch(repository, undefined, true);
|
|
}
|
|
|
|
private async promptForBranchName(defaultName?: string, initialValue?: string): Promise<string> {
|
|
const config = workspace.getConfiguration('git');
|
|
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;
|
|
}
|
|
|
|
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);
|
|
|
|
if (!branchName) {
|
|
return;
|
|
}
|
|
|
|
let target = 'HEAD';
|
|
|
|
if (from) {
|
|
const picks = [new HEADItem(repository), ...createCheckoutItems(repository)];
|
|
const placeHolder = localize('select a ref to create a new branch from', 'Select a ref to create the \'{0}\' branch from', branchName);
|
|
const choice = await window.showQuickPick(picks, { placeHolder });
|
|
|
|
if (!choice) {
|
|
return;
|
|
}
|
|
|
|
target = choice.label;
|
|
}
|
|
|
|
await repository.branch(branchName, true, target);
|
|
}
|
|
|
|
@command('git.deleteBranch', { repository: true })
|
|
async deleteBranch(repository: Repository, name: string, force?: boolean): Promise<void> {
|
|
let run: (force?: boolean) => Promise<void>;
|
|
if (typeof name === 'string') {
|
|
run = force => repository.deleteBranch(name, force);
|
|
} else {
|
|
const currentHead = repository.HEAD && repository.HEAD.name;
|
|
const heads = repository.refs.filter(ref => ref.type === RefType.Head && ref.name !== currentHead)
|
|
.map(ref => new BranchDeleteItem(ref));
|
|
|
|
const placeHolder = localize('select branch to delete', 'Select a branch to delete');
|
|
const choice = await window.showQuickPick<BranchDeleteItem>(heads, { placeHolder });
|
|
|
|
if (!choice || !choice.branchName) {
|
|
return;
|
|
}
|
|
name = choice.branchName;
|
|
run = force => choice.run(repository, force);
|
|
}
|
|
|
|
try {
|
|
await run(force);
|
|
} catch (err) {
|
|
if (err.gitErrorCode !== GitErrorCodes.BranchNotFullyMerged) {
|
|
throw err;
|
|
}
|
|
|
|
const message = localize('confirm force delete branch', "The branch '{0}' is not fully merged. Delete anyway?", name);
|
|
const yes = localize('delete branch', "Delete Branch");
|
|
const pick = await window.showWarningMessage(message, { modal: true }, yes);
|
|
|
|
if (pick === yes) {
|
|
await run(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
@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);
|
|
|
|
if (!branchName) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await repository.renameBranch(branchName);
|
|
} catch (err) {
|
|
switch (err.gitErrorCode) {
|
|
case GitErrorCodes.InvalidBranchName:
|
|
window.showErrorMessage(localize('invalid branch name', 'Invalid branch name'));
|
|
return;
|
|
case GitErrorCodes.BranchAlreadyExists:
|
|
window.showErrorMessage(localize('branch already exists', "A branch named '{0}' already exists", branchName));
|
|
return;
|
|
default:
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
@command('git.merge', { repository: true })
|
|
async merge(repository: Repository): Promise<void> {
|
|
const config = workspace.getConfiguration('git');
|
|
const checkoutType = config.get<string | string[]>('checkoutType');
|
|
const includeRemotes = checkoutType === 'all' || checkoutType === 'remote' || checkoutType?.includes('remote');
|
|
|
|
const heads = repository.refs.filter(ref => ref.type === RefType.Head)
|
|
.filter(ref => ref.name || ref.commit)
|
|
.map(ref => new MergeItem(ref as Branch));
|
|
|
|
const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
|
|
.filter(ref => ref.name || ref.commit)
|
|
.map(ref => new MergeItem(ref as Branch));
|
|
|
|
const picks = [...heads, ...remoteHeads];
|
|
const placeHolder = localize('select a branch to merge from', 'Select a branch to merge from');
|
|
const choice = await window.showQuickPick<MergeItem>(picks, { placeHolder });
|
|
|
|
if (!choice) {
|
|
return;
|
|
}
|
|
|
|
await choice.run(repository);
|
|
}
|
|
|
|
@command('git.rebase', { repository: true })
|
|
async rebase(repository: Repository): Promise<void> {
|
|
const config = workspace.getConfiguration('git');
|
|
const checkoutType = config.get<string | string[]>('checkoutType');
|
|
const includeRemotes = checkoutType === 'all' || checkoutType === 'remote' || checkoutType?.includes('remote');
|
|
|
|
const heads = repository.refs.filter(ref => ref.type === RefType.Head)
|
|
.filter(ref => ref.name !== repository.HEAD?.name)
|
|
.filter(ref => ref.name || ref.commit);
|
|
|
|
const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
|
|
.filter(ref => ref.name || ref.commit);
|
|
|
|
const picks = [...heads, ...remoteHeads]
|
|
.map(ref => new RebaseItem(ref));
|
|
|
|
// set upstream branch as first
|
|
if (repository.HEAD?.upstream) {
|
|
const upstreamName = `${repository.HEAD?.upstream.remote}/${repository.HEAD?.upstream.name}`;
|
|
const index = picks.findIndex(e => e.ref.name === upstreamName);
|
|
|
|
if (index > -1) {
|
|
const [ref] = picks.splice(index, 1);
|
|
ref.description = '(upstream)';
|
|
picks.unshift(ref);
|
|
}
|
|
}
|
|
|
|
const placeHolder = localize('select a branch to rebase onto', 'Select a branch to rebase onto');
|
|
const choice = await window.showQuickPick<RebaseItem>(picks, { placeHolder });
|
|
|
|
if (!choice) {
|
|
return;
|
|
}
|
|
|
|
await choice.run(repository);
|
|
}
|
|
|
|
@command('git.createTag', { repository: true })
|
|
async createTag(repository: Repository): Promise<void> {
|
|
const inputTagName = await window.showInputBox({
|
|
placeHolder: localize('tag name', "Tag name"),
|
|
prompt: localize('provide tag name', "Please provide a tag name"),
|
|
ignoreFocusOut: true
|
|
});
|
|
|
|
if (!inputTagName) {
|
|
return;
|
|
}
|
|
|
|
const inputMessage = await window.showInputBox({
|
|
placeHolder: localize('tag message', "Message"),
|
|
prompt: localize('provide tag message', "Please provide a message to annotate the tag"),
|
|
ignoreFocusOut: true
|
|
});
|
|
|
|
const name = inputTagName.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-');
|
|
await repository.tag(name, inputMessage);
|
|
}
|
|
|
|
@command('git.deleteTag', { repository: true })
|
|
async deleteTag(repository: Repository): Promise<void> {
|
|
const picks = repository.refs.filter(ref => ref.type === RefType.Tag)
|
|
.map(ref => new TagItem(ref));
|
|
|
|
if (picks.length === 0) {
|
|
window.showWarningMessage(localize('no tags', "This repository has no tags."));
|
|
return;
|
|
}
|
|
|
|
const placeHolder = localize('select a tag to delete', 'Select a tag to delete');
|
|
const choice = await window.showQuickPick(picks, { placeHolder });
|
|
|
|
if (!choice) {
|
|
return;
|
|
}
|
|
|
|
await repository.deleteTag(choice.label);
|
|
}
|
|
|
|
@command('git.fetch', { repository: true })
|
|
async fetch(repository: Repository): Promise<void> {
|
|
if (repository.remotes.length === 0) {
|
|
window.showWarningMessage(localize('no remotes to fetch', "This repository has no remotes configured to fetch from."));
|
|
return;
|
|
}
|
|
|
|
await repository.fetchDefault();
|
|
}
|
|
|
|
@command('git.fetchPrune', { repository: true })
|
|
async fetchPrune(repository: Repository): Promise<void> {
|
|
if (repository.remotes.length === 0) {
|
|
window.showWarningMessage(localize('no remotes to fetch', "This repository has no remotes configured to fetch from."));
|
|
return;
|
|
}
|
|
|
|
await repository.fetchPrune();
|
|
}
|
|
|
|
|
|
@command('git.fetchAll', { repository: true })
|
|
async fetchAll(repository: Repository): Promise<void> {
|
|
if (repository.remotes.length === 0) {
|
|
window.showWarningMessage(localize('no remotes to fetch', "This repository has no remotes configured to fetch from."));
|
|
return;
|
|
}
|
|
|
|
await repository.fetchAll();
|
|
}
|
|
|
|
@command('git.pullFrom', { repository: true })
|
|
async pullFrom(repository: Repository): Promise<void> {
|
|
const remotes = repository.remotes;
|
|
|
|
if (remotes.length === 0) {
|
|
window.showWarningMessage(localize('no remotes to pull', "Your repository has no remotes configured to pull from."));
|
|
return;
|
|
}
|
|
|
|
const remotePicks = remotes.filter(r => r.fetchUrl !== undefined).map(r => ({ label: r.name, description: r.fetchUrl! }));
|
|
const placeHolder = localize('pick remote pull repo', "Pick a remote to pull the branch from");
|
|
const remotePick = await window.showQuickPick(remotePicks, { placeHolder });
|
|
|
|
if (!remotePick) {
|
|
return;
|
|
}
|
|
|
|
const remoteRefs = repository.refs;
|
|
const remoteRefsFiltered = remoteRefs.filter(r => (r.remote === remotePick.label));
|
|
const branchPicks = remoteRefsFiltered.map(r => ({ label: r.name! }));
|
|
const branchPlaceHolder = localize('pick branch pull', "Pick a branch to pull from");
|
|
const branchPick = await window.showQuickPick(branchPicks, { placeHolder: branchPlaceHolder });
|
|
|
|
if (!branchPick) {
|
|
return;
|
|
}
|
|
|
|
const remoteCharCnt = remotePick.label.length;
|
|
|
|
await repository.pullFrom(false, remotePick.label, branchPick.label.slice(remoteCharCnt + 1));
|
|
}
|
|
|
|
@command('git.pull', { repository: true })
|
|
async pull(repository: Repository): Promise<void> {
|
|
const remotes = repository.remotes;
|
|
|
|
if (remotes.length === 0) {
|
|
window.showWarningMessage(localize('no remotes to pull', "Your repository has no remotes configured to pull from."));
|
|
return;
|
|
}
|
|
|
|
await repository.pull(repository.HEAD);
|
|
}
|
|
|
|
@command('git.pullRebase', { repository: true })
|
|
async pullRebase(repository: Repository): Promise<void> {
|
|
const remotes = repository.remotes;
|
|
|
|
if (remotes.length === 0) {
|
|
window.showWarningMessage(localize('no remotes to pull', "Your repository has no remotes configured to pull from."));
|
|
return;
|
|
}
|
|
|
|
await repository.pullWithRebase(repository.HEAD);
|
|
}
|
|
|
|
private async _push(repository: Repository, pushOptions: PushOptions) {
|
|
const remotes = repository.remotes;
|
|
|
|
if (remotes.length === 0) {
|
|
if (pushOptions.silent) {
|
|
return;
|
|
}
|
|
|
|
const addRemote = localize('addremote', 'Add Remote');
|
|
const result = await window.showWarningMessage(localize('no remotes to push', "Your repository has no remotes configured to push to."), addRemote);
|
|
|
|
if (result === addRemote) {
|
|
await this.addRemote(repository);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
const config = workspace.getConfiguration('git', Uri.file(repository.root));
|
|
let forcePushMode: ForcePushMode | undefined = undefined;
|
|
|
|
if (pushOptions.forcePush) {
|
|
if (!config.get<boolean>('allowForcePush')) {
|
|
await window.showErrorMessage(localize('force push not allowed', "Force push is not allowed, please enable it with the 'git.allowForcePush' setting."));
|
|
return;
|
|
}
|
|
|
|
forcePushMode = config.get<boolean>('useForcePushWithLease') === true ? ForcePushMode.ForceWithLease : ForcePushMode.Force;
|
|
|
|
if (config.get<boolean>('confirmForcePush')) {
|
|
const message = localize('confirm force push', "You are about to force push your changes, this can be destructive and could inadvertently overwrite changes made by others.\n\nAre you sure to continue?");
|
|
const yes = localize('ok', "OK");
|
|
const neverAgain = localize('never ask again', "OK, Don't Ask Again");
|
|
const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);
|
|
|
|
if (pick === neverAgain) {
|
|
config.update('confirmForcePush', false, true);
|
|
} else if (pick !== yes) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pushOptions.pushType === PushType.PushFollowTags) {
|
|
await repository.pushFollowTags(undefined, forcePushMode);
|
|
return;
|
|
}
|
|
|
|
if (pushOptions.pushType === PushType.PushTags) {
|
|
await repository.pushTags(undefined, forcePushMode);
|
|
}
|
|
|
|
if (!repository.HEAD || !repository.HEAD.name) {
|
|
if (!pushOptions.silent) {
|
|
window.showWarningMessage(localize('nobranch', "Please check out a branch to push to a remote."));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (pushOptions.pushType === PushType.Push) {
|
|
try {
|
|
await repository.push(repository.HEAD, forcePushMode);
|
|
} catch (err) {
|
|
if (err.gitErrorCode !== GitErrorCodes.NoUpstreamBranch) {
|
|
throw err;
|
|
}
|
|
|
|
if (pushOptions.silent) {
|
|
return;
|
|
}
|
|
|
|
const branchName = repository.HEAD.name;
|
|
const message = localize('confirm publish branch', "The branch '{0}' has no upstream branch. Would you like to publish this branch?", branchName);
|
|
const yes = localize('ok', "OK");
|
|
const pick = await window.showWarningMessage(message, { modal: true }, yes);
|
|
|
|
if (pick === yes) {
|
|
await this.publish(repository);
|
|
}
|
|
}
|
|
} else {
|
|
const branchName = repository.HEAD.name;
|
|
if (!pushOptions.pushTo?.remote) {
|
|
const addRemote = new AddRemoteItem(this);
|
|
const picks = [...remotes.filter(r => r.pushUrl !== undefined).map(r => ({ label: r.name, description: r.pushUrl })), addRemote];
|
|
const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
|
|
const choice = await window.showQuickPick(picks, { placeHolder });
|
|
|
|
if (!choice) {
|
|
return;
|
|
}
|
|
|
|
if (choice === addRemote) {
|
|
const newRemote = await this.addRemote(repository);
|
|
|
|
if (newRemote) {
|
|
await repository.pushTo(newRemote, branchName, undefined, forcePushMode);
|
|
}
|
|
} else {
|
|
await repository.pushTo(choice.label, branchName, undefined, forcePushMode);
|
|
}
|
|
} else {
|
|
await repository.pushTo(pushOptions.pushTo.remote, pushOptions.pushTo.refspec || branchName, pushOptions.pushTo.setUpstream, forcePushMode);
|
|
}
|
|
}
|
|
}
|
|
|
|
@command('git.push', { repository: true })
|
|
async push(repository: Repository): Promise<void> {
|
|
await this._push(repository, { pushType: PushType.Push });
|
|
}
|
|
|
|
@command('git.pushForce', { repository: true })
|
|
async pushForce(repository: Repository): Promise<void> {
|
|
await this._push(repository, { pushType: PushType.Push, forcePush: true });
|
|
}
|
|
|
|
@command('git.pushWithTags', { repository: true })
|
|
async pushFollowTags(repository: Repository): Promise<void> {
|
|
await this._push(repository, { pushType: PushType.PushFollowTags });
|
|
}
|
|
|
|
@command('git.pushWithTagsForce', { repository: true })
|
|
async pushFollowTagsForce(repository: Repository): Promise<void> {
|
|
await this._push(repository, { pushType: PushType.PushFollowTags, forcePush: true });
|
|
}
|
|
|
|
@command('git.cherryPick', { repository: true })
|
|
async cherryPick(repository: Repository): Promise<void> {
|
|
const hash = await window.showInputBox({
|
|
placeHolder: localize('commit hash', "Commit Hash"),
|
|
prompt: localize('provide commit hash', "Please provide the commit hash"),
|
|
ignoreFocusOut: true
|
|
});
|
|
|
|
if (!hash) {
|
|
return;
|
|
}
|
|
|
|
await repository.cherryPick(hash);
|
|
}
|
|
|
|
@command('git.pushTo', { repository: true })
|
|
async pushTo(repository: Repository, remote?: string, refspec?: string, setUpstream?: boolean): Promise<void> {
|
|
await this._push(repository, { pushType: PushType.PushTo, pushTo: { remote: remote, refspec: refspec, setUpstream: setUpstream } });
|
|
}
|
|
|
|
@command('git.pushToForce', { repository: true })
|
|
async pushToForce(repository: Repository, remote?: string, refspec?: string, setUpstream?: boolean): Promise<void> {
|
|
await this._push(repository, { pushType: PushType.PushTo, pushTo: { remote: remote, refspec: refspec, setUpstream: setUpstream }, forcePush: true });
|
|
}
|
|
|
|
@command('git.pushTags', { repository: true })
|
|
async pushTags(repository: Repository): Promise<void> {
|
|
await this._push(repository, { pushType: PushType.PushTags });
|
|
}
|
|
|
|
@command('git.addRemote', { repository: true })
|
|
async addRemote(repository: Repository): Promise<string | undefined> {
|
|
const url = await pickRemoteSource(this.model, {
|
|
providerLabel: provider => localize('addfrom', "Add remote from {0}", provider.name),
|
|
urlLabel: localize('addFrom', "Add remote from URL")
|
|
});
|
|
|
|
if (!url) {
|
|
return;
|
|
}
|
|
|
|
const resultName = await window.showInputBox({
|
|
placeHolder: localize('remote name', "Remote name"),
|
|
prompt: localize('provide remote name', "Please provide a remote name"),
|
|
ignoreFocusOut: true,
|
|
validateInput: (name: string) => {
|
|
if (!sanitizeRemoteName(name)) {
|
|
return localize('remote name format invalid', "Remote name format invalid");
|
|
} else if (repository.remotes.find(r => r.name === name)) {
|
|
return localize('remote already exists', "Remote '{0}' already exists.", name);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
});
|
|
|
|
const name = sanitizeRemoteName(resultName || '');
|
|
|
|
if (!name) {
|
|
return;
|
|
}
|
|
|
|
await repository.addRemote(name, url);
|
|
await repository.fetch(name);
|
|
return name;
|
|
}
|
|
|
|
@command('git.removeRemote', { repository: true })
|
|
async removeRemote(repository: Repository): Promise<void> {
|
|
const remotes = repository.remotes;
|
|
|
|
if (remotes.length === 0) {
|
|
window.showErrorMessage(localize('no remotes added', "Your repository has no remotes."));
|
|
return;
|
|
}
|
|
|
|
const picks = remotes.map(r => r.name);
|
|
const placeHolder = localize('remove remote', "Pick a remote to remove");
|
|
|
|
const remoteName = await window.showQuickPick(picks, { placeHolder });
|
|
|
|
if (!remoteName) {
|
|
return;
|
|
}
|
|
|
|
await repository.removeRemote(remoteName);
|
|
}
|
|
|
|
private async _sync(repository: Repository, rebase: boolean): Promise<void> {
|
|
const HEAD = repository.HEAD;
|
|
|
|
if (!HEAD) {
|
|
return;
|
|
} else if (!HEAD.upstream) {
|
|
const branchName = HEAD.name;
|
|
const message = localize('confirm publish branch', "The branch '{0}' has no upstream branch. Would you like to publish this branch?", branchName);
|
|
const yes = localize('ok', "OK");
|
|
const pick = await window.showWarningMessage(message, { modal: true }, yes);
|
|
|
|
if (pick === yes) {
|
|
await this.publish(repository);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const remoteName = HEAD.remote || HEAD.upstream.remote;
|
|
const remote = repository.remotes.find(r => r.name === remoteName);
|
|
const isReadonly = remote && remote.isReadOnly;
|
|
|
|
const config = workspace.getConfiguration('git');
|
|
const shouldPrompt = !isReadonly && config.get<boolean>('confirmSync') === true;
|
|
|
|
if (shouldPrompt) {
|
|
const message = localize('sync is unpredictable', "This action will push and pull commits to and from '{0}/{1}'.", HEAD.upstream.remote, HEAD.upstream.name);
|
|
const yes = localize('ok', "OK");
|
|
const neverAgain = localize('never again', "OK, Don't Show Again");
|
|
const pick = await window.showWarningMessage(message, { modal: true }, yes, neverAgain);
|
|
|
|
if (pick === neverAgain) {
|
|
await config.update('confirmSync', false, true);
|
|
} else if (pick !== yes) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (rebase) {
|
|
await repository.syncRebase(HEAD);
|
|
} else {
|
|
await repository.sync(HEAD);
|
|
}
|
|
}
|
|
|
|
@command('git.sync', { repository: true })
|
|
async sync(repository: Repository): Promise<void> {
|
|
try {
|
|
await this._sync(repository, false);
|
|
} catch (err) {
|
|
if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
|
|
return;
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
@command('git._syncAll')
|
|
async syncAll(): Promise<void> {
|
|
await Promise.all(this.model.repositories.map(async repository => {
|
|
const HEAD = repository.HEAD;
|
|
|
|
if (!HEAD || !HEAD.upstream) {
|
|
return;
|
|
}
|
|
|
|
await repository.sync(HEAD);
|
|
}));
|
|
}
|
|
|
|
@command('git.syncRebase', { repository: true })
|
|
async syncRebase(repository: Repository): Promise<void> {
|
|
try {
|
|
await this._sync(repository, true);
|
|
} catch (err) {
|
|
if (/Cancelled/i.test(err && (err.message || err.stderr || ''))) {
|
|
return;
|
|
}
|
|
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
@command('git.publish', { repository: true })
|
|
async publish(repository: Repository): Promise<void> {
|
|
const branchName = repository.HEAD && repository.HEAD.name || '';
|
|
const remotes = repository.remotes;
|
|
|
|
if (remotes.length === 0) {
|
|
const providers = this.model.getRemoteProviders().filter(p => !!p.publishRepository);
|
|
|
|
if (providers.length === 0) {
|
|
window.showWarningMessage(localize('no remotes to publish', "Your repository has no remotes configured to publish to."));
|
|
return;
|
|
}
|
|
|
|
let provider: RemoteSourceProvider;
|
|
|
|
if (providers.length === 1) {
|
|
provider = providers[0];
|
|
} else {
|
|
const picks = providers
|
|
.map(provider => ({ label: (provider.icon ? `$(${provider.icon}) ` : '') + localize('publish to', "Publish to {0}", provider.name), alwaysShow: true, provider }));
|
|
const placeHolder = localize('pick provider', "Pick a provider to publish the branch '{0}' to:", branchName);
|
|
const choice = await window.showQuickPick(picks, { placeHolder });
|
|
|
|
if (!choice) {
|
|
return;
|
|
}
|
|
|
|
provider = choice.provider;
|
|
}
|
|
|
|
await provider.publishRepository!(new ApiRepository(repository));
|
|
this.model.firePublishEvent(repository, branchName);
|
|
|
|
return;
|
|
}
|
|
|
|
if (remotes.length === 1) {
|
|
await repository.pushTo(remotes[0].name, branchName, true);
|
|
this.model.firePublishEvent(repository, branchName);
|
|
|
|
return;
|
|
}
|
|
|
|
const addRemote = new AddRemoteItem(this);
|
|
const picks = [...repository.remotes.map(r => ({ label: r.name, description: r.pushUrl })), addRemote];
|
|
const placeHolder = localize('pick remote', "Pick a remote to publish the branch '{0}' to:", branchName);
|
|
const choice = await window.showQuickPick(picks, { placeHolder });
|
|
|
|
if (!choice) {
|
|
return;
|
|
}
|
|
|
|
if (choice === addRemote) {
|
|
const newRemote = await this.addRemote(repository);
|
|
|
|
if (newRemote) {
|
|
await repository.pushTo(newRemote, branchName, true);
|
|
|
|
this.model.firePublishEvent(repository, branchName);
|
|
}
|
|
} else {
|
|
await repository.pushTo(choice.label, branchName, true);
|
|
|
|
this.model.firePublishEvent(repository, branchName);
|
|
}
|
|
}
|
|
|
|
@command('git.ignore')
|
|
async ignore(...resourceStates: SourceControlResourceState[]): Promise<void> {
|
|
resourceStates = resourceStates.filter(s => !!s);
|
|
|
|
if (resourceStates.length === 0 || (resourceStates[0] && !(resourceStates[0].resourceUri instanceof Uri))) {
|
|
const resource = this.getSCMResource();
|
|
|
|
if (!resource) {
|
|
return;
|
|
}
|
|
|
|
resourceStates = [resource];
|
|
}
|
|
|
|
const resources = resourceStates
|
|
.filter(s => s instanceof Resource)
|
|
.map(r => r.resourceUri);
|
|
|
|
if (!resources.length) {
|
|
return;
|
|
}
|
|
|
|
await this.runByRepository(resources, async (repository, resources) => repository.ignore(resources));
|
|
}
|
|
|
|
@command('git.revealInExplorer')
|
|
async revealInExplorer(resourceState: SourceControlResourceState): Promise<void> {
|
|
if (!resourceState) {
|
|
return;
|
|
}
|
|
|
|
if (!(resourceState.resourceUri instanceof Uri)) {
|
|
return;
|
|
}
|
|
|
|
await commands.executeCommand('revealInExplorer', resourceState.resourceUri);
|
|
}
|
|
|
|
private async _stash(repository: Repository, includeUntracked = false): Promise<void> {
|
|
const noUnstagedChanges = repository.workingTreeGroup.resourceStates.length === 0
|
|
&& (!includeUntracked || repository.untrackedGroup.resourceStates.length === 0);
|
|
const noStagedChanges = repository.indexGroup.resourceStates.length === 0;
|
|
|
|
if (noUnstagedChanges && noStagedChanges) {
|
|
window.showInformationMessage(localize('no changes stash', "There are no changes to stash."));
|
|
return;
|
|
}
|
|
|
|
const config = workspace.getConfiguration('git', Uri.file(repository.root));
|
|
const promptToSaveFilesBeforeStashing = config.get<'always' | 'staged' | 'never'>('promptToSaveFilesBeforeStash');
|
|
|
|
if (promptToSaveFilesBeforeStashing !== 'never') {
|
|
let documents = workspace.textDocuments
|
|
.filter(d => !d.isUntitled && d.isDirty && isDescendant(repository.root, d.uri.fsPath));
|
|
|
|
if (promptToSaveFilesBeforeStashing === 'staged' || repository.indexGroup.resourceStates.length > 0) {
|
|
documents = documents
|
|
.filter(d => repository.indexGroup.resourceStates.some(s => pathEquals(s.resourceUri.fsPath, d.uri.fsPath)));
|
|
}
|
|
|
|
if (documents.length > 0) {
|
|
const message = documents.length === 1
|
|
? localize('unsaved stash files single', "The following file has unsaved changes which won't be included in the stash if you proceed: {0}.\n\nWould you like to save it before stashing?", path.basename(documents[0].uri.fsPath))
|
|
: localize('unsaved stash files', "There are {0} unsaved files.\n\nWould you like to save them before stashing?", documents.length);
|
|
const saveAndStash = localize('save and stash', "Save All & Stash");
|
|
const stash = localize('stash', "Stash Anyway");
|
|
const pick = await window.showWarningMessage(message, { modal: true }, saveAndStash, stash);
|
|
|
|
if (pick === saveAndStash) {
|
|
await Promise.all(documents.map(d => d.save()));
|
|
} else if (pick !== stash) {
|
|
return; // do not stash on cancel
|
|
}
|
|
}
|
|
}
|
|
|
|
let message: string | undefined;
|
|
|
|
if (config.get<boolean>('useCommitInputAsStashMessage') && (!repository.sourceControl.commitTemplate || repository.inputBox.value !== repository.sourceControl.commitTemplate)) {
|
|
message = repository.inputBox.value;
|
|
}
|
|
|
|
message = await window.showInputBox({
|
|
value: message,
|
|
prompt: localize('provide stash message', "Optionally provide a stash message"),
|
|
placeHolder: localize('stash message', "Stash message")
|
|
});
|
|
|
|
if (typeof message === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
await repository.createStash(message, includeUntracked);
|
|
}
|
|
|
|
@command('git.stash', { repository: true })
|
|
stash(repository: Repository): Promise<void> {
|
|
return this._stash(repository);
|
|
}
|
|
|
|
@command('git.stashIncludeUntracked', { repository: true })
|
|
stashIncludeUntracked(repository: Repository): Promise<void> {
|
|
return this._stash(repository, true);
|
|
}
|
|
|
|
@command('git.stashPop', { repository: true })
|
|
async stashPop(repository: Repository): Promise<void> {
|
|
const placeHolder = localize('pick stash to pop', "Pick a stash to pop");
|
|
const stash = await this.pickStash(repository, placeHolder);
|
|
|
|
if (!stash) {
|
|
return;
|
|
}
|
|
|
|
await repository.popStash(stash.index);
|
|
}
|
|
|
|
@command('git.stashPopLatest', { repository: true })
|
|
async stashPopLatest(repository: Repository): Promise<void> {
|
|
const stashes = await repository.getStashes();
|
|
|
|
if (stashes.length === 0) {
|
|
window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
|
|
return;
|
|
}
|
|
|
|
await repository.popStash();
|
|
}
|
|
|
|
@command('git.stashApply', { repository: true })
|
|
async stashApply(repository: Repository): Promise<void> {
|
|
const placeHolder = localize('pick stash to apply', "Pick a stash to apply");
|
|
const stash = await this.pickStash(repository, placeHolder);
|
|
|
|
if (!stash) {
|
|
return;
|
|
}
|
|
|
|
await repository.applyStash(stash.index);
|
|
}
|
|
|
|
@command('git.stashApplyLatest', { repository: true })
|
|
async stashApplyLatest(repository: Repository): Promise<void> {
|
|
const stashes = await repository.getStashes();
|
|
|
|
if (stashes.length === 0) {
|
|
window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
|
|
return;
|
|
}
|
|
|
|
await repository.applyStash();
|
|
}
|
|
|
|
@command('git.stashDrop', { repository: true })
|
|
async stashDrop(repository: Repository): Promise<void> {
|
|
const placeHolder = localize('pick stash to drop', "Pick a stash to drop");
|
|
const stash = await this.pickStash(repository, placeHolder);
|
|
|
|
if (!stash) {
|
|
return;
|
|
}
|
|
|
|
// request confirmation for the operation
|
|
const yes = localize('yes', "Yes");
|
|
const result = await window.showWarningMessage(
|
|
localize('sure drop', "Are you sure you want to drop the stash: {0}?", stash.description),
|
|
yes
|
|
);
|
|
if (result !== yes) {
|
|
return;
|
|
}
|
|
|
|
await repository.dropStash(stash.index);
|
|
}
|
|
|
|
private async pickStash(repository: Repository, placeHolder: string): Promise<Stash | undefined> {
|
|
const stashes = await repository.getStashes();
|
|
|
|
if (stashes.length === 0) {
|
|
window.showInformationMessage(localize('no stashes', "There are no stashes in the repository."));
|
|
return;
|
|
}
|
|
|
|
const picks = stashes.map(stash => ({ label: `#${stash.index}: ${stash.description}`, description: '', details: '', stash }));
|
|
const result = await window.showQuickPick(picks, { placeHolder });
|
|
return result && result.stash;
|
|
}
|
|
|
|
@command('git.timeline.openDiff', { repository: false })
|
|
async timelineOpenDiff(item: TimelineItem, uri: Uri | undefined, _source: string) {
|
|
if (uri === undefined || uri === null || !GitTimelineItem.is(item)) {
|
|
return undefined;
|
|
}
|
|
|
|
const basename = path.basename(uri.fsPath);
|
|
|
|
let title;
|
|
if ((item.previousRef === 'HEAD' || item.previousRef === '~') && item.ref === '') {
|
|
title = localize('git.title.workingTree', '{0} (Working Tree)', basename);
|
|
}
|
|
else if (item.previousRef === 'HEAD' && item.ref === '~') {
|
|
title = localize('git.title.index', '{0} (Index)', basename);
|
|
} else {
|
|
title = localize('git.title.diffRefs', '{0} ({1}) ⟷ {0} ({2})', basename, item.shortPreviousRef, item.shortRef);
|
|
}
|
|
|
|
const options: TextDocumentShowOptions = {
|
|
preserveFocus: true,
|
|
preview: true,
|
|
viewColumn: ViewColumn.Active
|
|
};
|
|
|
|
return commands.executeCommand('vscode.diff', toGitUri(uri, item.previousRef), item.ref === '' ? uri : toGitUri(uri, item.ref), title, options);
|
|
}
|
|
|
|
@command('git.timeline.copyCommitId', { repository: false })
|
|
async timelineCopyCommitId(item: TimelineItem, _uri: Uri | undefined, _source: string) {
|
|
if (!GitTimelineItem.is(item)) {
|
|
return;
|
|
}
|
|
|
|
env.clipboard.writeText(item.ref);
|
|
}
|
|
|
|
@command('git.timeline.copyCommitMessage', { repository: false })
|
|
async timelineCopyCommitMessage(item: TimelineItem, _uri: Uri | undefined, _source: string) {
|
|
if (!GitTimelineItem.is(item)) {
|
|
return;
|
|
}
|
|
|
|
env.clipboard.writeText(item.message);
|
|
}
|
|
|
|
@command('git.rebaseAbort', { repository: true })
|
|
async rebaseAbort(repository: Repository): Promise<void> {
|
|
if (repository.rebaseCommit) {
|
|
await repository.rebaseAbort();
|
|
} else {
|
|
await window.showInformationMessage(localize('no rebase', "No rebase in progress."));
|
|
}
|
|
}
|
|
|
|
private createCommand(id: string, key: string, method: Function, options: CommandOptions): (...args: any[]) => any {
|
|
const result = (...args: any[]) => {
|
|
let result: Promise<any>;
|
|
|
|
if (!options.repository) {
|
|
result = Promise.resolve(method.apply(this, args));
|
|
} else {
|
|
// try to guess the repository based on the first argument
|
|
const repository = this.model.getRepository(args[0]);
|
|
let repositoryPromise: Promise<Repository | undefined>;
|
|
|
|
if (repository) {
|
|
repositoryPromise = Promise.resolve(repository);
|
|
} else if (this.model.repositories.length === 1) {
|
|
repositoryPromise = Promise.resolve(this.model.repositories[0]);
|
|
} else {
|
|
repositoryPromise = this.model.pickRepository();
|
|
}
|
|
|
|
result = repositoryPromise.then(repository => {
|
|
if (!repository) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
return Promise.resolve(method.apply(this, [repository, ...args.slice(1)]));
|
|
});
|
|
}
|
|
|
|
/* __GDPR__
|
|
"git.command" : {
|
|
"command" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
|
|
}
|
|
*/
|
|
this.telemetryReporter.sendTelemetryEvent('git.command', { command: id });
|
|
|
|
return result.catch(async err => {
|
|
const options: MessageOptions = {
|
|
modal: true
|
|
};
|
|
|
|
let message: string;
|
|
let type: 'error' | 'warning' = '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 showCommandOutputChoice = localize('show command output', "Show Command Output");
|
|
if (err.stderr) {
|
|
choices.set(showCommandOutputChoice, async () => {
|
|
const timestamp = new Date().getTime();
|
|
const uri = Uri.parse(`git-output:/git-error-${timestamp}`);
|
|
|
|
let command = 'git';
|
|
|
|
if (err.gitArgs) {
|
|
command = `${command} ${err.gitArgs.join(' ')}`;
|
|
} else if (err.gitCommand) {
|
|
command = `${command} ${err.gitCommand}`;
|
|
}
|
|
|
|
this.commandErrors.set(uri, `> ${command}\n${err.stderr}`);
|
|
|
|
try {
|
|
const doc = await workspace.openTextDocument(uri);
|
|
await window.showTextDocument(doc);
|
|
} finally {
|
|
this.commandErrors.delete(uri);
|
|
}
|
|
});
|
|
}
|
|
|
|
switch (err.gitErrorCode) {
|
|
case GitErrorCodes.DirtyWorkTree:
|
|
message = localize('clean repo', "Please clean your repository working tree before checkout.");
|
|
break;
|
|
case GitErrorCodes.PushRejected:
|
|
message = localize('cant push', "Can't push refs to remote. Try running 'Pull' first to integrate your changes.");
|
|
break;
|
|
case GitErrorCodes.Conflict:
|
|
message = localize('merge conflicts', "There are merge conflicts. Resolve them before committing.");
|
|
type = 'warning';
|
|
options.modal = false;
|
|
break;
|
|
case GitErrorCodes.StashConflict:
|
|
message = localize('stash merge conflicts', "There were merge conflicts while applying the stash.");
|
|
type = 'warning';
|
|
options.modal = false;
|
|
break;
|
|
case GitErrorCodes.AuthenticationFailed:
|
|
const regex = /Authentication failed for '(.*)'/i;
|
|
const match = regex.exec(err.stderr || String(err));
|
|
|
|
message = match
|
|
? localize('auth failed specific', "Failed to authenticate to git remote:\n\n{0}", match[1])
|
|
: localize('auth failed', "Failed to authenticate to git remote.");
|
|
break;
|
|
case GitErrorCodes.NoUserNameConfigured:
|
|
case GitErrorCodes.NoUserEmailConfigured:
|
|
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://git-scm.com/book/en/v2/Getting-Started-First-Time-Git-Setup')));
|
|
break;
|
|
default:
|
|
const hint = (err.stderr || err.message || String(err))
|
|
.replace(/^error: /mi, '')
|
|
.replace(/^> husky.*$/mi, '')
|
|
.split(/[\r\n]/)
|
|
.filter((line: string) => !!line)
|
|
[0];
|
|
|
|
message = hint
|
|
? localize('git error details', "Git: {0}", hint)
|
|
: localize('git error', "Git error");
|
|
|
|
break;
|
|
}
|
|
|
|
if (!message) {
|
|
console.error(err);
|
|
return;
|
|
}
|
|
|
|
const allChoices = Array.from(choices.keys());
|
|
const result = type === 'error'
|
|
? await window.showErrorMessage(message, options, ...allChoices)
|
|
: await window.showWarningMessage(message, options, ...allChoices);
|
|
|
|
if (result) {
|
|
const resultFn = choices.get(result);
|
|
|
|
if (resultFn) {
|
|
resultFn();
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
// patch this object, so people can call methods directly
|
|
(this as any)[key] = result;
|
|
|
|
return result;
|
|
}
|
|
|
|
private getSCMResource(uri?: Uri): Resource | undefined {
|
|
uri = uri ? uri : (window.activeTextEditor && window.activeTextEditor.document.uri);
|
|
|
|
this.outputChannel.appendLine(`git.getSCMResource.uri ${uri && uri.toString()}`);
|
|
|
|
for (const r of this.model.repositories.map(r => r.root)) {
|
|
this.outputChannel.appendLine(`repo root ${r}`);
|
|
}
|
|
|
|
if (!uri) {
|
|
return undefined;
|
|
}
|
|
|
|
if (isGitUri(uri)) {
|
|
const { path } = fromGitUri(uri);
|
|
uri = Uri.file(path);
|
|
}
|
|
|
|
if (uri.scheme === 'file') {
|
|
const uriString = uri.toString();
|
|
const repository = this.model.getRepository(uri);
|
|
|
|
if (!repository) {
|
|
return undefined;
|
|
}
|
|
|
|
return repository.workingTreeGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0]
|
|
|| repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString)[0];
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
private runByRepository<T>(resource: Uri, fn: (repository: Repository, resource: Uri) => Promise<T>): Promise<T[]>;
|
|
private runByRepository<T>(resources: Uri[], fn: (repository: Repository, resources: Uri[]) => Promise<T>): Promise<T[]>;
|
|
private async runByRepository<T>(arg: Uri | Uri[], fn: (repository: Repository, resources: any) => Promise<T>): Promise<T[]> {
|
|
const resources = arg instanceof Uri ? [arg] : arg;
|
|
const isSingleResource = arg instanceof Uri;
|
|
|
|
const groups = resources.reduce((result, resource) => {
|
|
let repository = this.model.getRepository(resource);
|
|
|
|
if (!repository) {
|
|
console.warn('Could not find git repository for ', resource);
|
|
return result;
|
|
}
|
|
|
|
// Could it be a submodule?
|
|
if (pathEquals(resource.fsPath, repository.root)) {
|
|
repository = this.model.getRepositoryForSubmodule(resource) || repository;
|
|
}
|
|
|
|
const tuple = result.filter(p => p.repository === repository)[0];
|
|
|
|
if (tuple) {
|
|
tuple.resources.push(resource);
|
|
} else {
|
|
result.push({ repository, resources: [resource] });
|
|
}
|
|
|
|
return result;
|
|
}, [] as { repository: Repository, resources: Uri[] }[]);
|
|
|
|
const promises = groups
|
|
.map(({ repository, resources }) => fn(repository as Repository, isSingleResource ? resources[0] : resources));
|
|
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
dispose(): void {
|
|
this.disposables.forEach(d => d.dispose());
|
|
}
|
|
}
|