mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-28 04:23:32 +01:00
Merge branch 'master' into pr/104312
This commit is contained in:
1
extensions/git/src/api/git.d.ts
vendored
1
extensions/git/src/api/git.d.ts
vendored
@@ -130,6 +130,7 @@ export interface CommitOptions {
|
||||
signoff?: boolean;
|
||||
signCommit?: boolean;
|
||||
empty?: boolean;
|
||||
noVerify?: boolean;
|
||||
}
|
||||
|
||||
export interface BranchQuery {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { lstat, Stats } from 'fs';
|
||||
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 } from 'vscode';
|
||||
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, GitErrorCodes, Ref, RefType, Status, CommitOptions, RemoteSourceProvider } from './api/git';
|
||||
@@ -31,14 +31,14 @@ class CheckoutItem implements QuickPickItem {
|
||||
|
||||
constructor(protected ref: Ref) { }
|
||||
|
||||
async run(repository: Repository): Promise<void> {
|
||||
async run(repository: Repository, opts?: { detached?: boolean }): Promise<void> {
|
||||
const ref = this.ref.name;
|
||||
|
||||
if (!ref) {
|
||||
return;
|
||||
}
|
||||
|
||||
await repository.checkout(ref);
|
||||
await repository.checkout(ref, opts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,32 +99,36 @@ class MergeItem implements QuickPickItem {
|
||||
}
|
||||
}
|
||||
|
||||
class CreateBranchItem implements QuickPickItem {
|
||||
class RebaseItem implements QuickPickItem {
|
||||
|
||||
constructor(private cc: CommandCenter) { }
|
||||
get label(): string { return this.ref.name || ''; }
|
||||
description: string = '';
|
||||
|
||||
get label(): string { return '$(plus) ' + localize('create branch', 'Create new branch...'); }
|
||||
get description(): string { return ''; }
|
||||
|
||||
get alwaysShow(): boolean { return true; }
|
||||
constructor(readonly ref: Ref) { }
|
||||
|
||||
async run(repository: Repository): Promise<void> {
|
||||
await this.cc.branch(repository);
|
||||
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 {
|
||||
|
||||
constructor(private cc: CommandCenter) { }
|
||||
|
||||
get label(): string { return '$(plus) ' + localize('create branch from', 'Create new branch from...'); }
|
||||
get description(): string { return ''; }
|
||||
|
||||
get alwaysShow(): boolean { return true; }
|
||||
}
|
||||
|
||||
async run(repository: Repository): Promise<void> {
|
||||
await this.cc.branch(repository);
|
||||
}
|
||||
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 {
|
||||
@@ -203,18 +207,53 @@ async function categorizeResourceByResolution(resources: Resource[]): Promise<{
|
||||
|
||||
function createCheckoutItems(repository: Repository): CheckoutItem[] {
|
||||
const config = workspace.getConfiguration('git');
|
||||
const checkoutType = config.get<string>('checkoutType') || 'all';
|
||||
const includeTags = checkoutType === 'all' || checkoutType === 'tags';
|
||||
const includeRemotes = checkoutType === 'all' || checkoutType === 'remote';
|
||||
const checkoutTypeConfig = config.get<string | string[]>('checkoutType');
|
||||
let checkoutTypes: string[];
|
||||
|
||||
const heads = repository.refs.filter(ref => ref.type === RefType.Head)
|
||||
.map(ref => new CheckoutItem(ref));
|
||||
const tags = (includeTags ? repository.refs.filter(ref => ref.type === RefType.Tag) : [])
|
||||
.map(ref => new CheckoutTagItem(ref));
|
||||
const remoteHeads = (includeRemotes ? repository.refs.filter(ref => ref.type === RefType.RemoteHead) : [])
|
||||
.map(ref => new CheckoutRemoteHeadItem(ref));
|
||||
if (checkoutTypeConfig === 'all' || !checkoutTypeConfig || checkoutTypeConfig.length === 0) {
|
||||
checkoutTypes = ['local', 'remote', 'tags'];
|
||||
} else if (typeof checkoutTypeConfig === 'string') {
|
||||
checkoutTypes = [checkoutTypeConfig];
|
||||
} else {
|
||||
checkoutTypes = checkoutTypeConfig;
|
||||
}
|
||||
|
||||
return [...heads, ...tags, ...remoteHeads];
|
||||
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) {
|
||||
@@ -232,6 +271,7 @@ enum PushType {
|
||||
Push,
|
||||
PushTo,
|
||||
PushFollowTags,
|
||||
PushTags
|
||||
}
|
||||
|
||||
interface PushOptions {
|
||||
@@ -240,9 +280,27 @@ interface PushOptions {
|
||||
silent?: 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,
|
||||
@@ -259,6 +317,8 @@ export class CommandCenter {
|
||||
return commands.registerCommand(commandId, command);
|
||||
}
|
||||
});
|
||||
|
||||
this.disposables.push(workspace.registerTextDocumentContentProvider('git-output', this.commandErrors));
|
||||
}
|
||||
|
||||
@command('git.setLogLevel')
|
||||
@@ -333,7 +393,9 @@ export class CommandCenter {
|
||||
right = toGitUri(resource.resourceUri, resource.resourceGroupType === ResourceGroupType.Index ? 'index' : 'wt', { submoduleOf: repository.root });
|
||||
}
|
||||
} else {
|
||||
if (resource.type !== Status.DELETED_BY_THEM) {
|
||||
if (resource.type === Status.DELETED_BY_US || resource.type === Status.DELETED_BY_THEM) {
|
||||
left = toGitUri(resource.resourceUri, '~1');
|
||||
} else {
|
||||
left = this.getLeftResource(resource);
|
||||
}
|
||||
|
||||
@@ -458,9 +520,8 @@ export class CommandCenter {
|
||||
}
|
||||
}
|
||||
|
||||
@command('git.clone')
|
||||
async clone(url?: string, parentPath?: string): Promise<void> {
|
||||
if (!url) {
|
||||
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")
|
||||
@@ -515,38 +576,57 @@ export class CommandCenter {
|
||||
|
||||
const repositoryPath = await window.withProgress(
|
||||
opts,
|
||||
(progress, token) => this.git.clone(url!, parentPath!, progress, token)
|
||||
(progress, token) => this.git.clone(url!, { parentPath: parentPath!, progress, recursive: options.recursive }, token)
|
||||
);
|
||||
|
||||
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 config = workspace.getConfiguration('git');
|
||||
const openAfterClone = config.get<'always' | 'alwaysNewWindow' | 'whenNoFolderOpen' | 'prompt'>('openAfterClone');
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
|
||||
const result = await window.showInformationMessage(message, ...choices);
|
||||
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;
|
||||
}
|
||||
|
||||
const openFolder = result === open;
|
||||
/* __GDPR__
|
||||
"clone" : {
|
||||
"outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
|
||||
"openFolder": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true }
|
||||
}
|
||||
*/
|
||||
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: openFolder ? 1 : 0 });
|
||||
this.telemetryReporter.sendTelemetryEvent('clone', { outcome: 'success' }, { openFolder: action === PostCloneAction.Open || action === PostCloneAction.OpenNewWindow ? 1 : 0 });
|
||||
|
||||
const uri = Uri.file(repositoryPath);
|
||||
|
||||
if (openFolder) {
|
||||
if (action === PostCloneAction.Open) {
|
||||
commands.executeCommand('vscode.openFolder', uri, { forceReuseWindow: true });
|
||||
} else if (result === addToWorkspace) {
|
||||
} else if (action === PostCloneAction.AddToWorkspace) {
|
||||
workspace.updateWorkspaceFolders(workspace.workspaceFolders!.length, 0, { uri });
|
||||
} else if (result === openNewWindow) {
|
||||
} else if (action === PostCloneAction.OpenNewWindow) {
|
||||
commands.executeCommand('vscode.openFolder', uri, { forceNewWindow: true });
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -572,6 +652,16 @@ export class CommandCenter {
|
||||
}
|
||||
}
|
||||
|
||||
@command('git.clone')
|
||||
async clone(url?: string, parentPath?: string): Promise<void> {
|
||||
this.cloneRepository(url, parentPath);
|
||||
}
|
||||
|
||||
@command('git.cloneRecursive')
|
||||
async cloneRecursive(url?: string, parentPath?: string): Promise<void> {
|
||||
this.cloneRepository(url, parentPath, { recursive: true });
|
||||
}
|
||||
|
||||
@command('git.init')
|
||||
async init(skipFolderPrompt = false): Promise<void> {
|
||||
let repositoryPath: string | undefined = undefined;
|
||||
@@ -830,6 +920,27 @@ export class CommandCenter {
|
||||
}
|
||||
}
|
||||
|
||||
@command('git.rename', { repository: true })
|
||||
async rename(repository: Repository, fromUri: Uri): Promise<void> {
|
||||
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}`);
|
||||
@@ -1002,6 +1113,9 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
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 })
|
||||
@@ -1049,6 +1163,9 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
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 })
|
||||
@@ -1070,7 +1187,9 @@ export class CommandCenter {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectionsBeforeRevert = textEditor.selections;
|
||||
await this._revertChanges(textEditor, selectedChanges);
|
||||
textEditor.selections = selectionsBeforeRevert;
|
||||
}
|
||||
|
||||
private async _revertChanges(textEditor: TextEditor, changes: LineChange[]): Promise<void> {
|
||||
@@ -1083,7 +1202,6 @@ export class CommandCenter {
|
||||
|
||||
const originalUri = toGitUri(modifiedUri, '~');
|
||||
const originalDocument = await workspace.openTextDocument(originalUri);
|
||||
const selectionsBeforeRevert = textEditor.selections;
|
||||
const visibleRangesBeforeRevert = textEditor.visibleRanges;
|
||||
const result = applyLineChanges(originalDocument, modifiedDocument, changes);
|
||||
|
||||
@@ -1093,7 +1211,6 @@ export class CommandCenter {
|
||||
|
||||
await modifiedDocument.save();
|
||||
|
||||
textEditor.selections = selectionsBeforeRevert;
|
||||
textEditor.revealRange(visibleRangesBeforeRevert[0]);
|
||||
}
|
||||
|
||||
@@ -1192,7 +1309,7 @@ export class CommandCenter {
|
||||
|
||||
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.", path.basename(scmResources[0].resourceUri.fsPath));
|
||||
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) {
|
||||
@@ -1297,7 +1414,7 @@ export class CommandCenter {
|
||||
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.", resources.length);
|
||||
: 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);
|
||||
@@ -1311,7 +1428,7 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
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.", path.basename(resource.resourceUri.fsPath));
|
||||
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);
|
||||
|
||||
@@ -1323,7 +1440,7 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
private async _cleanUntrackedChanges(repository: Repository, resources: Resource[]): Promise<void> {
|
||||
const message = localize('confirm delete multiple', "Are you sure you want to DELETE {0} files?", resources.length);
|
||||
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);
|
||||
|
||||
@@ -1389,7 +1506,7 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
// 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 automatically stage all your changes and commit them directly?");
|
||||
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");
|
||||
@@ -1429,10 +1546,38 @@ export class CommandCenter {
|
||||
// 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
|
||||
) {
|
||||
window.showInformationMessage(localize('no changes', "There are no changes to commit."));
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const message = await getCommitMessage();
|
||||
@@ -1539,8 +1684,7 @@ export class CommandCenter {
|
||||
await this.commitWithAnyInput(repository, { all: true, amend: true });
|
||||
}
|
||||
|
||||
@command('git.commitEmpty', { repository: true })
|
||||
async commitEmpty(repository: Repository): Promise<void> {
|
||||
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;
|
||||
@@ -1558,7 +1702,52 @@ export class CommandCenter {
|
||||
}
|
||||
}
|
||||
|
||||
await this.commitWithAnyInput(repository, { empty: true });
|
||||
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 })
|
||||
@@ -1597,20 +1786,38 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
@command('git.checkout', { repository: true })
|
||||
async checkout(repository: Repository, treeish: string): Promise<boolean> {
|
||||
if (typeof treeish === 'string') {
|
||||
await repository.checkout(treeish);
|
||||
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(this);
|
||||
const createBranchFrom = new CreateBranchFromItem(this);
|
||||
const picks = [createBranch, createBranchFrom, ...createCheckoutItems(repository)];
|
||||
const placeHolder = localize('select a ref to checkout', 'Select a ref to checkout');
|
||||
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 = placeHolder;
|
||||
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])));
|
||||
@@ -1624,8 +1831,31 @@ export class CommandCenter {
|
||||
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 {
|
||||
await (choice as CheckoutItem).run(repository);
|
||||
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;
|
||||
@@ -1778,6 +2008,44 @@ export class CommandCenter {
|
||||
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>('checkoutType') || 'all';
|
||||
const includeRemotes = checkoutType === 'all' || checkoutType === '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({
|
||||
@@ -1797,8 +2065,7 @@ export class CommandCenter {
|
||||
});
|
||||
|
||||
const name = inputTagName.replace(/^\.|\/\.|\.\.|~|\^|:|\/$|\.lock$|\.lock\/|\\|\*|\s|^\s*$|\.$/g, '-');
|
||||
const message = inputMessage || name;
|
||||
await repository.tag(name, message);
|
||||
await repository.tag(name, inputMessage);
|
||||
}
|
||||
|
||||
@command('git.deleteTag', { repository: true })
|
||||
@@ -1956,6 +2223,10 @@ export class CommandCenter {
|
||||
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."));
|
||||
@@ -2037,6 +2308,11 @@ export class CommandCenter {
|
||||
await this._push(repository, { pushType: PushType.PushTo, 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, {
|
||||
@@ -2070,6 +2346,7 @@ export class CommandCenter {
|
||||
}
|
||||
|
||||
await repository.addRemote(name, url);
|
||||
await repository.fetch(name);
|
||||
return name;
|
||||
}
|
||||
|
||||
@@ -2283,6 +2560,34 @@ export class CommandCenter {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const message = await this.getStashMessage();
|
||||
|
||||
if (typeof message === 'undefined') {
|
||||
@@ -2366,6 +2671,16 @@ export class CommandCenter {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2429,7 +2744,11 @@ export class CommandCenter {
|
||||
|
||||
@command('git.rebaseAbort', { repository: true })
|
||||
async rebaseAbort(repository: Repository): Promise<void> {
|
||||
await repository.rebaseAbort();
|
||||
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 {
|
||||
@@ -2480,6 +2799,31 @@ export class CommandCenter {
|
||||
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.");
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { window, workspace, Uri, Disposable, Event, EventEmitter, Decoration, DecorationProvider, ThemeColor } from 'vscode';
|
||||
import { window, workspace, Uri, Disposable, Event, EventEmitter, FileDecoration, FileDecorationProvider, ThemeColor } from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { Repository, GitResourceGroup } from './repository';
|
||||
import { Model } from './model';
|
||||
@@ -11,17 +11,17 @@ import { debounce } from './decorators';
|
||||
import { filterEvent, dispose, anyEvent, fireEvent, PromiseSource } from './util';
|
||||
import { GitErrorCodes, Status } from './api/git';
|
||||
|
||||
class GitIgnoreDecorationProvider implements DecorationProvider {
|
||||
class GitIgnoreDecorationProvider implements FileDecorationProvider {
|
||||
|
||||
private static Decoration: Decoration = { priority: 3, color: new ThemeColor('gitDecoration.ignoredResourceForeground') };
|
||||
private static Decoration: FileDecoration = { color: new ThemeColor('gitDecoration.ignoredResourceForeground') };
|
||||
|
||||
readonly onDidChangeDecorations: Event<Uri[]>;
|
||||
private queue = new Map<string, { repository: Repository; queue: Map<string, PromiseSource<Decoration | undefined>>; }>();
|
||||
readonly onDidChange: Event<Uri[]>;
|
||||
private queue = new Map<string, { repository: Repository; queue: Map<string, PromiseSource<FileDecoration | undefined>>; }>();
|
||||
private disposables: Disposable[] = [];
|
||||
|
||||
constructor(private model: Model) {
|
||||
this.onDidChangeDecorations = fireEvent(anyEvent<any>(
|
||||
filterEvent(workspace.onDidSaveTextDocument, e => e.fileName.endsWith('.gitignore')),
|
||||
this.onDidChange = fireEvent(anyEvent<any>(
|
||||
filterEvent(workspace.onDidSaveTextDocument, e => /\.gitignore$|\.git\/info\/exclude$/.test(e.uri.path)),
|
||||
model.onDidOpenRepository,
|
||||
model.onDidCloseRepository
|
||||
));
|
||||
@@ -29,7 +29,7 @@ class GitIgnoreDecorationProvider implements DecorationProvider {
|
||||
this.disposables.push(window.registerDecorationProvider(this));
|
||||
}
|
||||
|
||||
async provideDecoration(uri: Uri): Promise<Decoration | undefined> {
|
||||
async provideFileDecoration(uri: Uri): Promise<FileDecoration | undefined> {
|
||||
const repository = this.model.getRepository(uri);
|
||||
|
||||
if (!repository) {
|
||||
@@ -39,7 +39,7 @@ class GitIgnoreDecorationProvider implements DecorationProvider {
|
||||
let queueItem = this.queue.get(repository.root);
|
||||
|
||||
if (!queueItem) {
|
||||
queueItem = { repository, queue: new Map<string, PromiseSource<Decoration | undefined>>() };
|
||||
queueItem = { repository, queue: new Map<string, PromiseSource<FileDecoration | undefined>>() };
|
||||
this.queue.set(repository.root, queueItem);
|
||||
}
|
||||
|
||||
@@ -84,19 +84,19 @@ class GitIgnoreDecorationProvider implements DecorationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
class GitDecorationProvider implements DecorationProvider {
|
||||
class GitDecorationProvider implements FileDecorationProvider {
|
||||
|
||||
private static SubmoduleDecorationData: Decoration = {
|
||||
title: 'Submodule',
|
||||
letter: 'S',
|
||||
private static SubmoduleDecorationData: FileDecoration = {
|
||||
tooltip: 'Submodule',
|
||||
badge: 'S',
|
||||
color: new ThemeColor('gitDecoration.submoduleResourceForeground')
|
||||
};
|
||||
|
||||
private readonly _onDidChangeDecorations = new EventEmitter<Uri[]>();
|
||||
readonly onDidChangeDecorations: Event<Uri[]> = this._onDidChangeDecorations.event;
|
||||
readonly onDidChange: Event<Uri[]> = this._onDidChangeDecorations.event;
|
||||
|
||||
private disposables: Disposable[] = [];
|
||||
private decorations = new Map<string, Decoration>();
|
||||
private decorations = new Map<string, FileDecoration>();
|
||||
|
||||
constructor(private repository: Repository) {
|
||||
this.disposables.push(
|
||||
@@ -106,7 +106,7 @@ class GitDecorationProvider implements DecorationProvider {
|
||||
}
|
||||
|
||||
private onDidRunGitStatus(): void {
|
||||
let newDecorations = new Map<string, Decoration>();
|
||||
let newDecorations = new Map<string, FileDecoration>();
|
||||
|
||||
this.collectSubmoduleDecorationData(newDecorations);
|
||||
this.collectDecorationData(this.repository.indexGroup, newDecorations);
|
||||
@@ -119,7 +119,7 @@ class GitDecorationProvider implements DecorationProvider {
|
||||
this._onDidChangeDecorations.fire([...uris.values()].map(value => Uri.parse(value, true)));
|
||||
}
|
||||
|
||||
private collectDecorationData(group: GitResourceGroup, bucket: Map<string, Decoration>): void {
|
||||
private collectDecorationData(group: GitResourceGroup, bucket: Map<string, FileDecoration>): void {
|
||||
for (const r of group.resourceStates) {
|
||||
const decoration = r.resourceDecoration;
|
||||
|
||||
@@ -134,13 +134,13 @@ class GitDecorationProvider implements DecorationProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private collectSubmoduleDecorationData(bucket: Map<string, Decoration>): void {
|
||||
private collectSubmoduleDecorationData(bucket: Map<string, FileDecoration>): void {
|
||||
for (const submodule of this.repository.submodules) {
|
||||
bucket.set(Uri.file(path.join(this.repository.root, submodule.path)).toString(), GitDecorationProvider.SubmoduleDecorationData);
|
||||
}
|
||||
}
|
||||
|
||||
provideDecoration(uri: Uri): Decoration | undefined {
|
||||
provideFileDecoration(uri: Uri): FileDecoration | undefined {
|
||||
return this.decorations.get(uri.toString());
|
||||
}
|
||||
|
||||
|
||||
39
extensions/git/src/emoji.ts
Normal file
39
extensions/git/src/emoji.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
import { workspace, Uri } from 'vscode';
|
||||
import { getExtensionContext } from './main';
|
||||
import { TextDecoder } from 'util';
|
||||
|
||||
const emojiRegex = /:([-+_a-z0-9]+):/g;
|
||||
|
||||
let emojiMap: Record<string, string> | undefined;
|
||||
let emojiMapPromise: Promise<void> | undefined;
|
||||
|
||||
export async function ensureEmojis() {
|
||||
if (emojiMap === undefined) {
|
||||
if (emojiMapPromise === undefined) {
|
||||
emojiMapPromise = loadEmojiMap();
|
||||
}
|
||||
await emojiMapPromise;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEmojiMap() {
|
||||
const context = getExtensionContext();
|
||||
const uri = (Uri as any).joinPath(context.extensionUri, 'resources', 'emojis.json');
|
||||
emojiMap = JSON.parse(new TextDecoder('utf8').decode(await workspace.fs.readFile(uri)));
|
||||
}
|
||||
|
||||
export function emojify(message: string) {
|
||||
if (emojiMap === undefined) {
|
||||
return message;
|
||||
}
|
||||
|
||||
return message.replace(emojiRegex, (s, code) => {
|
||||
return emojiMap?.[code] || s;
|
||||
});
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import * as iconv from 'iconv-lite-umd';
|
||||
import * as filetype from 'file-type';
|
||||
import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter } from './util';
|
||||
import { CancellationToken, Progress, Uri } from 'vscode';
|
||||
import { URI } from 'vscode-uri';
|
||||
import { detectEncoding } from './encoding';
|
||||
import { Ref, RefType, Branch, Remote, GitErrorCodes, LogOptions, Change, Status, CommitOptions, BranchQuery } from './api/git';
|
||||
import * as byline from 'byline';
|
||||
@@ -139,18 +138,28 @@ function findGitWin32(onLookup: (path: string) => void): Promise<IGit> {
|
||||
.then(undefined, () => findGitWin32InPath(onLookup));
|
||||
}
|
||||
|
||||
export function findGit(hint: string | undefined, onLookup: (path: string) => void): Promise<IGit> {
|
||||
const first = hint ? findSpecificGit(hint, onLookup) : Promise.reject<IGit>(null);
|
||||
export async function findGit(hint: string | string[] | undefined, onLookup: (path: string) => void): Promise<IGit> {
|
||||
const hints = Array.isArray(hint) ? hint : hint ? [hint] : [];
|
||||
|
||||
return first
|
||||
.then(undefined, () => {
|
||||
switch (process.platform) {
|
||||
case 'darwin': return findGitDarwin(onLookup);
|
||||
case 'win32': return findGitWin32(onLookup);
|
||||
default: return findSpecificGit('git', onLookup);
|
||||
}
|
||||
})
|
||||
.then(null, () => Promise.reject(new Error('Git installation not found.')));
|
||||
for (const hint of hints) {
|
||||
try {
|
||||
return await findSpecificGit(hint, onLookup);
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
switch (process.platform) {
|
||||
case 'darwin': return await findGitDarwin(onLookup);
|
||||
case 'win32': return await findGitWin32(onLookup);
|
||||
default: return await findSpecificGit('git', onLookup);
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
throw new Error('Git installation not found.');
|
||||
}
|
||||
|
||||
export interface IExecutionResult<T extends string | Buffer> {
|
||||
@@ -251,6 +260,7 @@ export interface IGitErrorData {
|
||||
exitCode?: number;
|
||||
gitErrorCode?: string;
|
||||
gitCommand?: string;
|
||||
gitArgs?: string[];
|
||||
}
|
||||
|
||||
export class GitError {
|
||||
@@ -262,6 +272,7 @@ export class GitError {
|
||||
exitCode?: number;
|
||||
gitErrorCode?: string;
|
||||
gitCommand?: string;
|
||||
gitArgs?: string[];
|
||||
|
||||
constructor(data: IGitErrorData) {
|
||||
if (data.error) {
|
||||
@@ -278,6 +289,7 @@ export class GitError {
|
||||
this.exitCode = data.exitCode;
|
||||
this.gitErrorCode = data.gitErrorCode;
|
||||
this.gitCommand = data.gitCommand;
|
||||
this.gitArgs = data.gitArgs;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
@@ -341,6 +353,12 @@ function sanitizePath(path: string): string {
|
||||
|
||||
const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%B';
|
||||
|
||||
export interface ICloneOptions {
|
||||
readonly parentPath: string;
|
||||
readonly progress: Progress<{ increment: number }>;
|
||||
readonly recursive?: boolean;
|
||||
}
|
||||
|
||||
export class Git {
|
||||
|
||||
readonly path: string;
|
||||
@@ -363,18 +381,18 @@ export class Git {
|
||||
return;
|
||||
}
|
||||
|
||||
async clone(url: string, parentPath: string, progress: Progress<{ increment: number }>, cancellationToken?: CancellationToken): Promise<string> {
|
||||
async clone(url: string, options: ICloneOptions, cancellationToken?: CancellationToken): Promise<string> {
|
||||
let baseFolderName = decodeURI(url).replace(/[\/]+$/, '').replace(/^.*[\/\\]/, '').replace(/\.git$/, '') || 'repository';
|
||||
let folderName = baseFolderName;
|
||||
let folderPath = path.join(parentPath, folderName);
|
||||
let folderPath = path.join(options.parentPath, folderName);
|
||||
let count = 1;
|
||||
|
||||
while (count < 20 && await new Promise(c => exists(folderPath, c))) {
|
||||
folderName = `${baseFolderName}-${count++}`;
|
||||
folderPath = path.join(parentPath, folderName);
|
||||
folderPath = path.join(options.parentPath, folderName);
|
||||
}
|
||||
|
||||
await mkdirp(parentPath);
|
||||
await mkdirp(options.parentPath);
|
||||
|
||||
const onSpawn = (child: cp.ChildProcess) => {
|
||||
const decoder = new StringDecoder('utf8');
|
||||
@@ -398,14 +416,18 @@ export class Git {
|
||||
}
|
||||
|
||||
if (totalProgress !== previousProgress) {
|
||||
progress.report({ increment: totalProgress - previousProgress });
|
||||
options.progress.report({ increment: totalProgress - previousProgress });
|
||||
previousProgress = totalProgress;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
try {
|
||||
await this.exec(parentPath, ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath, '--progress'], { cancellationToken, onSpawn });
|
||||
let command = ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath, '--progress'];
|
||||
if (options.recursive) {
|
||||
command.push('--recursive');
|
||||
}
|
||||
await this.exec(options.parentPath, command, { cancellationToken, onSpawn });
|
||||
} catch (err) {
|
||||
if (err.stderr) {
|
||||
err.stderr = err.stderr.replace(/^Cloning.+$/m, '').trim();
|
||||
@@ -435,7 +457,7 @@ export class Git {
|
||||
const [, letter] = match;
|
||||
|
||||
try {
|
||||
const networkPath = await new Promise<string>(resolve =>
|
||||
const networkPath = await new Promise<string | undefined>(resolve =>
|
||||
realpath.native(`${letter}:`, { encoding: 'utf8' }, (err, resolvedPath) =>
|
||||
resolve(err !== null ? undefined : resolvedPath),
|
||||
),
|
||||
@@ -516,7 +538,8 @@ export class Git {
|
||||
stderr: result.stderr,
|
||||
exitCode: result.exitCode,
|
||||
gitErrorCode: getGitErrorCode(result.stderr),
|
||||
gitCommand: args[0]
|
||||
gitCommand: args[0],
|
||||
gitArgs: args
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -669,7 +692,7 @@ export function parseGitmodules(raw: string): Submodule[] {
|
||||
return;
|
||||
}
|
||||
|
||||
const propertyMatch = /^\s*(\w+)\s+=\s+(.*)$/.exec(line);
|
||||
const propertyMatch = /^\s*(\w+)\s*=\s*(.*)$/.exec(line);
|
||||
|
||||
if (!propertyMatch) {
|
||||
return;
|
||||
@@ -1145,7 +1168,7 @@ export class Repository {
|
||||
break;
|
||||
}
|
||||
|
||||
const originalUri = URI.file(path.isAbsolute(resourcePath) ? resourcePath : path.join(this.repositoryRoot, resourcePath));
|
||||
const originalUri = Uri.file(path.isAbsolute(resourcePath) ? resourcePath : path.join(this.repositoryRoot, resourcePath));
|
||||
let status: Status = Status.UNTRACKED;
|
||||
|
||||
// Copy or Rename status comes with a number, e.g. 'R100'. We don't need the number, so we use only first character of the status.
|
||||
@@ -1173,7 +1196,7 @@ export class Repository {
|
||||
break;
|
||||
}
|
||||
|
||||
const uri = URI.file(path.isAbsolute(newPath) ? newPath : path.join(this.repositoryRoot, newPath));
|
||||
const uri = Uri.file(path.isAbsolute(newPath) ? newPath : path.join(this.repositoryRoot, newPath));
|
||||
result.push({
|
||||
uri,
|
||||
renameUri: uri,
|
||||
@@ -1223,7 +1246,7 @@ export class Repository {
|
||||
}
|
||||
|
||||
if (paths && paths.length) {
|
||||
for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
|
||||
for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) {
|
||||
await this.run([...args, '--', ...chunk]);
|
||||
}
|
||||
} else {
|
||||
@@ -1276,13 +1299,17 @@ export class Repository {
|
||||
await this.run(['update-index', add, '--cacheinfo', mode, hash, path]);
|
||||
}
|
||||
|
||||
async checkout(treeish: string, paths: string[], opts: { track?: boolean } = Object.create(null)): Promise<void> {
|
||||
async checkout(treeish: string, paths: string[], opts: { track?: boolean, detached?: boolean } = Object.create(null)): Promise<void> {
|
||||
const args = ['checkout', '-q'];
|
||||
|
||||
if (opts.track) {
|
||||
args.push('--track');
|
||||
}
|
||||
|
||||
if (opts.detached) {
|
||||
args.push('--detach');
|
||||
}
|
||||
|
||||
if (treeish) {
|
||||
args.push(treeish);
|
||||
}
|
||||
@@ -1298,6 +1325,7 @@ export class Repository {
|
||||
} catch (err) {
|
||||
if (/Please,? commit your changes or stash them/.test(err.stderr || '')) {
|
||||
err.gitErrorCode = GitErrorCodes.DirtyWorkTree;
|
||||
err.gitTreeish = treeish;
|
||||
}
|
||||
|
||||
throw err;
|
||||
@@ -1322,10 +1350,15 @@ export class Repository {
|
||||
if (opts.signCommit) {
|
||||
args.push('-S');
|
||||
}
|
||||
|
||||
if (opts.empty) {
|
||||
args.push('--allow-empty');
|
||||
}
|
||||
|
||||
if (opts.noVerify) {
|
||||
args.push('--no-verify');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.run(args, { input: message || '' });
|
||||
} catch (commitErr) {
|
||||
@@ -1390,6 +1423,11 @@ export class Repository {
|
||||
await this.run(args);
|
||||
}
|
||||
|
||||
async move(from: string, to: string): Promise<void> {
|
||||
const args = ['mv', from, to];
|
||||
await this.run(args);
|
||||
}
|
||||
|
||||
async setBranchUpstream(name: string, upstream: string): Promise<void> {
|
||||
const args = ['branch', '--set-upstream-to', upstream, name];
|
||||
await this.run(args);
|
||||
@@ -1440,7 +1478,7 @@ export class Repository {
|
||||
const args = ['clean', '-f', '-q'];
|
||||
|
||||
for (const paths of groups) {
|
||||
for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
|
||||
for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) {
|
||||
promises.push(limiter.queue(() => this.run([...args, '--', ...chunk])));
|
||||
}
|
||||
}
|
||||
@@ -1480,7 +1518,7 @@ export class Repository {
|
||||
|
||||
try {
|
||||
if (paths && paths.length > 0) {
|
||||
for (const chunk of splitInChunks(paths, MAX_CLI_LENGTH)) {
|
||||
for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) {
|
||||
await this.run([...args, '--', ...chunk]);
|
||||
}
|
||||
} else {
|
||||
@@ -1593,7 +1631,25 @@ export class Repository {
|
||||
}
|
||||
}
|
||||
|
||||
async push(remote?: string, name?: string, setUpstream: boolean = false, tags = false, forcePushMode?: ForcePushMode): Promise<void> {
|
||||
async rebase(branch: string, options: PullOptions = {}): Promise<void> {
|
||||
const args = ['rebase'];
|
||||
|
||||
args.push(branch);
|
||||
|
||||
try {
|
||||
await this.run(args, options);
|
||||
} catch (err) {
|
||||
if (/^CONFLICT \([^)]+\): \b/m.test(err.stdout || '')) {
|
||||
err.gitErrorCode = GitErrorCodes.Conflict;
|
||||
} else if (/cannot rebase onto multiple branches/i.test(err.stderr || '')) {
|
||||
err.gitErrorCode = GitErrorCodes.CantRebaseMultipleBranches;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async push(remote?: string, name?: string, setUpstream: boolean = false, followTags = false, forcePushMode?: ForcePushMode, tags = false): Promise<void> {
|
||||
const args = ['push'];
|
||||
|
||||
if (forcePushMode === ForcePushMode.ForceWithLease) {
|
||||
@@ -1606,10 +1662,14 @@ export class Repository {
|
||||
args.push('-u');
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
if (followTags) {
|
||||
args.push('--follow-tags');
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
args.push('--tags');
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
args.push(remote);
|
||||
}
|
||||
@@ -1719,11 +1779,17 @@ export class Repository {
|
||||
}
|
||||
}
|
||||
|
||||
getStatus(limit = 5000): Promise<{ status: IFileStatus[]; didHitLimit: boolean; }> {
|
||||
getStatus(opts?: { limit?: number, ignoreSubmodules?: boolean }): Promise<{ status: IFileStatus[]; didHitLimit: boolean; }> {
|
||||
return new Promise<{ status: IFileStatus[]; didHitLimit: boolean; }>((c, e) => {
|
||||
const parser = new GitStatusParser();
|
||||
const env = { GIT_OPTIONAL_LOCKS: '0' };
|
||||
const child = this.stream(['status', '-z', '-u'], { env });
|
||||
const args = ['status', '-z', '-u'];
|
||||
|
||||
if (opts?.ignoreSubmodules) {
|
||||
args.push('--ignore-submodules');
|
||||
}
|
||||
|
||||
const child = this.stream(args, { env });
|
||||
|
||||
const onExit = (exitCode: number) => {
|
||||
if (exitCode !== 0) {
|
||||
@@ -1733,13 +1799,15 @@ export class Repository {
|
||||
stderr,
|
||||
exitCode,
|
||||
gitErrorCode: getGitErrorCode(stderr),
|
||||
gitCommand: 'status'
|
||||
gitCommand: 'status',
|
||||
gitArgs: args
|
||||
}));
|
||||
}
|
||||
|
||||
c({ status: parser.status, didHitLimit: false });
|
||||
};
|
||||
|
||||
const limit = opts?.limit ?? 5000;
|
||||
const onStdoutData = (raw: string) => {
|
||||
parser.update(raw);
|
||||
|
||||
@@ -1803,7 +1871,7 @@ export class Repository {
|
||||
args.push('--sort', `-${opts.sort}`);
|
||||
}
|
||||
|
||||
args.push('--format', '%(refname) %(objectname)');
|
||||
args.push('--format', '%(refname) %(objectname) %(*objectname)');
|
||||
|
||||
if (opts?.pattern) {
|
||||
args.push(opts.pattern);
|
||||
@@ -1818,12 +1886,12 @@ export class Repository {
|
||||
const fn = (line: string): Ref | null => {
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
if (match = /^refs\/heads\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) {
|
||||
if (match = /^refs\/heads\/([^ ]+) ([0-9a-f]{40}) ([0-9a-f]{40})?$/.exec(line)) {
|
||||
return { name: match[1], commit: match[2], type: RefType.Head };
|
||||
} else if (match = /^refs\/remotes\/([^/]+)\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) {
|
||||
} else if (match = /^refs\/remotes\/([^/]+)\/([^ ]+) ([0-9a-f]{40}) ([0-9a-f]{40})?$/.exec(line)) {
|
||||
return { name: `${match[1]}/${match[2]}`, commit: match[3], type: RefType.RemoteHead, remote: match[1] };
|
||||
} else if (match = /^refs\/tags\/([^ ]+) ([0-9a-f]{40})$/.exec(line)) {
|
||||
return { name: match[1], commit: match[2], type: RefType.Tag };
|
||||
} else if (match = /^refs\/tags\/([^ ]+) ([0-9a-f]{40}) ([0-9a-f]{40})?$/.exec(line)) {
|
||||
return { name: match[1], commit: match[3] ?? match[2], type: RefType.Tag };
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -1872,7 +1940,7 @@ export class Repository {
|
||||
remote.pushUrl = url;
|
||||
}
|
||||
|
||||
// https://github.com/Microsoft/vscode/issues/45271
|
||||
// https://github.com/microsoft/vscode/issues/45271
|
||||
remote.isReadOnly = remote.pushUrl === undefined || remote.pushUrl === 'no_push';
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function deactivate(): Promise<any> {
|
||||
}
|
||||
|
||||
async function createModel(context: ExtensionContext, outputChannel: OutputChannel, telemetryReporter: TelemetryReporter, disposables: Disposable[]): Promise<Model> {
|
||||
const pathHint = workspace.getConfiguration('git').get<string>('path');
|
||||
const pathHint = workspace.getConfiguration('git').get<string | string[]>('path');
|
||||
const info = await findGit(pathHint, path => outputChannel.appendLine(localize('looking', "Looking for git in: {0}", path)));
|
||||
|
||||
const askpass = await Askpass.create(outputChannel, context.storagePath);
|
||||
@@ -169,7 +169,14 @@ export async function _activate(context: ExtensionContext): Promise<GitExtension
|
||||
}
|
||||
}
|
||||
|
||||
let _context: ExtensionContext;
|
||||
export function getExtensionContext(): ExtensionContext {
|
||||
return _context;
|
||||
}
|
||||
|
||||
export async function activate(context: ExtensionContext): Promise<GitExtension> {
|
||||
_context = context;
|
||||
|
||||
const result = await _activate(context);
|
||||
context.subscriptions.push(registerAPICommands(result));
|
||||
return result;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, OutputChannel, commands } from 'vscode';
|
||||
import { Repository, RepositoryState } from './repository';
|
||||
import { memoize, sequentialize, debounce } from './decorators';
|
||||
import { dispose, anyEvent, filterEvent, isDescendant, firstIndex, pathEquals, toDisposable, eventToPromise } from './util';
|
||||
import { dispose, anyEvent, filterEvent, isDescendant, pathEquals, toDisposable, eventToPromise } from './util';
|
||||
import { Git } from './git';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
@@ -260,7 +260,7 @@ export class Model implements IRemoteSourceProviderRegistry, IPushErrorHandlerRe
|
||||
|
||||
// This can happen whenever `path` has the wrong case sensitivity in
|
||||
// case insensitive file systems
|
||||
// https://github.com/Microsoft/vscode/issues/33498
|
||||
// https://github.com/microsoft/vscode/issues/33498
|
||||
const repositoryRoot = Uri.file(rawRoot).fsPath;
|
||||
|
||||
if (this.getRepository(repositoryRoot)) {
|
||||
@@ -372,7 +372,7 @@ export class Model implements IRemoteSourceProviderRegistry, IPushErrorHandlerRe
|
||||
const picks = this.openRepositories.map((e, index) => new RepositoryPick(e.repository, index));
|
||||
const active = window.activeTextEditor;
|
||||
const repository = active && this.getRepository(active.document.fileName);
|
||||
const index = firstIndex(picks, pick => pick.repository === repository);
|
||||
const index = picks.findIndex(pick => pick.repository === repository);
|
||||
|
||||
// Move repository pick containing the active text editor to appear first
|
||||
if (index > -1) {
|
||||
|
||||
@@ -34,4 +34,4 @@ export class GitProtocolHandler implements UriHandler {
|
||||
dispose(): void {
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, Decoration } from 'vscode';
|
||||
import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, OutputChannel, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands } from 'vscode';
|
||||
import * as nls from 'vscode-nls';
|
||||
import { Branch, Change, GitErrorCodes, LogOptions, Ref, RefType, Remote, Status, CommitOptions, BranchQuery } from './api/git';
|
||||
import { AutoFetcher } from './autofetch';
|
||||
@@ -205,9 +205,11 @@ export class Resource implements SourceControlResourceState {
|
||||
get color(): ThemeColor {
|
||||
switch (this.type) {
|
||||
case Status.INDEX_MODIFIED:
|
||||
return new ThemeColor('gitDecoration.stageModifiedResourceForeground');
|
||||
case Status.MODIFIED:
|
||||
return new ThemeColor('gitDecoration.modifiedResourceForeground');
|
||||
case Status.INDEX_DELETED:
|
||||
return new ThemeColor('gitDecoration.stageDeletedResourceForeground');
|
||||
case Status.DELETED:
|
||||
return new ThemeColor('gitDecoration.deletedResourceForeground');
|
||||
case Status.INDEX_ADDED:
|
||||
@@ -253,14 +255,10 @@ export class Resource implements SourceControlResourceState {
|
||||
}
|
||||
}
|
||||
|
||||
get resourceDecoration(): Decoration {
|
||||
return {
|
||||
bubble: this.type !== Status.DELETED && this.type !== Status.INDEX_DELETED,
|
||||
title: this.tooltip,
|
||||
letter: this.letter,
|
||||
color: this.color,
|
||||
priority: this.priority
|
||||
};
|
||||
get resourceDecoration(): FileDecoration {
|
||||
const res = new FileDecoration(this.letter, this.tooltip, this.color);
|
||||
res.propagate = this.type !== Status.DELETED && this.type !== Status.INDEX_DELETED;
|
||||
return res;
|
||||
}
|
||||
|
||||
constructor(
|
||||
@@ -302,6 +300,7 @@ export const enum Operation {
|
||||
RenameBranch = 'RenameBranch',
|
||||
DeleteRef = 'DeleteRef',
|
||||
Merge = 'Merge',
|
||||
Rebase = 'Rebase',
|
||||
Ignore = 'Ignore',
|
||||
Tag = 'Tag',
|
||||
DeleteTag = 'DeleteTag',
|
||||
@@ -316,6 +315,8 @@ export const enum Operation {
|
||||
Blame = 'Blame',
|
||||
Log = 'Log',
|
||||
LogFile = 'LogFile',
|
||||
|
||||
Move = 'Move'
|
||||
}
|
||||
|
||||
function isReadOnly(operation: Operation): boolean {
|
||||
@@ -644,6 +645,7 @@ export class Repository implements Disposable {
|
||||
}
|
||||
|
||||
this._rebaseCommit = rebaseCommit;
|
||||
commands.executeCommand('setContext', 'gitRebaseInProgress', !!this._rebaseCommit);
|
||||
}
|
||||
|
||||
get rebaseCommit(): Commit | undefined {
|
||||
@@ -746,11 +748,11 @@ export class Repository implements Disposable {
|
||||
onConfigListener(updateIndexGroupVisibility, this, this.disposables);
|
||||
updateIndexGroupVisibility();
|
||||
|
||||
const onConfigListenerForBranchSortOrder = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.branchSortOrder', root));
|
||||
onConfigListenerForBranchSortOrder(this.updateModelState, this, this.disposables);
|
||||
|
||||
const onConfigListenerForUntracked = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.untrackedChanges', root));
|
||||
onConfigListenerForUntracked(this.updateModelState, this, this.disposables);
|
||||
filterEvent(workspace.onDidChangeConfiguration, e =>
|
||||
e.affectsConfiguration('git.branchSortOrder', root)
|
||||
|| e.affectsConfiguration('git.untrackedChanges', root)
|
||||
|| e.affectsConfiguration('git.ignoreSubmodules', root)
|
||||
)(this.updateModelState, this, this.disposables);
|
||||
|
||||
const updateInputBoxVisibility = () => {
|
||||
const config = workspace.getConfiguration('git', root);
|
||||
@@ -771,7 +773,7 @@ export class Repository implements Disposable {
|
||||
|
||||
this.disposables.push(new AutoFetcher(this, globalState));
|
||||
|
||||
// https://github.com/Microsoft/vscode/issues/39039
|
||||
// https://github.com/microsoft/vscode/issues/39039
|
||||
const onSuccessfulPush = filterEvent(this.onDidRunOperation, e => e.operation === Operation.Push && !e.error);
|
||||
onSuccessfulPush(() => {
|
||||
const gitConfig = workspace.getConfiguration('git');
|
||||
@@ -864,6 +866,12 @@ export class Repository implements Disposable {
|
||||
return;
|
||||
}
|
||||
|
||||
const path = uri.path;
|
||||
|
||||
if (this.mergeGroup.resourceStates.some(r => r.resourceUri.path === path)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return toGitUri(uri, '', { replaceFileExtension: true });
|
||||
}
|
||||
|
||||
@@ -1053,6 +1061,10 @@ export class Repository implements Disposable {
|
||||
await this.run(Operation.RenameBranch, () => this.repository.renameBranch(name));
|
||||
}
|
||||
|
||||
async move(from: string, to: string): Promise<void> {
|
||||
await this.run(Operation.Move, () => this.repository.move(from, to));
|
||||
}
|
||||
|
||||
async getBranch(name: string): Promise<Branch> {
|
||||
return await this.run(Operation.GetBranch, () => this.repository.getBranch(name));
|
||||
}
|
||||
@@ -1069,6 +1081,10 @@ export class Repository implements Disposable {
|
||||
await this.run(Operation.Merge, () => this.repository.merge(ref));
|
||||
}
|
||||
|
||||
async rebase(branch: string): Promise<void> {
|
||||
await this.run(Operation.Rebase, () => this.repository.rebase(branch));
|
||||
}
|
||||
|
||||
async tag(name: string, message?: string): Promise<void> {
|
||||
await this.run(Operation.Tag, () => this.repository.tag(name, message));
|
||||
}
|
||||
@@ -1077,8 +1093,8 @@ export class Repository implements Disposable {
|
||||
await this.run(Operation.DeleteTag, () => this.repository.deleteTag(name));
|
||||
}
|
||||
|
||||
async checkout(treeish: string): Promise<void> {
|
||||
await this.run(Operation.Checkout, () => this.repository.checkout(treeish, []));
|
||||
async checkout(treeish: string, opts?: { detached?: boolean }): Promise<void> {
|
||||
await this.run(Operation.Checkout, () => this.repository.checkout(treeish, [], opts));
|
||||
}
|
||||
|
||||
async checkoutTracking(treeish: string): Promise<void> {
|
||||
@@ -1115,21 +1131,31 @@ export class Repository implements Disposable {
|
||||
|
||||
@throttle
|
||||
async fetchDefault(options: { silent?: boolean } = {}): Promise<void> {
|
||||
await this.run(Operation.Fetch, () => this.repository.fetch(options));
|
||||
await this._fetch({ silent: options.silent });
|
||||
}
|
||||
|
||||
@throttle
|
||||
async fetchPrune(): Promise<void> {
|
||||
await this.run(Operation.Fetch, () => this.repository.fetch({ prune: true }));
|
||||
await this._fetch({ prune: true });
|
||||
}
|
||||
|
||||
@throttle
|
||||
async fetchAll(): Promise<void> {
|
||||
await this.run(Operation.Fetch, () => this.repository.fetch({ all: true }));
|
||||
await this._fetch({ all: true });
|
||||
}
|
||||
|
||||
async fetch(remote?: string, ref?: string, depth?: number): Promise<void> {
|
||||
await this.run(Operation.Fetch, () => this.repository.fetch({ remote, ref, depth }));
|
||||
await this._fetch({ remote, ref, depth });
|
||||
}
|
||||
|
||||
private async _fetch(options: { remote?: string, ref?: string, all?: boolean, prune?: boolean, depth?: number, silent?: boolean } = {}): Promise<void> {
|
||||
if (!options.prune) {
|
||||
const config = workspace.getConfiguration('git', Uri.file(this.root));
|
||||
const prune = config.get<boolean>('pruneOnFetch');
|
||||
options.prune = prune;
|
||||
}
|
||||
|
||||
await this.run(Operation.Fetch, async () => this.repository.fetch(options));
|
||||
}
|
||||
|
||||
@throttle
|
||||
@@ -1195,6 +1221,10 @@ export class Repository implements Disposable {
|
||||
await this.run(Operation.Push, () => this._push(remote, undefined, false, true, forcePushMode));
|
||||
}
|
||||
|
||||
async pushTags(remote?: string, forcePushMode?: ForcePushMode): Promise<void> {
|
||||
await this.run(Operation.Push, () => this._push(remote, undefined, false, false, forcePushMode, true));
|
||||
}
|
||||
|
||||
async blame(path: string): Promise<string> {
|
||||
return await this.run(Operation.Blame, () => this.repository.blame(path));
|
||||
}
|
||||
@@ -1415,9 +1445,9 @@ export class Repository implements Disposable {
|
||||
return ignored;
|
||||
}
|
||||
|
||||
private async _push(remote?: string, refspec?: string, setUpstream: boolean = false, tags = false, forcePushMode?: ForcePushMode): Promise<void> {
|
||||
private async _push(remote?: string, refspec?: string, setUpstream: boolean = false, followTags = false, forcePushMode?: ForcePushMode, tags = false): Promise<void> {
|
||||
try {
|
||||
await this.repository.push(remote, refspec, setUpstream, tags, forcePushMode);
|
||||
await this.repository.push(remote, refspec, setUpstream, followTags, forcePushMode, tags);
|
||||
} catch (err) {
|
||||
if (!remote || !refspec) {
|
||||
throw err;
|
||||
@@ -1515,9 +1545,12 @@ export class Repository implements Disposable {
|
||||
|
||||
@throttle
|
||||
private async updateModelState(): Promise<void> {
|
||||
const { status, didHitLimit } = await this.repository.getStatus();
|
||||
const config = workspace.getConfiguration('git');
|
||||
const scopedConfig = workspace.getConfiguration('git', Uri.file(this.repository.root));
|
||||
const ignoreSubmodules = scopedConfig.get<boolean>('ignoreSubmodules');
|
||||
|
||||
const { status, didHitLimit } = await this.repository.getStatus({ ignoreSubmodules });
|
||||
|
||||
const config = workspace.getConfiguration('git');
|
||||
const shouldIgnore = config.get<boolean>('ignoreLimitWarning') === true;
|
||||
const useIcons = !config.get<boolean>('decorations.enabled', true);
|
||||
this.isRepositoryHuge = didHitLimit;
|
||||
@@ -1789,6 +1822,28 @@ export class Repository implements Disposable {
|
||||
return `${this.HEAD.behind}↓ ${this.HEAD.ahead}↑`;
|
||||
}
|
||||
|
||||
get syncTooltip(): string {
|
||||
if (!this.HEAD
|
||||
|| !this.HEAD.name
|
||||
|| !this.HEAD.commit
|
||||
|| !this.HEAD.upstream
|
||||
|| !(this.HEAD.ahead || this.HEAD.behind)
|
||||
) {
|
||||
return localize('sync changes', "Synchronize Changes");
|
||||
}
|
||||
|
||||
const remoteName = this.HEAD && this.HEAD.remote || this.HEAD.upstream.remote;
|
||||
const remote = this.remotes.find(r => r.name === remoteName);
|
||||
|
||||
if ((remote && remote.isReadOnly) || !this.HEAD.ahead) {
|
||||
return localize('pull n', "Pull {0} commits from {1}/{2}", this.HEAD.behind, this.HEAD.upstream.remote, this.HEAD.upstream.name);
|
||||
} else if (!this.HEAD.behind) {
|
||||
return localize('push n', "Push {0} commits to {1}/{2}", this.HEAD.ahead, this.HEAD.upstream.remote, this.HEAD.upstream.name);
|
||||
} else {
|
||||
return localize('pull push n', "Pull {0} and push {1} commits between {2}/{3}", this.HEAD.behind, this.HEAD.ahead, this.HEAD.upstream.remote, this.HEAD.upstream.name);
|
||||
}
|
||||
}
|
||||
|
||||
private updateInputBoxPlaceholder(): void {
|
||||
const branchName = this.headShortName;
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export function applyLineChanges(original: TextDocument, modified: TextDocument,
|
||||
|
||||
// if this is a deletion at the very end of the document,then we need to account
|
||||
// for a newline at the end of the last line which may have been deleted
|
||||
// https://github.com/Microsoft/vscode/issues/59670
|
||||
// https://github.com/microsoft/vscode/issues/59670
|
||||
if (isDeletion && diff.originalEndLineNumber === original.lineCount) {
|
||||
endLine -= 1;
|
||||
endCharacter = original.lineAt(endLine).range.end.character;
|
||||
|
||||
@@ -28,7 +28,7 @@ class CheckoutStatusBar {
|
||||
|
||||
return {
|
||||
command: 'git.checkout',
|
||||
tooltip: `${this.repository.headLabel}`,
|
||||
tooltip: localize('checkout', "Checkout branch/tag..."),
|
||||
title,
|
||||
arguments: [this.repository.sourceControl]
|
||||
};
|
||||
@@ -150,7 +150,7 @@ class SyncStatusBar {
|
||||
const rebaseWhenSync = config.get<string>('rebaseWhenSync');
|
||||
|
||||
command = rebaseWhenSync ? 'git.syncRebase' : 'git.sync';
|
||||
tooltip = localize('sync changes', "Synchronize Changes");
|
||||
tooltip = this.repository.syncTooltip;
|
||||
} else {
|
||||
icon = '$(cloud-upload)';
|
||||
command = 'git.publish';
|
||||
|
||||
@@ -184,6 +184,17 @@ suite('git', () => {
|
||||
{ name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' }
|
||||
]);
|
||||
});
|
||||
|
||||
test('whitespace again #108371', () => {
|
||||
const sample = `[submodule "deps/spdlog"]
|
||||
path= deps/spdlog
|
||||
url=https://github.com/gabime/spdlog.git
|
||||
`;
|
||||
|
||||
assert.deepEqual(parseGitmodules(sample), [
|
||||
{ name: 'deps/spdlog', path: 'deps/spdlog', url: 'https://github.com/gabime/spdlog.git' }
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('parseGitCommit', () => {
|
||||
|
||||
@@ -6,21 +6,31 @@
|
||||
const path = require('path');
|
||||
const testRunner = require('vscode/lib/testrunner');
|
||||
|
||||
const suite = 'Integration Git Tests';
|
||||
|
||||
const options: any = {
|
||||
ui: 'tdd',
|
||||
useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'),
|
||||
timeout: 60000
|
||||
};
|
||||
|
||||
// These integration tests is being run in multiple environments (electron, web, remote)
|
||||
// so we need to set the suite name based on the environment as the suite name is used
|
||||
// for the test results file name
|
||||
let suite = '';
|
||||
if (process.env.VSCODE_BROWSER) {
|
||||
suite = `${process.env.VSCODE_BROWSER} Browser Integration Git Tests`;
|
||||
} else if (process.env.REMOTE_VSCODE) {
|
||||
suite = 'Remote Integration Git Tests';
|
||||
} else {
|
||||
suite = 'Integration Git Tests';
|
||||
}
|
||||
|
||||
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
|
||||
options.reporter = 'mocha-multi-reporters';
|
||||
options.reporterOptions = {
|
||||
reporterEnabled: 'spec, mocha-junit-reporter',
|
||||
mochaJunitReporterReporterOptions: {
|
||||
testsuitesTitle: `${suite} ${process.platform}`,
|
||||
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
|
||||
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
import { CancellationToken, Disposable, env, Event, EventEmitter, ThemeIcon, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider, Uri, workspace } from 'vscode';
|
||||
import { CancellationToken, ConfigurationChangeEvent, Disposable, env, Event, EventEmitter, ThemeIcon, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider, Uri, workspace } from 'vscode';
|
||||
import { Model } from './model';
|
||||
import { Repository, Resource } from './repository';
|
||||
import { debounce } from './decorators';
|
||||
import { emojify, ensureEmojis } from './emoji';
|
||||
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
@@ -75,6 +76,7 @@ export class GitTimelineProvider implements TimelineProvider {
|
||||
constructor(private readonly model: Model) {
|
||||
this.disposable = Disposable.from(
|
||||
model.onDidOpenRepository(this.onRepositoriesChanged, this),
|
||||
workspace.onDidChangeConfiguration(this.onConfigurationChanged, this)
|
||||
);
|
||||
|
||||
if (model.repositories.length) {
|
||||
@@ -110,6 +112,8 @@ export class GitTimelineProvider implements TimelineProvider {
|
||||
);
|
||||
}
|
||||
|
||||
const config = workspace.getConfiguration('git.timeline');
|
||||
|
||||
// TODO@eamodio: Ensure that the uri is a file -- if not we could get the history of the repo?
|
||||
|
||||
let limit: number | undefined;
|
||||
@@ -129,6 +133,8 @@ export class GitTimelineProvider implements TimelineProvider {
|
||||
limit = options.limit === undefined ? undefined : options.limit + 1;
|
||||
}
|
||||
|
||||
await ensureEmojis();
|
||||
|
||||
const commits = await repo.logFile(uri, {
|
||||
maxEntries: limit,
|
||||
hash: options.cursor,
|
||||
@@ -146,13 +152,20 @@ export class GitTimelineProvider implements TimelineProvider {
|
||||
|
||||
const dateFormatter = new Intl.DateTimeFormat(env.language, { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
|
||||
|
||||
const items = commits.map<GitTimelineItem>((c, i) => {
|
||||
const date = c.commitDate; // c.authorDate
|
||||
const dateType = config.get<'committed' | 'authored'>('date');
|
||||
const showAuthor = config.get<boolean>('showAuthor');
|
||||
|
||||
const item = new GitTimelineItem(c.hash, commits[i + 1]?.hash ?? `${c.hash}^`, c.message, date?.getTime() ?? 0, c.hash, 'git:file:commit');
|
||||
const items = commits.map<GitTimelineItem>((c, i) => {
|
||||
const date = dateType === 'authored' ? c.authorDate : c.commitDate;
|
||||
|
||||
const message = emojify(c.message);
|
||||
|
||||
const item = new GitTimelineItem(c.hash, commits[i + 1]?.hash ?? `${c.hash}^`, message, date?.getTime() ?? 0, c.hash, 'git:file:commit');
|
||||
item.iconPath = new (ThemeIcon as any)('git-commit');
|
||||
item.description = c.authorName;
|
||||
item.detail = `${c.authorName} (${c.authorEmail}) \u2014 ${c.hash.substr(0, 8)}\n${dateFormatter.format(date)}\n\n${c.message}`;
|
||||
if (showAuthor) {
|
||||
item.description = c.authorName;
|
||||
}
|
||||
item.detail = `${c.authorName} (${c.authorEmail}) — ${c.hash.substr(0, 8)}\n${dateFormatter.format(date)}\n\n${message}`;
|
||||
item.command = {
|
||||
title: 'Open Comparison',
|
||||
command: 'git.timeline.openDiff',
|
||||
@@ -173,7 +186,7 @@ export class GitTimelineProvider implements TimelineProvider {
|
||||
// TODO@eamodio: Replace with a better icon -- reflecting its status maybe?
|
||||
item.iconPath = new (ThemeIcon as any)('git-commit');
|
||||
item.description = '';
|
||||
item.detail = localize('git.timeline.detail', '{0} \u2014 {1}\n{2}\n\n{3}', you, localize('git.index', 'Index'), dateFormatter.format(date), Resource.getStatusText(index.type));
|
||||
item.detail = localize('git.timeline.detail', '{0} — {1}\n{2}\n\n{3}', you, localize('git.index', 'Index'), dateFormatter.format(date), Resource.getStatusText(index.type));
|
||||
item.command = {
|
||||
title: 'Open Comparison',
|
||||
command: 'git.timeline.openDiff',
|
||||
@@ -191,7 +204,7 @@ export class GitTimelineProvider implements TimelineProvider {
|
||||
// TODO@eamodio: Replace with a better icon -- reflecting its status maybe?
|
||||
item.iconPath = new (ThemeIcon as any)('git-commit');
|
||||
item.description = '';
|
||||
item.detail = localize('git.timeline.detail', '{0} \u2014 {1}\n{2}\n\n{3}', you, localize('git.workingTree', 'Working Tree'), dateFormatter.format(date), Resource.getStatusText(working.type));
|
||||
item.detail = localize('git.timeline.detail', '{0} — {1}\n{2}\n\n{3}', you, localize('git.workingTree', 'Working Tree'), dateFormatter.format(date), Resource.getStatusText(working.type));
|
||||
item.command = {
|
||||
title: 'Open Comparison',
|
||||
command: 'git.timeline.openDiff',
|
||||
@@ -214,6 +227,12 @@ export class GitTimelineProvider implements TimelineProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private onConfigurationChanged(e: ConfigurationChangeEvent) {
|
||||
if (e.affectsConfiguration('git.timeline.date') || e.affectsConfiguration('git.timeline.showAuthor')) {
|
||||
this.fireChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private onRepositoriesChanged(_repo: Repository) {
|
||||
// console.log(`GitTimelineProvider.onRepositoriesChanged`);
|
||||
|
||||
@@ -232,7 +251,7 @@ export class GitTimelineProvider implements TimelineProvider {
|
||||
private onRepositoryStatusChanged(_repo: Repository) {
|
||||
// console.log(`GitTimelineProvider.onRepositoryStatusChanged`);
|
||||
|
||||
// This is crappy, but for now just save the last time a status was run and use that as the timestamp for staged items
|
||||
// This is less than ideal, but for now just save the last time a status was run and use that as the timestamp for staged items
|
||||
this.repoStatusDate = new Date();
|
||||
|
||||
this.fireChanged();
|
||||
|
||||
@@ -182,16 +182,6 @@ export function uniqueFilter<T>(keyFn: (t: T) => string): (t: T) => boolean {
|
||||
};
|
||||
}
|
||||
|
||||
export function firstIndex<T>(array: T[], fn: (t: T) => boolean): number {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (fn(array[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
export function find<T>(array: T[], fn: (t: T) => boolean): T | undefined {
|
||||
let result: T | undefined = undefined;
|
||||
|
||||
@@ -346,7 +336,7 @@ export function* splitInChunks(array: string[], maxChunkLength: number): Iterabl
|
||||
|
||||
interface ILimitedTaskFactory<T> {
|
||||
factory: () => Promise<T>;
|
||||
c: (value?: T | Promise<T>) => void;
|
||||
c: (value: T | Promise<T>) => void;
|
||||
e: (error?: any) => void;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user