Merge vscode source through release 1.79.2 (#23482)

* log when an editor action doesn't run because of enablement

* notebooks create/dispose editors. this means controllers must be created eagerly (😢) and that notebooks need a custom way of plugging comparision keys for session. works unless creating another session for the same cell of a duplicated editor

* Set offSide to sql lang configuration to true (#183461)

* Fixes #181764 (#183550)

* fix typo

* Always scroll down and focus the input (#183557)

* Fixes #180386 (#183561)

* cli: ensure ordering of rpc server messages (#183558)

* cli: ensure ordering of rpc server messages

Sending lots of messages to a stream would block them around the async
tokio mutex, which is "fair" so doesn't preserve ordering. Instead, use
the write_loop approach I introduced to the server_multiplexer for the
same reason some time ago.

* fix clippy

* update for May endgame

* testing: allow invalidateTestResults to take an array (#183569)

* Document `ShareProvider` API proposal (#183568)

* Document `ShareProvider` API proposal

* Remove mention of VS Code from JSDoc

* Add support for rendering svg and md in welcome message (#183580)

* Remove toggle setting more eagerly (#183584)

* rm message abt macOS

* Change text (#183589)

* Change text

* Accidentally changed the wrong file

* cli: improve output for code tunnel status (#183571)

* testing: allow invalidateTestResults to take an array

* cli: improve output for code tunnel status

Fixes #183570

* [json/css/html] update services (#183595)

* Add experimental setting to enable this dialog

* Fix exporting chat model to JSON before it is initialized (#183597)

* minimum scrolling to reveal the next cell on shift+enter (#183600)

do minimum scrolling to reveal the next cell on Execute cell and select next

* Fixing Jupyter notebook issue 13263 (#183527)

fix for the issue, still need to understand why there is strange focusing

* Tweak proposed API JSDoc (#183590)

* Tweak proposed API JSDoc

* workbench -> workspace

* fix ? operator

* Use active editor and show progress when sharing (#183603)

Use active editor and show progress

* use scroll setting variable correctly

* Schedule welcome widget to show once between typing. (#183606)

* Schedule dialog to show once between typing

* Don't re-render if already displayed once

* Add F10 keybinding for debugger step, even on Web. (#183510)

Fixes #181792.
Previously, for Web the keyboard shortcut was Alt-F10, because it was
believed that F10 could not be bound on browsers. This turned out to be
incorrect, so we make the shortcut consistent (F10) with desktop VSCode
which is also what many other debuggers use.
We keep Alt-F10 on web as a secondary keybinding to keep the experience
some web users may have gotten used to by now.

* Also pass process.env

* Restore missing chat clear commands (#183651)

* chore: update electron@22.5.4 (#183716)

* Show remote indicator in web when remoteAuthority is set (#183728)

* feat: .vuerc as json file (#153017)

Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>

* Delete --compatibility=1.63 code from the server (#183738)

* Copy vscode.dev link to tunnel generates an invalid link when an untitled workspace is open (#183739)

* Recent devcontainer display string corrupted on Get Started page (#183740)

* Improve "next codeblock" navigation (#183744)

* Improve "next codeblock" navigation
Operate on the current focused response, or the last one, and scroll to the selected item

* Normalize command title

* Git - run git status if similarityThreshold changes (#183762)

* fix aria-label issue in kb editor

fixes A11y_GradeB_VSCode_Keyboard shortcut reads words together - Blind: Arrow key navigation to row Find the binding keys and  "when" cell data are read together resulting in a word " CTRL + FeditorFocus  instead of CTRL + F editorFocus" #182490

* Status - fix compact padding (#183768)

* Remove angle brackets from VB brackets (#183782)

Fixes #183359

* Update language config schema with more details about brackets. (#183779)

* fix comment (#183812)

* Support for `Notebook` CodeAction Kind (#183457)

* nb kind support -- wip

* allow notebook codeactions around single cell edit check

* move notebook code action type out of editor

---------

Co-authored-by: rebornix <penn.lv@gmail.com>

* cli: fix connection default being applied (#183827)

* cli: bump to openssl 1.1.1u (#183828)

* Implement "delete" action for chat history (#183609)

* Use desired file name when generating new md pasted file paths (#183861)

Fixes #183851

* Default to filename for markdown new file if empty (#183864)

Fixes #183848

* Fix small typo (#183865)

Fixes #183819

* Noop when moving a symbol into the file it is already in (#183866)

Fixes #183793

* Adjust codeAction validation to account for notebook kind (#183859)

* Make JS/TS `go to configuration` commands work on non-`file:` file systems (#183688)

Make `go to project` commands work on non-`file:` file systems

Fixes #183685

* Can't do regex search after opening notebook (#183884)

Fixes #183858

* Default to current dir for `move to file` select (#183875)

Fixes #183870

`showOpenDialog` seems to ignore `defaultUri` if the file doesn't exist

* Use `<...>` style markdown links when needed (#183876)

Fixes #183849

* Remove check for context keys

* Update xterm package

* Enable updating a chat model without triggering incremental typing (#183894)

* Enable chat "move" commands on empty sessions (#183895)

* Enable chat "move" commands on empty sessions
and also imported sessions

* Fix command name

* Fix some chat keybindings on windows (#183896)

* "Revert File" on inactive editors are ignored (fix #177557) (#183903)

* Empty reason while switching profile (fix #183775) (#183904)

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4278 (#183910)

* fix https://github.com/microsoft/vscode/issues/183770 (#183914)

* code --status displays a lot of errors before actual status output (fix #183787) (#183915)

* joh/icy manatee (#183917)

* Use idle value for widget of interactive editor controller

https://github.com/microsoft/vscode/issues/183820

* also make preview editors idle values

https://github.com/microsoft/vscode/issues/183820

* Fix #183777 (#183929)

* Fix #182309 (#183925)

* Tree checkbox item -> items (#183931)

Fixes #183826

* Fixes #183909 (#183940)

* Fix #183837 (#183943)

fix #183837

* Git - fix #183941 (#183944)

* Update xterm.css

Fixes #181242

* chore: add @ulugbekna and @aiday-mar to my-endgame notebook (#183946)

* Revert "When snippet mode is active, make `Tab` not accept suggestion but advance placeholder"

This reverts commit 50a80cdb61511343996ff1d41d0b676c3d329f48.

* revert not focusing completion list when quick suggest happens during snippet

* change `snippetsPreventQuickSuggestions` default to false

* Fix #181446 (#183956)

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4298 (#183957)

* fix: remove extraneous incorrect context keys (#183959)

These were actually getting added in getTestItemContextOverlay, and the test ID was using the extended ID which extensions do not know about.

Fixes #183612

* Fixes https://github.com/microsoft/monaco-editor/issues/3920 (#183960)

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4324 (#183961)

* fix #183030

* fix #180826 (#183962)

* make message more generic for interactive editor help

* .

* fix #183968

* Keep codeblock toolbar visible when focused

* Fix when clause on "Run in terminal" command

* add important info to help menu

* fix #183970

* Set `isRefactoring` for all TS refactoring edits (#183982)

* consolidate

* Disable move to file in TS versions < 5.2 (#183992)

There are still a few key bugs with refactoring. We will  ship this as a preview for TS 5.2+ instead of for 5.1

* Polish query accepting (#183995)

We shouldn't send the same request to Copilot if the query hasn't changed. So if the query is the same, we short circut.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/4286

Also, when we open in chat, we should use the last accepted query, not what's in the input box.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/4280

* Allow widget to have focus (#184000)

So that selecting non-code text works.

Fixes https://github.com/microsoft/vscode-internalbacklog/issues/4294

* Fix microsoft/vscode-internalbacklog#4257. Mitigate zindex for zone widgets. (#184001)

* Change welcome dialog contribution to Eventually

* Misc fixes

* Workspace folder picker entry descriptions are suboptimal for some filesystems (fix #183418) (#184018)

* cli - ignore std error unless verbose (#183787) (#184031)

* joh/inquisitive meerkat (#184034)

* only stash sessions that are none empty

https://github.com/microsoft/vscode-internalbacklog/issues/4281

* only unstash a session once - unless new exchanges are made,

https://github.com/microsoft/vscode-internalbacklog/issues/4281

* account for all exchange types

* Improve declared components (#184039)

* make sure to read setting (#184040)

d'oh, related to https://github.com/microsoft/vscode/issues/173387#issuecomment-1571696644

* [html] update service (#184049)

[html] update service. FIxes #181176

* reset context keys on reset/hide (#184042)

fixes https://github.com/microsoft/vscode-internalbacklog/issues/4330

* use `Lazy`, not `IdleValue` for the IE widget held by the eager controller (#184048)

https://github.com/microsoft/vscode/issues/183820

* fix https://github.com/microsoft/vscode-internalbacklog/issues/4333 (#184067)

* use undo-loop instead of undo-edit when discarding chat session (#184063)

* use undo-loop instead of undo-edit when discarding chat session

fixes https://github.com/microsoft/vscode-internalbacklog/issues/4118

* fix tests, wait for correct state

* Add logging to node download (#184070)

Add logging to node download. For #182951

* re-enable default zone widget revealing when showing (#184072)

fixes https://github.com/microsoft/vscode-internalbacklog/issues/4332, also fixes https://github.com/microsoft/vscode-internalbacklog/issues/3784

* fix #178202

* Allow APIs in stable (#184062)

* Fix microsoft/vscode-internalbacklog#4206. Override List view whitespace css for monaco editor (#184087)

* Fix JSDoc grammatical error (#184090)

* Pick up TS 5.1.3 (#184091)

Fixes #182931

* Misc fixes

* update distro (#184097)

* chore: update electron@22.5.5 (#184116)

* Extension host veto is registered multiple times on restart (fix #183778) (#184127)

Extension host veto is registered multiple times on restart (#183778)

* Do not auto start the local web worker extension host (#184137)

* Allow embedders to intercept trustedTypes.createPolicy calls (#184136)

Allow embedders to intercept trustedTypes.createPolicy calls (#184100)

* fix: reading from console output for --status on windows and linux (#184138)

fix: reading from console output for --status on windows and linux (#184118)

* Misc fixes

* code --status displays a lot of errors before actual status output (fix #183787) (#184200)

fix 183787

* (cherry-pick to 1.79 from main) Handle galleryExtension failure in featuredExtensionService (#184205)

Handle galleryExtension failure in featuredExtensionService (#184198)

Handle galleryExtension failure

* Fix #184183. Multiple output height updates are skipped. (#184188)

* Post merge init fixes

* Misc build issues

* disable toggle inline diff of `alt` down

https://github.com/microsoft/vscode-internalbacklog/issues/4342

* Take into account already activated extensions when computing running locations (#184303)

Take into account already activated extensions when computing running locations (fixes #184180)

* Avoid `extensionService.getExtension` and use `ActivationKind.Immediate` to allow that URI handling works while resolving (#184310)

Avoid `extensionService.getExtension` and use `ActivationKind.Immediate` to allow that URI handling works while resolving (fixes #182217)

* WIP

* rm fish auto injection

* More breaks

* Fix Port Attributes constructor (#184412)

* WIP

* WIP

* Allow extensions to get at the exports of other extensions during resolving (#184487)

Allow extensions to get at the exports of other extensions during resolving (fixes #184472)

* do not auto finish session when inline chat widgets have focus

re https://github.com/microsoft/vscode-internalbacklog/issues/4354

* fix compile errors caused by new base method

* WIP

* WIP

* WIP

* WIP

* Build errors

* unc - fix path traversal bypass

* Bump version

* cherry-pick prod changes from main

* Disable sandbox

* Build break from merge

* bump version

* Merge pull request #184739 from max06/max06/issue184659

Restore ShellIntegration for fish (#184659)

* Git - only add --find-renames if the value is not the default one (#185053)

Git - only add --find-renames if the value is not the default one (#184992)

* Cherry-pick: Revert changes to render featured extensions when available (#184747)

Revert changes to render featured extensions when available.  (#184573)

* Lower timeouts for experimentation and gallery service

* Revert changes to render extensions when available

* Add audio cues

* fix: disable app sandbox when --no-sandbox is present (#184913)

* fix: disable app sandbox when --no-sandbox is present (#184897)

* fix: loading minimist in packaged builds

* Runtime errors

* UNC allow list checks cannot be disabled in extension host (fix #184989) (#185085)

* UNC allow list checks cannot be disabled in extension host (#184989)

* Update src/vs/base/node/unc.js

Co-authored-by: Robo <hop2deep@gmail.com>

---------

Co-authored-by: Robo <hop2deep@gmail.com>

* Add notebook extension

* Fix mangling issues

* Fix mangling issues

* npm install

* npm install

* Issues blocking bundle

* Fix build folder compile errors

* Fix windows bundle build

* Linting fixes

* Fix sqllint issues

* Update yarn.lock files

* Fix unit tests

* Fix a couple breaks from test fixes

* Bump distro

* redo the checkbox style

* Update linux build container dockerfile

* Bump build image tag

* Bump native watch dog package

* Bump node-pty

* Bump distro

* Fix documnetation error

* Update distro

* redo the button styles

* Update datasource TS

* Add missing yarn.lock files

* Windows setup fix

* Turn off extension unit tests while investigating

* color box style

* Remove appx

* Turn off test log upload

* update dropdownlist style

* fix universal app build error (#23488)

* Skip flaky bufferContext vscode test

---------

Co-authored-by: Johannes <johannes.rieken@gmail.com>
Co-authored-by: Henning Dieterichs <hdieterichs@microsoft.com>
Co-authored-by: Julien Richard <jairbubbles@hotmail.com>
Co-authored-by: Charles Gagnon <chgagnon@microsoft.com>
Co-authored-by: Megan Rogge <merogge@microsoft.com>
Co-authored-by: meganrogge <megan.rogge@microsoft.com>
Co-authored-by: Rob Lourens <roblourens@gmail.com>
Co-authored-by: Connor Peet <connor@peet.io>
Co-authored-by: Joyce Er <joyce.er@microsoft.com>
Co-authored-by: Bhavya U <bhavyau@microsoft.com>
Co-authored-by: Raymond Zhao <7199958+rzhao271@users.noreply.github.com>
Co-authored-by: Martin Aeschlimann <martinae@microsoft.com>
Co-authored-by: Aaron Munger <aamunger@microsoft.com>
Co-authored-by: Aiday Marlen Kyzy <amarlenkyzy@microsoft.com>
Co-authored-by: rebornix <penn.lv@gmail.com>
Co-authored-by: Ole <oler@google.com>
Co-authored-by: Jean Pierre <jeanp413@hotmail.com>
Co-authored-by: Robo <hop2deep@gmail.com>
Co-authored-by: Yash Singh <saiansh2525@gmail.com>
Co-authored-by: Ladislau Szomoru <3372902+lszomoru@users.noreply.github.com>
Co-authored-by: Ulugbek Abdullaev <ulugbekna@gmail.com>
Co-authored-by: Alex Ross <alros@microsoft.com>
Co-authored-by: Michael Lively <milively@microsoft.com>
Co-authored-by: Matt Bierner <matb@microsoft.com>
Co-authored-by: Andrea Mah <31675041+andreamah@users.noreply.github.com>
Co-authored-by: Benjamin Pasero <benjamin.pasero@microsoft.com>
Co-authored-by: Sandeep Somavarapu <sasomava@microsoft.com>
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
Co-authored-by: Tyler James Leonhardt <me@tylerleonhardt.com>
Co-authored-by: Alexandru Dima <alexdima@microsoft.com>
Co-authored-by: Joao Moreno <Joao.Moreno@microsoft.com>
Co-authored-by: Alan Ren <alanren@microsoft.com>
This commit is contained in:
Karl Burtram
2023-06-27 15:26:51 -07:00
committed by GitHub
parent 7975fda6dd
commit 01e66ab3e6
4335 changed files with 252586 additions and 164604 deletions

View File

@@ -3,18 +3,16 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { Command, Disposable, Event, EventEmitter, SourceControlActionButton, Uri, workspace } from 'vscode';
import { ApiRepository } from './api/api1';
import { Branch, Status } from './api/git';
import { IPostCommitCommandsProviderRegistry } from './postCommitCommands';
import { Repository, Operation } from './repository';
import { Command, Disposable, Event, EventEmitter, SourceControlActionButton, Uri, workspace, l10n } from 'vscode';
import { Branch, RefType, Status } from './api/git';
import { OperationKind } from './operation';
import { CommitCommandsCenter } from './postCommitCommands';
import { Repository } from './repository';
import { dispose } from './util';
const localize = nls.loadMessageBundle();
interface ActionButtonState {
readonly HEAD: Branch | undefined;
readonly isCheckoutInProgress: boolean;
readonly isCommitInProgress: boolean;
readonly isMergeInProgress: boolean;
readonly isRebaseInProgress: boolean;
@@ -39,9 +37,10 @@ export class ActionButtonCommand {
constructor(
readonly repository: Repository,
readonly postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry) {
readonly postCommitCommandCenter: CommitCommandsCenter) {
this._state = {
HEAD: undefined,
isCheckoutInProgress: false,
isCommitInProgress: false,
isMergeInProgress: false,
isRebaseInProgress: false,
@@ -52,7 +51,8 @@ export class ActionButtonCommand {
repository.onDidRunGitStatus(this.onDidRunGitStatus, this, this.disposables);
repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables);
this.disposables.push(postCommitCommandsProviderRegistry.onDidChangePostCommitCommandsProviders(() => this._onDidChange.fire()));
this.disposables.push(repository.onDidChangeBranchProtection(() => this._onDidChange.fire()));
this.disposables.push(postCommitCommandCenter.onDidChange(() => this._onDidChange.fire()));
const root = Uri.file(repository.root);
this.disposables.push(workspace.onDidChangeConfiguration(e => {
@@ -62,9 +62,9 @@ export class ActionButtonCommand {
this.onDidChangeSmartCommitSettings();
}
if (e.affectsConfiguration('git.branchProtection', root) ||
e.affectsConfiguration('git.branchProtectionPrompt', root) ||
if (e.affectsConfiguration('git.branchProtectionPrompt', root) ||
e.affectsConfiguration('git.postCommitCommand', root) ||
e.affectsConfiguration('git.rememberPostCommitCommand', root) ||
e.affectsConfiguration('git.showActionButton', root)) {
this._onDidChange.fire();
}
@@ -92,8 +92,10 @@ export class ActionButtonCommand {
// The button is disabled
if (!showActionButton.commit) { return undefined; }
const primaryCommand = this.getCommitActionButtonPrimaryCommand();
return {
command: this.getCommitActionButtonPrimaryCommand(),
command: primaryCommand,
secondaryCommands: this.getCommitActionButtonSecondaryCommands(),
enabled: (this.state.repositoryHasChangesToCommit || this.state.isRebaseInProgress) && !this.state.isCommitInProgress && !this.state.isMergeInProgress
};
@@ -104,90 +106,28 @@ export class ActionButtonCommand {
if (this.state.isRebaseInProgress) {
return {
command: 'git.commit',
title: localize('scm button continue title', "{0} Continue", '$(check)'),
tooltip: this.state.isCommitInProgress ? localize('scm button continuing tooltip', "Continuing Rebase...") : localize('scm button continue tooltip', "Continue Rebase"),
title: l10n.t('{0} Continue', '$(check)'),
tooltip: this.state.isCommitInProgress ? l10n.t('Continuing Rebase...') : l10n.t('Continue Rebase'),
arguments: [this.repository.sourceControl, '']
};
}
// Commit
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const postCommitCommand = config.get<string>('postCommitCommand');
// Branch protection
const isBranchProtected = this.repository.isBranchProtected();
const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!;
const alwaysPrompt = isBranchProtected && branchProtectionPrompt === 'alwaysPrompt';
const alwaysCommitToNewBranch = isBranchProtected && branchProtectionPrompt === 'alwaysCommitToNewBranch';
// Icon
const icon = alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined;
let commandArg = '';
let title = localize('scm button commit title', "{0} Commit", icon ?? '$(check)');
let tooltip = this.state.isCommitInProgress ? localize('scm button committing tooltip', "Committing Changes...") : localize('scm button commit tooltip', "Commit Changes");
// Title, tooltip
switch (postCommitCommand) {
case 'push': {
commandArg = 'git.push';
title = localize('scm button commit and push title', "{0} Commit & Push", icon ?? '$(arrow-up)');
if (alwaysCommitToNewBranch) {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing to new branch and pushing tooltip', "Committing to New Branch & Pushing Changes...") :
localize('scm button commit to new branch and push tooltip', "Commit to New Branch & Push Changes");
} else {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing and pushing tooltip', "Committing & Pushing Changes...") :
localize('scm button commit and push tooltip', "Commit & Push Changes");
}
break;
}
case 'sync': {
commandArg = 'git.sync';
title = localize('scm button commit and sync title', "{0} Commit & Sync", icon ?? '$(sync)');
if (alwaysCommitToNewBranch) {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing to new branch and synching tooltip', "Committing to New Branch & Synching Changes...") :
localize('scm button commit to new branch and sync tooltip', "Commit to New Branch & Sync Changes");
} else {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing and synching tooltip', "Committing & Synching Changes...") :
localize('scm button commit and sync tooltip', "Commit & Sync Changes");
}
break;
}
default: {
if (alwaysCommitToNewBranch) {
tooltip = this.state.isCommitInProgress ?
localize('scm button committing to new branch tooltip', "Committing Changes to New Branch...") :
localize('scm button commit to new branch tooltip', "Commit Changes to New Branch");
}
break;
}
}
return { command: 'git.commit', title, tooltip, arguments: [this.repository.sourceControl, commandArg] };
return this.postCommitCommandCenter.getPrimaryCommand();
}
private getCommitActionButtonSecondaryCommands(): Command[][] {
// Rebase Continue
if (this.state.isRebaseInProgress) {
return [];
}
// Commit
const commandGroups: Command[][] = [];
if (!this.state.isRebaseInProgress) {
for (const provider of this.postCommitCommandsProviderRegistry.getPostCommitCommandsProviders()) {
const commands = provider.getCommands(new ApiRepository(this.repository));
commandGroups.push((commands ?? []).map(c => {
return {
command: 'git.commit',
title: c.title,
arguments: [this.repository.sourceControl, c.command]
};
}));
}
if (commandGroups.length > 0) {
commandGroups[0].splice(0, 0, { command: 'git.commit', title: localize('scm secondary button commit', "Commit") });
}
for (const commands of this.postCommitCommandCenter.getSecondaryCommands()) {
commandGroups.push(commands.map(c => {
return { command: 'git.commit', title: c.title, tooltip: c.tooltip, arguments: c.arguments };
}));
}
return commandGroups;
@@ -197,19 +137,26 @@ export class ActionButtonCommand {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const showActionButton = config.get<{ publish: boolean }>('showActionButton', { publish: true });
// Branch does have an upstream, commit/merge/rebase is in progress, or the button is disabled
if (this.state.HEAD?.upstream || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.publish) { return undefined; }
// Not a branch (tag, detached), branch does have an upstream, commit/merge/rebase is in progress, or the button is disabled
if (this.state.HEAD?.type === RefType.Tag || !this.state.HEAD?.name || this.state.HEAD?.upstream || this.state.isCommitInProgress || this.state.isMergeInProgress || this.state.isRebaseInProgress || !showActionButton.publish) { return undefined; }
// Button icon
const icon = this.state.isSyncInProgress ? '$(sync~spin)' : '$(cloud-upload)';
return {
command: {
command: 'git.publish',
title: localize({ key: 'scm publish branch action button title', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }, "{0} Publish Branch", '$(cloud-upload)'),
title: l10n.t({ message: '{0} Publish Branch', args: [icon], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }),
tooltip: this.state.isSyncInProgress ?
localize({ key: 'scm button publish branch running', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }, "Publishing Branch...") :
localize({ key: 'scm button publish branch', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }, "Publish Branch"),
(this.state.HEAD?.name ?
l10n.t({ message: 'Publishing Branch "{0}"...', args: [this.state.HEAD.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
l10n.t({ message: 'Publishing Branch...', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })) :
(this.repository.HEAD?.name ?
l10n.t({ message: 'Publish Branch "{0}"', args: [this.state.HEAD?.name], comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] }) :
l10n.t({ message: 'Publish Branch', comment: ['{Locked="Branch"}', 'Do not translate "Branch" as it is a git term'] })),
arguments: [this.repository.sourceControl],
},
enabled: !this.state.isSyncInProgress
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress
};
}
@@ -228,28 +175,33 @@ export class ActionButtonCommand {
return {
command: {
command: 'git.sync',
title: `${icon}${behind}${ahead}`,
title: l10n.t('{0} Sync Changes{1}{2}', icon, behind, ahead),
tooltip: this.state.isSyncInProgress ?
localize('syncing changes', "Synchronizing Changes...")
l10n.t('Synchronizing Changes...')
: this.repository.syncTooltip,
arguments: [this.repository.sourceControl],
},
description: localize('scm button sync description', "{0} Sync Changes{1}{2}", icon, behind, ahead),
enabled: !this.state.isSyncInProgress
description: `${icon}${behind}${ahead}`,
enabled: !this.state.isCheckoutInProgress && !this.state.isSyncInProgress
};
}
private onDidChangeOperations(): void {
const isCheckoutInProgress
= this.repository.operations.isRunning(OperationKind.Checkout) ||
this.repository.operations.isRunning(OperationKind.CheckoutTracking);
const isCommitInProgress =
this.repository.operations.isRunning(Operation.Commit) ||
this.repository.operations.isRunning(Operation.RebaseContinue);
this.repository.operations.isRunning(OperationKind.Commit) ||
this.repository.operations.isRunning(OperationKind.PostCommitCommand) ||
this.repository.operations.isRunning(OperationKind.RebaseContinue);
const isSyncInProgress =
this.repository.operations.isRunning(Operation.Sync) ||
this.repository.operations.isRunning(Operation.Push) ||
this.repository.operations.isRunning(Operation.Pull);
this.repository.operations.isRunning(OperationKind.Sync) ||
this.repository.operations.isRunning(OperationKind.Push) ||
this.repository.operations.isRunning(OperationKind.Pull);
this.state = { ...this.state, isCommitInProgress, isSyncInProgress };
this.state = { ...this.state, isCheckoutInProgress, isCommitInProgress, isSyncInProgress };
}
private onDidChangeSmartCommitSettings(): void {

View File

@@ -5,8 +5,8 @@
import { Model } from '../model';
import { Repository as BaseRepository, Resource } from '../repository';
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, ICloneOptions, PostCommitCommandsProvider } from './git'; // {{SQL CARBON EDIT}} add ICloneOptions
import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode'; // {{SQL CARBON EDIT}} Add cancellationToken
import { InputBox, Git, API, Repository, Remote, RepositoryState, Branch, ForcePushMode, Ref, Submodule, Commit, Change, RepositoryUIState, Status, LogOptions, APIState, CommitOptions, RefType, CredentialsProvider, BranchQuery, PushErrorHandler, PublishEvent, FetchOptions, RemoteSourceProvider, RemoteSourcePublisher, PostCommitCommandsProvider, RefQuery, BranchProtectionProvider, InitOptions } from './git';
import { Event, SourceControlInputBox, Uri, SourceControl, Disposable, commands, CancellationToken } from 'vscode';
import { combinedDisposable, mapEvent } from '../util';
import { toGitUri } from '../uri';
import { GitExtensionImpl } from './extension';
@@ -32,7 +32,10 @@ export class ApiChange implements Change {
export class ApiRepositoryState implements RepositoryState {
get HEAD(): Branch | undefined { return this._repository.HEAD; }
get refs(): Ref[] { return [...this._repository.refs]; }
/**
* @deprecated Use ApiRepository.getRefs() instead.
*/
get refs(): Ref[] { console.warn('Deprecated. Use ApiRepository.getRefs() instead.'); return []; }
get remotes(): Remote[] { return [...this._repository.remotes]; }
get submodules(): Submodule[] { return [...this._repository.submodules]; }
get rebaseCommit(): Commit | undefined { return this._repository.rebaseCommit; }
@@ -170,14 +173,18 @@ export class ApiRepository implements Repository {
return this.repository.getBranch(name);
}
getBranches(query: BranchQuery): Promise<Ref[]> {
return this.repository.getBranches(query);
getBranches(query: BranchQuery, cancellationToken?: CancellationToken): Promise<Ref[]> {
return this.repository.getBranches(query, cancellationToken);
}
setBranchUpstream(name: string, upstream: string): Promise<void> {
return this.repository.setBranchUpstream(name, upstream);
}
getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise<Ref[]> {
return this.repository.getRefs(query, cancellationToken);
}
getMergeBase(ref1: string, ref2: string): Promise<string> {
return this.repository.getMergeBase(ref1, ref2);
}
@@ -279,7 +286,7 @@ export class ApiImpl implements API {
}
// {{SQL CARBON EDIT}}
async clone(url: string, options: ICloneOptions, cancellationToken?: CancellationToken): Promise<string> {
async clone(url: string, options: any, cancellationToken?: CancellationToken): Promise<string> {
return this._model.git.clone(url, options, cancellationToken);
}
@@ -292,9 +299,9 @@ export class ApiImpl implements API {
return result ? new ApiRepository(result) : null;
}
async init(root: Uri): Promise<Repository | null> {
async init(root: Uri, options?: InitOptions): Promise<Repository | null> {
const path = root.fsPath;
await this._model.git.init(path);
await this._model.git.init(path, options);
await this._model.openRepository(path);
return this.getRepository(root) || null;
}
@@ -331,6 +338,10 @@ export class ApiImpl implements API {
return this._model.registerPushErrorHandler(handler);
}
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable {
return this._model.registerBranchProtectionProvider(root, provider);
}
constructor(private _model: Model) { }
}
@@ -356,6 +367,7 @@ function getStatus(status: Status): string {
case Status.UNTRACKED: return 'UNTRACKED';
case Status.IGNORED: return 'IGNORED';
case Status.INTENT_TO_ADD: return 'INTENT_TO_ADD';
case Status.INTENT_TO_RENAME: return 'INTENT_TO_RENAME';
case Status.ADDED_BY_US: return 'ADDED_BY_US';
case Status.ADDED_BY_THEM: return 'ADDED_BY_THEM';
case Status.DELETED_BY_US: return 'DELETED_BY_US';

View File

@@ -8,6 +8,7 @@ export { ProviderResult } from 'vscode';
export interface API {
pickRemoteSource(options: PickRemoteSourceOptions): Promise<string | PickRemoteSourceResult | undefined>;
getRemoteSourceActions(url: string): Promise<RemoteSourceAction[]>;
registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable;
}
@@ -31,9 +32,12 @@ export interface GitBaseExtension {
export interface PickRemoteSourceOptions {
readonly providerLabel?: (provider: RemoteSourceProvider) => string;
readonly urlLabel?: string;
readonly urlLabel?: string | ((url: string) => string);
readonly providerName?: string;
readonly title?: string;
readonly placeholder?: string;
readonly branch?: boolean; // then result is PickRemoteSourceResult
readonly showRecentSources?: boolean;
}
export interface PickRemoteSourceResult {
@@ -41,20 +45,42 @@ export interface PickRemoteSourceResult {
readonly branch?: string;
}
export interface RemoteSourceAction {
readonly label: string;
/**
* Codicon name
*/
readonly icon: string;
run(branch: string): void;
}
export interface RemoteSource {
readonly name: string;
readonly description?: string;
readonly detail?: string;
/**
* Codicon name
*/
readonly icon?: string;
readonly url: string | string[];
}
export interface RecentRemoteSource extends RemoteSource {
readonly timestamp: number;
}
export interface RemoteSourceProvider {
readonly name: string;
/**
* Codicon name
*/
readonly icon?: string;
readonly label?: string;
readonly placeholder?: string;
readonly supportsQuery?: boolean;
getBranches?(url: string): ProviderResult<string[]>;
getRemoteSourceActions?(url: string): ProviderResult<RemoteSourceAction[]>;
getRecentRemoteSources?(query?: string): ProviderResult<RecentRemoteSource[]>;
getRemoteSources(query?: string): ProviderResult<RemoteSource[]>;
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Uri, Event, Disposable, ProviderResult, CancellationToken, Progress, Command } from 'vscode'; // {{SQL CARBON EDIT}} add CancellationToken
import { Uri, Event, Disposable, ProviderResult, Command, Progress, CancellationToken } from 'vscode'; // {{SQL CARBON EDIT}} - add Progress
export { ProviderResult } from 'vscode';
export interface Git {
@@ -78,6 +78,7 @@ export const enum Status {
UNTRACKED,
IGNORED,
INTENT_TO_ADD,
INTENT_TO_RENAME,
ADDED_BY_US,
ADDED_BY_THEM,
@@ -139,7 +140,13 @@ export interface CommitOptions {
requireUserConfig?: boolean;
useEditor?: boolean;
verbose?: boolean;
postCommitCommand?: string;
/**
* string - execute the specified command after the commit operation
* undefined - execute the command specified in git.postCommitCommand
* after the commit operation
* null - do not execute any command after the commit operation
*/
postCommitCommand?: string | null;
}
export interface FetchOptions {
@@ -150,11 +157,19 @@ export interface FetchOptions {
depth?: number;
}
export interface BranchQuery {
readonly remote?: boolean;
readonly pattern?: string;
readonly count?: number;
export interface InitOptions {
defaultBranch?: string;
}
export interface RefQuery {
readonly contains?: string;
readonly count?: number;
readonly pattern?: string;
readonly sort?: 'alphabetically' | 'committerdate';
}
export interface BranchQuery extends RefQuery {
readonly remote?: boolean;
}
export interface Repository {
@@ -198,9 +213,11 @@ export interface Repository {
createBranch(name: string, checkout: boolean, ref?: string): Promise<void>;
deleteBranch(name: string, force?: boolean): Promise<void>;
getBranch(name: string): Promise<Branch>;
getBranches(query: BranchQuery): Promise<Ref[]>;
getBranches(query: BranchQuery, cancellationToken?: CancellationToken): Promise<Ref[]>;
setBranchUpstream(name: string, upstream: string): Promise<void>;
getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise<Ref[]>;
getMergeBase(ref1: string, ref2: string): Promise<string>;
tag(name: string, upstream: string): Promise<void>;
@@ -262,6 +279,21 @@ export interface PushErrorHandler {
handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise<boolean>;
}
export interface BranchProtection {
readonly remote: string;
readonly rules: BranchProtectionRule[];
}
export interface BranchProtectionRule {
readonly include?: string[];
readonly exclude?: string[];
}
export interface BranchProtectionProvider {
onDidChangeBranchProtection: Event<Uri>;
provideBranchProtection(): BranchProtection[];
}
export type APIState = 'uninitialized' | 'initialized';
export interface PublishEvent {
@@ -285,10 +317,10 @@ export interface API {
* @param cancellationToken
* @returns a promise to the string location where the repository was cloned to
*/
clone(url: string, options: ICloneOptions, cancellationToken?: CancellationToken): Promise<string>; // {{SQL CARBON EDIT}}
clone(url: string, options: any, cancellationToken?: CancellationToken): Promise<string>; // {{SQL CARBON EDIT}}
toGitUri(uri: Uri, ref: string): Uri;
getRepository(uri: Uri): Repository | null;
init(root: Uri): Promise<Repository | null>;
init(root: Uri, options?: InitOptions): Promise<Repository | null>;
openRepository(root: Uri): Promise<Repository | null>
registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable;
@@ -296,6 +328,7 @@ export interface API {
registerCredentialsProvider(provider: CredentialsProvider): Disposable;
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable;
registerPushErrorHandler(handler: PushErrorHandler): Disposable;
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable;
}
export interface GitExtension {
@@ -352,7 +385,10 @@ export const enum GitErrorCodes {
PatchDoesNotApply = 'PatchDoesNotApply',
NoPathFound = 'NoPathFound',
UnknownPath = 'UnknownPath',
EmptyCommitMessage = 'EmptyCommitMessage'
EmptyCommitMessage = 'EmptyCommitMessage',
BranchFastForwardRejected = 'BranchFastForwardRejected',
BranchNotYetBorn = 'BranchNotYetBorn',
TagConflict = 'TagConflict'
}
// {{SQL CARBON EDIT}} move ICloneOptions from git.ts to here since it's used in clone()
@@ -360,4 +396,5 @@ export interface ICloneOptions {
readonly parentPath: string;
readonly progress: Progress<{ increment: number }>;
readonly recursive?: boolean;
readonly ref?: string;
}

View File

@@ -4,36 +4,58 @@
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as nls from 'vscode-nls';
import { IPCClient } from './ipc/ipcClient';
const localize = nls.loadMessageBundle();
function fatal(err: any): void {
console.error(localize('missOrInvalid', "Missing or invalid credentials."));
console.error('Missing or invalid credentials.');
console.error(err);
process.exit(1);
}
function main(argv: string[]): void {
if (argv.length !== 5) {
return fatal('Wrong number of arguments');
}
if (!process.env['VSCODE_GIT_ASKPASS_PIPE']) {
return fatal('Missing pipe');
}
if (!process.env['VSCODE_GIT_ASKPASS_TYPE']) {
return fatal('Missing type');
}
if (process.env['VSCODE_GIT_ASKPASS_TYPE'] !== 'https' && process.env['VSCODE_GIT_ASKPASS_TYPE'] !== 'ssh') {
return fatal(`Invalid type: ${process.env['VSCODE_GIT_ASKPASS_TYPE']}`);
}
if (process.env['VSCODE_GIT_COMMAND'] === 'fetch' && !!process.env['VSCODE_GIT_FETCH_SILENT']) {
return fatal('Skip silent fetch commands');
}
const output = process.env['VSCODE_GIT_ASKPASS_PIPE'] as string;
const request = argv[2];
const host = argv[4].replace(/^["']+|["':]+$/g, '');
const ipcClient = new IPCClient('askpass');
const askpassType = process.env['VSCODE_GIT_ASKPASS_TYPE'] as 'https' | 'ssh';
ipcClient.call({ request, host }).then(res => {
// HTTPS (username | password), SSH (passphrase | authenticity)
const request = askpassType === 'https' ? argv[2] : argv[3];
let host: string | undefined,
file: string | undefined,
fingerprint: string | undefined;
if (askpassType === 'https') {
host = argv[4].replace(/^["']+|["':]+$/g, '');
}
if (askpassType === 'ssh') {
if (/passphrase/i.test(request)) {
// passphrase
file = argv[6].replace(/^["']+|["':]+$/g, '');
} else {
// authenticity
host = argv[6].replace(/^["']+|["':]+$/g, '');
fingerprint = argv[15];
}
}
const ipcClient = new IPCClient('askpass');
ipcClient.call({ askpassType, request, host, file, fingerprint }).then(res => {
fs.writeFileSync(output, res + '\n');
setTimeout(() => process.exit(0), 0);
}).catch(err => fatal(err));

View File

@@ -1,5 +1,5 @@
#!/bin/sh
VSCODE_GIT_ASKPASS_PIPE=`mktemp`
ELECTRON_RUN_AS_NODE="1" VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $VSCODE_GIT_ASKPASS_EXTRA_ARGS $*
ELECTRON_RUN_AS_NODE="1" VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" VSCODE_GIT_ASKPASS_TYPE="https" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $VSCODE_GIT_ASKPASS_EXTRA_ARGS $*
cat $VSCODE_GIT_ASKPASS_PIPE
rm $VSCODE_GIT_ASKPASS_PIPE

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { window, InputBoxOptions, Uri, Disposable, workspace } from 'vscode';
import { window, InputBoxOptions, Uri, Disposable, workspace, QuickPickOptions, l10n } from 'vscode';
import { IDisposable, EmptyDisposable, toDisposable } from './util';
import * as path from 'path';
import { IIPCHandler, IIPCServer } from './ipc/ipcServer';
@@ -13,6 +13,7 @@ import { ITerminalEnvironmentProvider } from './terminal';
export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider {
private env: { [key: string]: string };
private sshEnv: { [key: string]: string };
private disposable: IDisposable = EmptyDisposable;
private cache = new Map<string, Credentials>();
private credentialsProviders = new Set<CredentialsProvider>();
@@ -23,14 +24,25 @@ export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider {
}
this.env = {
// GIT_ASKPASS
GIT_ASKPASS: path.join(__dirname, this.ipc ? 'askpass.sh' : 'askpass-empty.sh'),
// VSCODE_GIT_ASKPASS
VSCODE_GIT_ASKPASS_NODE: process.execPath,
VSCODE_GIT_ASKPASS_EXTRA_ARGS: (process.versions['electron'] && process.versions['microsoft-build']) ? '--ms-enable-electron-run-as-node' : '',
VSCODE_GIT_ASKPASS_MAIN: path.join(__dirname, 'askpass-main.js'),
};
this.sshEnv = {
// SSH_ASKPASS
SSH_ASKPASS: path.join(__dirname, this.ipc ? 'ssh-askpass.sh' : 'ssh-askpass-empty.sh'),
SSH_ASKPASS_REQUIRE: 'force',
};
}
async handle({ request, host }: { request: string; host: string }): Promise<string> {
async handle(payload:
{ askpassType: 'https'; request: string; host: string } |
{ askpassType: 'ssh'; request: string; host?: string; file?: string; fingerprint?: string }
): Promise<string> {
const config = workspace.getConfiguration('git', null);
const enabled = config.get<boolean>('enabled');
@@ -38,6 +50,16 @@ export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider {
return '';
}
// https
if (payload.askpassType === 'https') {
return await this.handleAskpass(payload.request, payload.host);
}
// ssh
return await this.handleSSHAskpass(payload.request, payload.host, payload.file, payload.fingerprint);
}
async handleAskpass(request: string, host: string): Promise<string> {
const uri = Uri.parse(host);
const authority = uri.authority.replace(/^.*@/, '');
const password = /password/i.test(request);
@@ -72,9 +94,33 @@ export class Askpass implements IIPCHandler, ITerminalEnvironmentProvider {
return await window.showInputBox(options) || '';
}
async handleSSHAskpass(request: string, host?: string, file?: string, fingerprint?: string): Promise<string> {
// passphrase
if (/passphrase/i.test(request)) {
const options: InputBoxOptions = {
password: true,
placeHolder: l10n.t('Passphrase'),
prompt: `SSH Key: ${file}`,
ignoreFocusOut: true
};
return await window.showInputBox(options) || '';
}
// authenticity
const options: QuickPickOptions = {
canPickMany: false,
ignoreFocusOut: true,
placeHolder: l10n.t('Are you sure you want to continue connecting?'),
title: l10n.t('"{0}" has fingerprint "{1}"', host ?? '', fingerprint ?? '')
};
const items = [l10n.t('yes'), l10n.t('no')];
return await window.showQuickPick(items, options) ?? '';
}
getEnv(): { [key: string]: string } {
const config = workspace.getConfiguration('git');
return config.get<boolean>('useIntegratedAskPass') ? this.env : {};
return config.get<boolean>('useIntegratedAskPass') ? { ...this.env, ...this.sshEnv } : {};
}
getTerminalEnv(): { [key: string]: string } {

View File

@@ -3,18 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workspace, Disposable, EventEmitter, Memento, window, MessageItem, ConfigurationTarget, Uri, ConfigurationChangeEvent } from 'vscode';
import { Repository, Operation } from './repository';
import { workspace, Disposable, EventEmitter, Memento, window, MessageItem, ConfigurationTarget, Uri, ConfigurationChangeEvent, l10n, env } from 'vscode';
import { Repository } from './repository';
import { eventToPromise, filterEvent, onceEvent } from './util';
import * as nls from 'vscode-nls';
import { GitErrorCodes } from './api/git';
const localize = nls.loadMessageBundle();
function isRemoteOperation(operation: Operation): boolean {
return operation === Operation.Pull || operation === Operation.Push || operation === Operation.Sync || operation === Operation.Fetch;
}
export class AutoFetcher {
private static DidInformUser = 'autofetch.didInformUser';
@@ -33,7 +26,7 @@ export class AutoFetcher {
workspace.onDidChangeConfiguration(this.onConfiguration, this, this.disposables);
this.onConfiguration();
const onGoodRemoteOperation = filterEvent(repository.onDidRunOperation, ({ operation, error }) => !error && isRemoteOperation(operation));
const onGoodRemoteOperation = filterEvent(repository.onDidRunOperation, ({ operation, error }) => !error && operation.remote);
const onFirstGoodRemoteOperation = onceEvent(onGoodRemoteOperation);
onFirstGoodRemoteOperation(this.onFirstGoodRemoteOperation, this, this.disposables);
}
@@ -51,11 +44,10 @@ export class AutoFetcher {
return;
}
const yes: MessageItem = { title: localize('yes', "Yes") };
const no: MessageItem = { isCloseAffordance: true, title: localize('no', "No") };
const askLater: MessageItem = { title: localize('not now', "Ask Me Later") };
// {{SQL CARBON EDIT}}
const result = await window.showInformationMessage(localize('suggest auto fetch', "Would you like Azure Data Studio to [periodically run 'git fetch']({0})?", 'https://go.microsoft.com/fwlink/?linkid=865294'), yes, no, askLater);
const yes: MessageItem = { title: l10n.t('Yes') };
const no: MessageItem = { isCloseAffordance: true, title: l10n.t('No') };
const askLater: MessageItem = { title: l10n.t('Ask Me Later') };
const result = await window.showInformationMessage(l10n.t('Would you like {0} to [periodically run "git fetch"]({1})?', env.appName, 'https://go.microsoft.com/fwlink/?linkid=865294'), yes, no, askLater);
if (result === askLater) {
return;
@@ -115,7 +107,7 @@ export class AutoFetcher {
try {
if (this._fetchAll) {
await this.repository.fetchAll();
await this.repository.fetchAll({ silent: true });
} else {
await this.repository.fetchDefault({ silent: true });
}

View File

@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Event, EventEmitter, Uri, workspace } from 'vscode';
import { BranchProtection, BranchProtectionProvider } from './api/git';
import { dispose, filterEvent } from './util';
export interface IBranchProtectionProviderRegistry {
readonly onDidChangeBranchProtectionProviders: Event<Uri>;
getBranchProtectionProviders(root: Uri): BranchProtectionProvider[];
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable;
}
export class GitBranchProtectionProvider implements BranchProtectionProvider {
private readonly _onDidChangeBranchProtection = new EventEmitter<Uri>();
onDidChangeBranchProtection = this._onDidChangeBranchProtection.event;
private branchProtection!: BranchProtection;
private disposables: Disposable[] = [];
constructor(private readonly repositoryRoot: Uri) {
const onDidChangeBranchProtectionEvent = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.branchProtection', repositoryRoot));
onDidChangeBranchProtectionEvent(this.updateBranchProtection, this, this.disposables);
this.updateBranchProtection();
}
provideBranchProtection(): BranchProtection[] {
return [this.branchProtection];
}
private updateBranchProtection(): void {
const scopedConfig = workspace.getConfiguration('git', this.repositoryRoot);
const branchProtectionConfig = scopedConfig.get<unknown>('branchProtection') ?? [];
const branchProtectionValues = Array.isArray(branchProtectionConfig) ? branchProtectionConfig : [branchProtectionConfig];
const branches = branchProtectionValues
.map(bp => typeof bp === 'string' ? bp.trim() : '')
.filter(bp => bp !== '');
this.branchProtection = { remote: '', rules: [{ include: branches }] };
this._onDidChangeBranchProtection.fire(this.repositoryRoot);
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -127,7 +127,11 @@ class GitDecorationProvider implements FileDecorationProvider {
// not deleted and has a decoration
bucket.set(r.original.toString(), decoration);
if (r.type === Status.INDEX_RENAMED) {
if (r.type === Status.DELETED && r.rightUri) {
bucket.set(r.rightUri.toString(), decoration);
}
if (r.type === Status.INDEX_RENAMED || r.type === Status.INTENT_TO_RENAME) {
bucket.set(r.resourceUri.toString(), decoration);
}
}

View File

@@ -0,0 +1,115 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as vscode from 'vscode';
import { RefType } from './api/git';
import { Model } from './model';
export class GitEditSessionIdentityProvider implements vscode.EditSessionIdentityProvider, vscode.Disposable {
private providerRegistration: vscode.Disposable;
constructor(private model: Model) {
this.providerRegistration = vscode.workspace.registerEditSessionIdentityProvider('file', this);
vscode.workspace.onWillCreateEditSessionIdentity((e) => {
e.waitUntil(this._onWillCreateEditSessionIdentity(e.workspaceFolder));
});
}
dispose() {
this.providerRegistration.dispose();
}
async provideEditSessionIdentity(workspaceFolder: vscode.WorkspaceFolder, token: vscode.CancellationToken): Promise<string | undefined> {
await this.model.openRepository(path.dirname(workspaceFolder.uri.fsPath));
const repository = this.model.getRepository(workspaceFolder.uri);
await repository?.status();
if (!repository || !repository?.HEAD?.upstream) {
return undefined;
}
const remoteUrl = repository.remotes.find((remote) => remote.name === repository.HEAD?.upstream?.remote)?.pushUrl?.replace(/^(git@[^\/:]+)(:)/i, 'ssh://$1/');
const remote = remoteUrl ? await vscode.workspace.getCanonicalUri(vscode.Uri.parse(remoteUrl), { targetScheme: 'https' }, token) : null;
return JSON.stringify({
remote: remote?.toString() ?? remoteUrl,
ref: repository.HEAD?.upstream?.name ?? null,
sha: repository.HEAD?.commit ?? null,
});
}
provideEditSessionIdentityMatch(identity1: string, identity2: string): vscode.EditSessionIdentityMatch {
try {
const normalizedIdentity1 = normalizeEditSessionIdentity(identity1);
const normalizedIdentity2 = normalizeEditSessionIdentity(identity2);
if (normalizedIdentity1.remote === normalizedIdentity2.remote &&
normalizedIdentity1.ref === normalizedIdentity2.ref &&
normalizedIdentity1.sha === normalizedIdentity2.sha) {
// This is a perfect match
return vscode.EditSessionIdentityMatch.Complete;
} else if (normalizedIdentity1.remote === normalizedIdentity2.remote &&
normalizedIdentity1.ref === normalizedIdentity2.ref &&
normalizedIdentity1.sha !== normalizedIdentity2.sha) {
// Same branch and remote but different SHA
return vscode.EditSessionIdentityMatch.Partial;
} else {
return vscode.EditSessionIdentityMatch.None;
}
} catch (ex) {
return vscode.EditSessionIdentityMatch.Partial;
}
}
private async _onWillCreateEditSessionIdentity(workspaceFolder: vscode.WorkspaceFolder): Promise<void> {
await this._doPublish(workspaceFolder);
}
private async _doPublish(workspaceFolder: vscode.WorkspaceFolder) {
await this.model.openRepository(path.dirname(workspaceFolder.uri.fsPath));
const repository = this.model.getRepository(workspaceFolder.uri);
if (!repository) {
return;
}
await repository.status();
// If this branch hasn't been published to the remote yet,
// ensure that it is published before Continue On is invoked
if (!repository.HEAD?.upstream && repository.HEAD?.type === RefType.Head) {
const publishBranch = vscode.l10n.t('Publish Branch');
const selection = await vscode.window.showInformationMessage(
vscode.l10n.t('The current branch is not published to the remote. Would you like to publish it to access your changes elsewhere?'),
{ modal: true },
publishBranch
);
if (selection !== publishBranch) {
throw new vscode.CancellationError();
}
await vscode.commands.executeCommand('git.publish');
}
}
}
function normalizeEditSessionIdentity(identity: string) {
let { remote, ref, sha } = JSON.parse(identity);
if (typeof remote === 'string' && remote.endsWith('.git')) {
remote = remote.slice(0, remote.length - 4);
}
return {
remote,
ref,
sha
};
}

View File

@@ -12,10 +12,10 @@ import * as which from 'which';
import { EventEmitter } from 'events';
import * as iconv from '@vscode/iconv-lite-umd';
import * as filetype from 'file-type';
import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows } from './util';
import { CancellationToken, ConfigurationChangeEvent, Uri, workspace } from 'vscode'; // {{SQL CARBON EDIT}} remove Progress
import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals } from './util';
import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, Uri, workspace } from 'vscode'; // {{SQL CARBON EDIT}} remove Progress
import { detectEncoding } from './encoding';
import { Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, BranchQuery, ICloneOptions } from './api/git'; // {{SQL CARBON EDIT}} add ICloneOptions
import { Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery, InitOptions, ICloneOptions } from './api/git'; // {{SQL CARBON EDIT}} add ICloneOptions
import * as byline from 'byline';
import { StringDecoder } from 'string_decoder';
@@ -198,7 +198,7 @@ async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToke
}
if (cancellationToken && cancellationToken.isCancellationRequested) {
throw new GitError({ message: 'Cancelled' });
throw new CancellationError();
}
const disposables: IDisposable[] = [];
@@ -239,7 +239,7 @@ async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToke
// noop
}
e(new GitError({ message: 'Cancelled' }));
e(new CancellationError());
});
});
@@ -360,6 +360,7 @@ const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B';
readonly parentPath: string;
readonly progress: Progress<{ increment: number }>;
readonly recursive?: boolean;
readonly ref?: string;
}*/
export class Git {
@@ -396,13 +397,18 @@ export class Git {
return Versions.compare(Versions.fromString(this.version), Versions.fromString(version));
}
open(repository: string, dotGit: { path: string; commonPath?: string }): Repository {
return new Repository(this, repository, dotGit);
open(repository: string, dotGit: { path: string; commonPath?: string }, logger: LogOutputChannel): Repository {
return new Repository(this, repository, dotGit, logger);
}
async init(repository: string): Promise<void> {
await this.exec(repository, ['init']);
return;
async init(repository: string, options: InitOptions = {}): Promise<void> {
const args = ['init'];
if (options.defaultBranch && options.defaultBranch !== '') {
args.push('-b', options.defaultBranch);
}
await this.exec(repository, args);
}
async clone(url: string, options: ICloneOptions, cancellationToken?: CancellationToken): Promise<string> {
@@ -427,7 +433,7 @@ export class Git {
let previousProgress = 0;
lineStream.on('data', (line: string) => {
let match: RegExpMatchArray | null = null;
let match: RegExpExecArray | null = null;
if (match = /Counting objects:\s*(\d+)%/i.exec(line)) {
totalProgress = Math.floor(parseInt(match[1]) * 0.1);
@@ -451,6 +457,9 @@ export class Git {
if (options.recursive) {
command.push('--recursive');
}
if (options.ref) {
command.push('--branch', options.ref);
}
await this.exec(options.parentPath, command, {
cancellationToken,
env: { 'GIT_HTTP_USER_AGENT': this.userAgent },
@@ -481,7 +490,7 @@ export class Git {
const repoUri = Uri.file(repoPath);
const pathUri = Uri.file(repositoryPath);
if (repoUri.authority.length !== 0 && pathUri.authority.length === 0) {
// eslint-disable-next-line code-no-look-behind-regex
// eslint-disable-next-line local/code-no-look-behind-regex
const match = /(?<=^\/?)([a-zA-Z])(?=:\/)/.exec(pathUri.path);
if (match !== null) {
const [, letter] = match;
@@ -493,10 +502,14 @@ export class Git {
),
);
if (networkPath !== undefined) {
// If the repository is at the root of the mapped drive then we
// have to append `\` (ex: D:\) otherwise the path is not valid.
const isDriveRoot = pathEquals(repoUri.fsPath, networkPath);
return path.normalize(
repoUri.fsPath.replace(
networkPath,
`${letter.toLowerCase()}:${networkPath.endsWith('\\') ? '\\' : ''}`
`${letter.toLowerCase()}:${isDriveRoot || networkPath.endsWith('\\') ? '\\' : ''}`
),
);
}
@@ -505,13 +518,6 @@ export class Git {
return path.normalize(pathUri.fsPath);
}
// On Windows, there are cases in which the normalized path for a mapped folder contains a trailing `\`
// character (ex: \\server\folder\) due to the implementation of `path.normalize()`. This behaviour is
// by design as documented in https://github.com/nodejs/node/issues/1765.
if (repoUri.authority.length !== 0) {
return repoPath.replace(/\\$/, '');
}
}
return repoPath;
@@ -554,7 +560,7 @@ export class Git {
if (options.log !== false) {
const startTime = Date.now();
child.on('exit', (_) => {
this.log(`> git ${args.join(' ')} [${Date.now() - startTime}ms]\n`);
this.log(`> git ${args.join(' ')} [${Date.now() - startTime}ms]${child.killed ? ' (cancelled)' : ''}\n`);
});
}
@@ -570,12 +576,22 @@ export class Git {
child.stdin!.end(options.input, 'utf8');
}
const startTime = Date.now();
const bufferResult = await exec(child, options.cancellationToken);
const startExec = Date.now();
let bufferResult: IExecutionResult<Buffer>;
try {
bufferResult = await exec(child, options.cancellationToken);
} catch (ex) {
if (ex instanceof CancellationError) {
this.log(`> git ${args.join(' ')} [${Date.now() - startExec}ms] (cancelled)\n`);
}
throw ex;
}
if (options.log !== false) {
// command
this.log(`> git ${args.join(' ')} [${Date.now() - startTime}ms]\n`);
this.log(`> git ${args.join(' ')} [${Date.now() - startExec}ms]\n`);
// stdout
if (bufferResult.stdout.length > 0 && args.find(a => this.commandsToLog.includes(a))) {
@@ -656,6 +672,32 @@ export class Git {
private log(output: string): void {
this._onOutput.emit('log', output);
}
async mergeFile(options: { input1Path: string; input2Path: string; basePath: string; diff3?: boolean }): Promise<string> {
const args = ['merge-file', '-p', options.input1Path, options.basePath, options.input2Path];
if (options.diff3) {
args.push('--diff3');
} else {
args.push('--no-diff3');
}
try {
const result = await this.exec(os.homedir(), args);
return result.stdout;
} catch (err) {
if (typeof err.stdout === 'string') {
// The merge had conflicts, stdout still contains the merged result (with conflict markers)
return err.stdout;
} else {
throw err;
}
}
}
async addSafeDirectory(repositoryPath: string): Promise<void> {
await this.exec(os.homedir(), ['config', '--global', '--add', 'safe.directory', repositoryPath]);
return;
}
}
export interface Commit {
@@ -669,6 +711,50 @@ export interface Commit {
refNames: string[];
}
interface GitConfigSection {
name: string;
subSectionName?: string;
properties: { [key: string]: string };
}
class GitConfigParser {
private static readonly _lineSeparator = /\r?\n/;
private static readonly _propertyRegex = /^\s*(\w+)\s*=\s*"?([^"]+)"?$/;
private static readonly _sectionRegex = /^\s*\[\s*([^\]]+?)\s*(\"[^"]+\")*\]\s*$/;
static parse(raw: string): GitConfigSection[] {
const config: { sections: GitConfigSection[] } = { sections: [] };
let section: GitConfigSection = { name: 'DEFAULT', properties: {} };
const addSection = (section?: GitConfigSection) => {
if (!section) { return; }
config.sections.push(section);
};
for (const line of raw.split(GitConfigParser._lineSeparator)) {
// Section
const sectionMatch = line.match(GitConfigParser._sectionRegex);
if (sectionMatch?.length === 3) {
addSection(section);
section = { name: sectionMatch[1], subSectionName: sectionMatch[2]?.replaceAll('"', ''), properties: {} };
continue;
}
// Property
const propertyMatch = line.match(GitConfigParser._propertyRegex);
if (propertyMatch?.length === 3 && !Object.keys(section.properties).includes(propertyMatch[1])) {
section.properties[propertyMatch[1]] = propertyMatch[2];
}
}
addSection(section);
return config.sections;
}
}
export class GitStatusParser {
private lastRaw = '';
@@ -707,7 +793,7 @@ export class GitStatusParser {
// space
i++;
if (entry.x === 'R' || entry.x === 'C') {
if (entry.x === 'R' || entry.y === 'R' || entry.x === 'C') {
lastIndex = raw.indexOf('\0', i);
if (lastIndex === -1) {
@@ -742,61 +828,38 @@ export interface Submodule {
}
export function parseGitmodules(raw: string): Submodule[] {
const regex = /\r?\n/g;
let position = 0;
let match: RegExpExecArray | null = null;
const result: Submodule[] = [];
let submodule: Partial<Submodule> = {};
function parseLine(line: string): void {
const sectionMatch = /^\s*\[submodule "([^"]+)"\]\s*$/.exec(line);
if (sectionMatch) {
if (submodule.name && submodule.path && submodule.url) {
result.push(submodule as Submodule);
}
const name = sectionMatch[1];
if (name) {
submodule = { name };
return;
}
for (const submoduleSection of GitConfigParser.parse(raw).filter(s => s.name === 'submodule')) {
if (submoduleSection.subSectionName && submoduleSection.properties['path'] && submoduleSection.properties['url']) {
result.push({
name: submoduleSection.subSectionName,
path: submoduleSection.properties['path'],
url: submoduleSection.properties['url']
});
}
if (!submodule) {
return;
}
const propertyMatch = /^\s*(\w+)\s*=\s*(.*)$/.exec(line);
if (!propertyMatch) {
return;
}
const [, key, value] = propertyMatch;
switch (key) {
case 'path': submodule.path = value; break;
case 'url': submodule.url = value; break;
}
}
while (match = regex.exec(raw)) {
parseLine(raw.substring(position, match.index));
position = match.index + match[0].length;
}
parseLine(raw.substring(position));
if (submodule.name && submodule.path && submodule.url) {
result.push(submodule as Submodule);
}
return result;
}
export function parseGitRemotes(raw: string): MutableRemote[] {
const remotes: MutableRemote[] = [];
for (const remoteSection of GitConfigParser.parse(raw).filter(s => s.name === 'remote')) {
if (remoteSection.subSectionName) {
remotes.push({
name: remoteSection.subSectionName,
fetchUrl: remoteSection.properties['url'],
pushUrl: remoteSection.properties['pushurl'] ?? remoteSection.properties['url'],
isReadOnly: false
});
}
}
return remotes;
}
const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm;
export function parseGitCommits(data: string): Commit[] {
@@ -882,7 +945,8 @@ export class Repository {
constructor(
private _git: Git,
private repositoryRoot: string,
readonly dotGit: { path: string; commonPath?: string }
readonly dotGit: { path: string; commonPath?: string },
private logger: LogOutputChannel
) { }
get git(): Git {
@@ -1399,6 +1463,8 @@ export class Repository {
if (/Please,? commit your changes or stash them/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.DirtyWorkTree;
err.gitTreeish = treeish;
} else if (/You are on a branch yet to be born/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.BranchNotYetBorn;
}
throw err;
@@ -1556,6 +1622,10 @@ export class Repository {
}
}
async mergeAbort(): Promise<void> {
await this.exec(['merge', '--abort']);
}
async tag(name: string, message?: string): Promise<void> {
let args = ['tag'];
@@ -1573,6 +1643,11 @@ export class Repository {
await this.exec(args);
}
async deleteRemoteTag(remoteName: string, tagName: string): Promise<void> {
const args = ['push', '--delete', remoteName, tagName];
await this.exec(args);
}
async clean(paths: string[]): Promise<void> {
const pathsByGroup = groupBy(paths.map(sanitizePath), p => path.dirname(p));
const groups = Object.keys(pathsByGroup).map(k => pathsByGroup[k]);
@@ -1690,12 +1765,34 @@ export class Repository {
err.gitErrorCode = GitErrorCodes.NoRemoteRepositorySpecified;
} else if (/Could not read from remote repository/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
} else if (/! \[rejected\].*\(non-fast-forward\)/m.test(err.stderr || '')) {
// The local branch has outgoing changes and it cannot be fast-forwarded.
err.gitErrorCode = GitErrorCodes.BranchFastForwardRejected;
}
throw err;
}
}
async fetchTags(options: { remote: string; tags: string[]; force?: boolean }): Promise<void> {
const args = ['fetch'];
const spawnOptions: SpawnOptions = {
env: { 'GIT_HTTP_USER_AGENT': this.git.userAgent }
};
args.push(options.remote);
for (const tag of options.tags) {
args.push(`refs/tags/${tag}:refs/tags/${tag}`);
}
if (options.force) {
args.push('--force');
}
await this.exec(args, spawnOptions);
}
async pull(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise<void> {
const args = ['pull'];
@@ -1735,6 +1832,8 @@ export class Repository {
err.gitErrorCode = GitErrorCodes.CantLockRef;
} else if (/cannot rebase onto multiple branches/i.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.CantRebaseMultipleBranches;
} else if (/! \[rejected\].*\(would clobber existing tag\)/m.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.TagConflict;
}
throw err;
@@ -1824,7 +1923,7 @@ export class Repository {
}
}
async createStash(message?: string, includeUntracked?: boolean): Promise<void> {
async createStash(message?: string, includeUntracked?: boolean, staged?: boolean): Promise<void> {
try {
const args = ['stash', 'push'];
@@ -1832,6 +1931,10 @@ export class Repository {
args.push('-u');
}
if (staged) {
args.push('-S');
}
if (message) {
args.push('-m', message);
}
@@ -1897,25 +2000,37 @@ export class Repository {
}
}
getStatus(opts?: { limit?: number; ignoreSubmodules?: boolean; untrackedChanges?: 'mixed' | 'separate' | 'hidden' }): Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean }> {
return new Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean }>((c, e) => {
async getStatus(opts?: { limit?: number; ignoreSubmodules?: boolean; similarityThreshold?: number; untrackedChanges?: 'mixed' | 'separate' | 'hidden'; cancellationToken?: CancellationToken }): Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean }> {
if (opts?.cancellationToken && opts?.cancellationToken.isCancellationRequested) {
throw new CancellationError();
}
const disposables: IDisposable[] = [];
const env = { GIT_OPTIONAL_LOCKS: '0' };
const args = ['status', '-z'];
if (opts?.untrackedChanges === 'hidden') {
args.push('-uno');
} else {
args.push('-uall');
}
if (opts?.ignoreSubmodules) {
args.push('--ignore-submodules');
}
// --find-renames option is only available starting with git 2.18.0
if (opts?.similarityThreshold && opts.similarityThreshold !== 50 && this._git.compareGitVersionTo('2.18.0') !== -1) {
args.push(`--find-renames=${opts.similarityThreshold}%`);
}
const child = this.stream(args, { env });
let result = new Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean }>((c, e) => {
const parser = new GitStatusParser();
const env = { GIT_OPTIONAL_LOCKS: '0' };
const args = ['status', '-z'];
if (opts?.untrackedChanges === 'hidden') {
args.push('-uno');
} else {
args.push('-uall');
}
if (opts?.ignoreSubmodules) {
args.push('--ignore-submodules');
}
const child = this.stream(args, { env });
const onExit = (exitCode: number) => {
const onClose = (exitCode: number) => {
if (exitCode !== 0) {
const stderr = stderrData.join('');
return e(new GitError({
@@ -1936,7 +2051,7 @@ export class Repository {
parser.update(raw);
if (limit !== 0 && parser.status.length > limit) {
child.removeListener('exit', onExit);
child.removeListener('close', onClose);
child.stdout!.removeListener('data', onStdoutData);
child.kill();
@@ -1952,12 +2067,71 @@ export class Repository {
child.stderr!.on('data', raw => stderrData.push(raw as string));
child.on('error', cpErrorHandler(e));
child.on('exit', onExit);
child.on('close', onClose);
});
if (opts?.cancellationToken) {
const cancellationPromise = new Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean }>((_, e) => {
disposables.push(onceEvent(opts.cancellationToken!.onCancellationRequested)(() => {
try {
child.kill();
} catch (err) {
// noop
}
e(new CancellationError());
}));
});
result = Promise.race([result, cancellationPromise]);
}
try {
const { status, statusLength, didHitLimit } = await result;
return { status, statusLength, didHitLimit };
}
finally {
dispose(disposables);
}
}
async getHEADRef(): Promise<Branch | undefined> {
let HEAD: Branch | undefined;
try {
HEAD = await this.getHEAD();
if (HEAD.name) {
// Branch
HEAD = await this.getBranch(HEAD.name);
} else if (HEAD.commit) {
// Tag || Commit
const tags = await this.getRefs({ pattern: 'refs/tags' });
const tag = tags.find(tag => tag.commit === HEAD!.commit);
if (tag) {
HEAD = { ...HEAD, name: tag.name, type: RefType.Tag };
}
}
} catch (err) {
// noop
}
return HEAD;
}
async getHEAD(): Promise<Ref> {
try {
// Attempt to parse the HEAD file
const result = await this.getHEADFS();
return result;
}
catch (err) {
this.logger.warn(err.message);
}
try {
// Fallback to using git to determine HEAD
const result = await this.exec(['symbolic-ref', '--short', 'HEAD']);
if (!result.stdout) {
@@ -1965,15 +2139,35 @@ export class Repository {
}
return { name: result.stdout.trim(), commit: undefined, type: RefType.Head };
} catch (err) {
const result = await this.exec(['rev-parse', 'HEAD']);
if (!result.stdout) {
throw new Error('Error parsing HEAD');
}
return { name: undefined, commit: result.stdout.trim(), type: RefType.Head };
}
catch (err) { }
// Detached HEAD
const result = await this.exec(['rev-parse', 'HEAD']);
if (!result.stdout) {
throw new Error('Error parsing HEAD');
}
return { name: undefined, commit: result.stdout.trim(), type: RefType.Head };
}
async getHEADFS(): Promise<Ref> {
const raw = await fs.readFile(path.join(this.dotGit.path, 'HEAD'), 'utf8');
// Branch
const branchMatch = raw.match(/^ref: refs\/heads\/(?<name>.*)$/m);
if (branchMatch?.groups?.name) {
return { name: branchMatch.groups.name, commit: undefined, type: RefType.Head };
}
// Detached
const commitMatch = raw.match(/^(?<commit>[0-9a-f]{40})$/m);
if (commitMatch?.groups?.commit) {
return { name: undefined, commit: commitMatch.groups.commit, type: RefType.Head };
}
throw new Error(`Unable to parse HEAD file. HEAD file contents: ${raw}.`);
}
async findTrackingBranches(upstreamBranch: string): Promise<Branch[]> {
@@ -1984,28 +2178,32 @@ export class Repository {
.map(([ref]) => ({ name: ref, type: RefType.Head } as Branch));
}
async getRefs(opts?: { sort?: 'alphabetically' | 'committerdate'; contains?: string; pattern?: string; count?: number }): Promise<Ref[]> {
const args = ['for-each-ref'];
if (opts?.count) {
args.push(`--count=${opts.count}`);
async getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise<Ref[]> {
if (cancellationToken && cancellationToken.isCancellationRequested) {
throw new CancellationError();
}
if (opts && opts.sort && opts.sort !== 'alphabetically') {
args.push('--sort', `-${opts.sort}`);
const args = ['for-each-ref'];
if (query.count) {
args.push(`--count=${query.count}`);
}
if (query.sort && query.sort !== 'alphabetically') {
args.push('--sort', `-${query.sort}`);
}
args.push('--format', '%(refname) %(objectname) %(*objectname)');
if (opts?.pattern) {
args.push(opts.pattern);
if (query.pattern) {
args.push(query.pattern.startsWith('refs/') ? query.pattern : `refs/${query.pattern}`);
}
if (opts?.contains) {
args.push('--contains', opts.contains);
if (query.contains) {
args.push('--contains', query.contains);
}
const result = await this.exec(args);
const result = await this.exec(args, { cancellationToken });
const fn = (line: string): Ref | null => {
let match: RegExpExecArray | null;
@@ -2027,6 +2225,43 @@ export class Repository {
.filter(ref => !!ref) as Ref[];
}
async getRemoteRefs(remote: string, opts?: { heads?: boolean; tags?: boolean; cancellationToken?: CancellationToken }): Promise<Ref[]> {
if (opts?.cancellationToken && opts?.cancellationToken.isCancellationRequested) {
throw new CancellationError();
}
const args = ['ls-remote'];
if (opts?.heads) {
args.push('--heads');
}
if (opts?.tags) {
args.push('--tags');
}
args.push(remote);
const result = await this.exec(args, { cancellationToken: opts?.cancellationToken });
const fn = (line: string): Ref | null => {
let match: RegExpExecArray | null;
if (match = /^([0-9a-f]{40})\trefs\/heads\/([^ ]+)$/.exec(line)) {
return { name: match[1], commit: match[2], type: RefType.Head };
} else if (match = /^([0-9a-f]{40})\trefs\/tags\/([^ ]+)$/.exec(line)) {
return { name: match[2], commit: match[1], type: RefType.Tag };
}
return null;
};
return result.stdout.split('\n')
.filter(line => !!line)
.map(fn)
.filter(ref => !!ref) as Ref[];
}
async getStashes(): Promise<Stash[]> {
const result = await this.exec(['stash', 'list']);
const regex = /^stash@{(\d+)}:(.+)$/;
@@ -2040,9 +2275,41 @@ export class Repository {
}
async getRemotes(): Promise<Remote[]> {
const remotes: MutableRemote[] = [];
try {
// Attempt to parse the config file
remotes.push(...await this.getRemotesFS());
if (remotes.length === 0) {
this.logger.info('No remotes found in the git config file.');
}
}
catch (err) {
this.logger.warn(`getRemotes() - ${err.message}`);
// Fallback to using git to get the remotes
remotes.push(...await this.getRemotesGit());
}
for (const remote of remotes) {
// https://github.com/microsoft/vscode/issues/45271
remote.isReadOnly = remote.pushUrl === undefined || remote.pushUrl === 'no_push';
}
return remotes;
}
private async getRemotesFS(): Promise<MutableRemote[]> {
const raw = await fs.readFile(path.join(this.dotGit.commonPath ?? this.dotGit.path, 'config'), 'utf8');
return parseGitRemotes(raw);
}
private async getRemotesGit(): Promise<MutableRemote[]> {
const remotes: MutableRemote[] = [];
const result = await this.exec(['remote', '--verbose']);
const lines = result.stdout.trim().split('\n').filter(l => !!l);
const remotes: MutableRemote[] = [];
for (const line of lines) {
const parts = line.split(/\s/);
@@ -2063,9 +2330,6 @@ export class Repository {
remote.fetchUrl = url;
remote.pushUrl = url;
}
// https://github.com/microsoft/vscode/issues/45271
remote.isReadOnly = remote.pushUrl === undefined || remote.pushUrl === 'no_push';
}
return remotes;
@@ -2154,11 +2418,6 @@ export class Repository {
return Promise.reject<Branch>(new Error('No such branch'));
}
async getBranches(query: BranchQuery): Promise<Ref[]> {
const refs = await this.getRefs({ contains: query.contains, pattern: query.pattern ? `refs/${query.pattern}` : undefined, count: query.count });
return refs.filter(value => (value.type !== RefType.Tag) && (query.remote || !value.remote));
}
// TODO: Support core.commentChar
stripCommitMessageComments(message: string): string {
return message.replace(/^\s*#.*$\n?/gm, '').trim();

View File

@@ -17,7 +17,7 @@ function getIPCHandlePath(id: string): string {
return `\\\\.\\pipe\\vscode-git-${id}-sock`;
}
if (process.env['XDG_RUNTIME_DIR']) {
if (process.platform !== 'darwin' && process.env['XDG_RUNTIME_DIR']) {
return path.join(process.env['XDG_RUNTIME_DIR'] as string, `vscode-git-${id}.sock`);
}

View File

@@ -1,115 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { commands, Disposable, Event, EventEmitter, OutputChannel, window, workspace } from 'vscode';
import { dispose } from './util';
/**
* The severity level of a log message
*/
export enum LogLevel {
Trace = 1,
Debug = 2,
Info = 3,
Warning = 4,
Error = 5,
Critical = 6,
Off = 7
}
/**
* Output channel logger
*/
export class OutputChannelLogger {
private _onDidChangeLogLevel = new EventEmitter<LogLevel>();
readonly onDidChangeLogLevel: Event<LogLevel> = this._onDidChangeLogLevel.event;
private _currentLogLevel!: LogLevel;
get currentLogLevel(): LogLevel {
return this._currentLogLevel;
}
set currentLogLevel(value: LogLevel) {
if (this._currentLogLevel === value) {
return;
}
this._currentLogLevel = value;
this._onDidChangeLogLevel.fire(value);
this.log(localize('gitLogLevel', "Log level: {0}", LogLevel[value]));
}
private _defaultLogLevel!: LogLevel;
get defaultLogLevel(): LogLevel {
return this._defaultLogLevel;
}
private _outputChannel: OutputChannel;
private _disposables: Disposable[] = [];
constructor() {
// Output channel
this._outputChannel = window.createOutputChannel('Git');
commands.registerCommand('git.showOutput', () => this.showOutputChannel());
this._disposables.push(this._outputChannel);
this._disposables.push(workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('git.logLevel')) {
this.onLogLevelChange();
}
}));
this.onLogLevelChange();
}
private onLogLevelChange(): void {
const config = workspace.getConfiguration('git');
const logLevel: keyof typeof LogLevel = config.get('logLevel', 'Info');
this.currentLogLevel = this._defaultLogLevel = LogLevel[logLevel] ?? LogLevel.Info;
}
log(message: string, logLevel?: LogLevel): void {
if (logLevel && logLevel < this._currentLogLevel) {
return;
}
this._outputChannel.appendLine(`[${new Date().toISOString()}]${logLevel ? ` [${LogLevel[logLevel].toLowerCase()}]` : ''} ${message}`);
}
logCritical(message: string): void {
this.log(message, LogLevel.Critical);
}
logDebug(message: string): void {
this.log(message, LogLevel.Debug);
}
logError(message: string): void {
this.log(message, LogLevel.Error);
}
logInfo(message: string): void {
this.log(message, LogLevel.Info);
}
logTrace(message: string): void {
this.log(message, LogLevel.Trace);
}
logWarning(message: string): void {
this.log(message, LogLevel.Warning);
}
showOutputChannel(): void {
this._outputChannel.show();
}
dispose(): void {
this._disposables = dispose(this._disposables);
}
}

View File

@@ -3,11 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import { ExtensionContext, workspace, Disposable, commands } from 'vscode';
import { findGit, Git } from './git';
import { ExtensionContext, workspace, window, Disposable, commands, LogOutputChannel, l10n, LogLevel } from 'vscode'; // {{SQL CARBON EDIT}} - remove unused
import { findGit, Git } from './git'; // {{SQL CARBON EDIT}} - remove unused
import { Model } from './model';
import { CommandCenter } from './commands';
import { GitFileSystemProvider } from './fileSystemProvider';
@@ -19,14 +16,15 @@ import { GitExtension } from './api/git';
import { GitProtocolHandler } from './protocolHandler';
import { GitExtensionImpl } from './api/extension';
import * as path from 'path';
// import * as fs from 'fs';
// import * as fs from 'fs'; // {{SQL CARBON EDIT}} - remove unused
import * as os from 'os';
import { GitTimelineProvider } from './timelineProvider';
import { registerAPICommands } from './api/api1';
import { TerminalEnvironmentManager } from './terminal';
import { OutputChannelLogger } from './log';
import { createIPCServer, IPCServer } from './ipc/ipcServer';
import { GitEditor } from './gitEditor';
// import { GitPostCommitCommandsProvider } from './postCommitCommands'; // {{SQL CARBON EDIT}} - remove unused
import { GitEditSessionIdentityProvider } from './editSessionIdentityProvider';
const deactivateTasks: { (): Promise<any> }[] = [];
@@ -36,7 +34,7 @@ export async function deactivate(): Promise<any> {
}
}
async function createModel(context: ExtensionContext, outputChannelLogger: OutputChannelLogger, telemetryReporter: TelemetryReporter, disposables: Disposable[]): Promise<Model> {
async function createModel(context: ExtensionContext, logger: LogOutputChannel, telemetryReporter: TelemetryReporter, disposables: Disposable[]): Promise<Model> {
const pathValue = workspace.getConfiguration('git').get<string | string[]>('path');
let pathHints = Array.isArray(pathValue) ? pathValue : pathValue ? [pathValue] : [];
@@ -49,7 +47,7 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu
}
const info = await findGit(pathHints, gitPath => {
outputChannelLogger.logInfo(localize('validating', "Validating found git in: {0}", gitPath));
logger.info(l10n.t('Validating found git in: "{0}"', gitPath));
if (excludes.length === 0) {
return true;
}
@@ -57,7 +55,7 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu
const normalized = path.normalize(gitPath).replace(/[\r\n]+$/, '');
const skip = excludes.some(e => normalized.startsWith(e));
if (skip) {
outputChannelLogger.logInfo(localize('skipped', "Skipped found git in: {0}", gitPath));
logger.info(l10n.t('Skipped found git in: "{0}"', gitPath));
}
return !skip;
});
@@ -67,7 +65,7 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu
try {
ipcServer = await createIPCServer(context.storagePath);
} catch (err) {
outputChannelLogger.logError(`Failed to create git IPC: ${err}`);
logger.error(`Failed to create git IPC: ${err}`);
}
const askpass = new Askpass(ipcServer);
@@ -80,15 +78,15 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu
const terminalEnvironmentManager = new TerminalEnvironmentManager(context, [askpass, gitEditor, ipcServer]);
disposables.push(terminalEnvironmentManager);
outputChannelLogger.logInfo(localize('using git', "Using git {0} from {1}", info.version, info.path));
logger.info(l10n.t('Using git "{0}" from "{1}"', info.version, info.path));
const git = new Git({
gitPath: info.path,
userAgent: `git/${info.version} (${(os as any).version?.() ?? os.type()} ${os.release()}; ${os.platform()} ${os.arch()}) azuredatudio`,
userAgent: `git/${info.version} (${(os as any).version?.() ?? os.type()} ${os.release()}; ${os.platform()} ${os.arch()}) azuredatudio`, // {{SQL CARBON EDIT}} - update product name
version: info.version,
env: environment,
});
const model = new Model(git, askpass, context.globalState, outputChannelLogger, telemetryReporter);
const model = new Model(git, askpass, context.globalState, logger, telemetryReporter);
disposables.push(model);
const onRepository = () => commands.executeCommand('setContext', 'gitOpenRepositoryCount', `${model.repositories.length}`);
@@ -103,24 +101,25 @@ async function createModel(context: ExtensionContext, outputChannelLogger: Outpu
lines.pop();
}
outputChannelLogger.log(lines.join('\n'));
logger.appendLine(lines.join('\n'));
};
git.onOutput.addListener('log', onOutput);
disposables.push(toDisposable(() => git.onOutput.removeListener('log', onOutput)));
const cc = new CommandCenter(git, model, outputChannelLogger, telemetryReporter);
const cc = new CommandCenter(git, model, context.globalState, logger, telemetryReporter);
disposables.push(
cc,
new GitFileSystemProvider(model),
new GitDecorations(model),
new GitProtocolHandler(),
new GitTimelineProvider(model, cc)
new GitTimelineProvider(model, cc),
new GitEditSessionIdentityProvider(model)
);
// const postCommitCommandsProvider = new GitPostCommitCommandsProvider(); {{SQL CARBON TODO}} lewissanchez - Do we need this?
// model.registerPostCommitCommandsProvider(postCommitCommandsProvider); {{SQL CARBON TODO}} lewissanchez - Do we need this?
// checkGitVersion(info); {{SQL CARBON EDIT}} Don't check git version
// commands.executeCommand('setContext', 'gitVersion2.35', git.compareGitVersionTo('2.35') >= 0);
return model;
}
@@ -159,10 +158,10 @@ async function warnAboutMissingGit(): Promise<void> {
return;
}
const download = localize('downloadgit', "Download Git");
const neverShowAgain = localize('neverShowAgain', "Don't Show Again");
const download = l10n.t('Download Git');
const neverShowAgain = l10n.t('Don\'t Show Again');
const choice = await window.showWarningMessage(
localize('notfound', "Git not found. Install it or configure it using the 'git.path' setting."),
l10n.t('Git not found. Install it or configure it using the "git.path" setting.'),
download,
neverShowAgain
);
@@ -172,17 +171,23 @@ async function warnAboutMissingGit(): Promise<void> {
} else if (choice === neverShowAgain) {
await config.update('ignoreMissingGitWarning', true, true);
}
}*/
}*/ // {{SQl CARBON EDIT}} - end comment block
export async function _activate(context: ExtensionContext): Promise<GitExtensionImpl> {
const disposables: Disposable[] = [];
context.subscriptions.push(new Disposable(() => Disposable.from(...disposables).dispose()));
const outputChannelLogger = new OutputChannelLogger();
disposables.push(outputChannelLogger);
const logger = window.createOutputChannel('Git', { log: true });
disposables.push(logger);
const { name, version, aiKey } = require('../package.json') as { name: string; version: string; aiKey: string };
const telemetryReporter = new TelemetryReporter(name, version, aiKey);
const onDidChangeLogLevel = (logLevel: LogLevel) => {
logger.appendLine(l10n.t('Log level: {0}', LogLevel[logLevel]));
};
disposables.push(logger.onDidChangeLogLevel(onDidChangeLogLevel));
onDidChangeLogLevel(logger.logLevel);
const { aiKey } = require('../package.json') as { aiKey: string };
const telemetryReporter = new TelemetryReporter(aiKey);
deactivateTasks.push(() => telemetryReporter.dispose());
const config = workspace.getConfiguration('git', null);
@@ -193,12 +198,12 @@ export async function _activate(context: ExtensionContext): Promise<GitExtension
const onEnabled = filterEvent(onConfigChange, () => workspace.getConfiguration('git', null).get<boolean>('enabled') === true);
const result = new GitExtensionImpl();
eventToPromise(onEnabled).then(async () => result.model = await createModel(context, outputChannelLogger, telemetryReporter, disposables));
eventToPromise(onEnabled).then(async () => result.model = await createModel(context, logger, telemetryReporter, disposables));
return result;
}
try {
const model = await createModel(context, outputChannelLogger, telemetryReporter, disposables);
const model = await createModel(context, logger, telemetryReporter, disposables);
return new GitExtensionImpl(model);
} catch (err) {
if (!/Git installation not found/.test(err.message || '')) {
@@ -206,7 +211,7 @@ export async function _activate(context: ExtensionContext): Promise<GitExtension
}
// console.warn(err.message); {{SQL CARBON EDIT}} turn-off Git missing prompt
// outputChannel.appendLine(err.message); {{SQL CARBON EDIT}} turn-off Git missing prompt
// logger.warn(err.message);
/* __GDPR__
"git.missing" : {
@@ -219,6 +224,8 @@ export async function _activate(context: ExtensionContext): Promise<GitExtension
// warnAboutMissingGit(); {{SQL CARBON EDIT}} turn-off Git missing prompt
return new GitExtensionImpl();
} finally {
disposables.push(new GitProtocolHandler(logger));
}
}

View File

@@ -3,7 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, commands } from 'vscode';
import { workspace, WorkspaceFoldersChangeEvent, Uri, window, Event, EventEmitter, QuickPickItem, Disposable, SourceControl, SourceControlResourceGroup, TextEditor, Memento, commands, LogOutputChannel, l10n, ProgressLocation, WorkspaceFolder } from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import { Repository, RepositoryState } from './repository';
import { memoize, sequentialize, debounce } from './decorators';
@@ -11,17 +11,14 @@ import { dispose, anyEvent, filterEvent, isDescendant, pathEquals, toDisposable,
import { Git } from './git';
import * as path from 'path';
import * as fs from 'fs';
import * as nls from 'vscode-nls';
import { fromGitUri } from './uri';
import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider } from './api/git';
import { APIState as State, CredentialsProvider, PushErrorHandler, PublishEvent, RemoteSourcePublisher, PostCommitCommandsProvider, BranchProtectionProvider } from './api/git';
import { Askpass } from './askpass';
import { IPushErrorHandlerRegistry } from './pushError';
import { ApiRepository } from './api/api1';
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
import { OutputChannelLogger } from './log';
import { IPostCommitCommandsProviderRegistry } from './postCommitCommands';
const localize = nls.loadMessageBundle();
import { IBranchProtectionProviderRegistry } from './branchProtection';
class RepositoryPick implements QuickPickItem {
@memoize get label(): string {
@@ -37,6 +34,50 @@ class RepositoryPick implements QuickPickItem {
constructor(public readonly repository: Repository, public readonly index: number) { }
}
abstract class RepositoryMap<T = void> extends Map<string, T> {
constructor() {
super();
this.updateContextKey();
}
override set(key: string, value: T): this {
const result = super.set(key, value);
this.updateContextKey();
return result;
}
override delete(key: string): boolean {
const result = super.delete(key);
this.updateContextKey();
return result;
}
abstract updateContextKey(): void;
}
/**
* Key - normalized path used in user interface
* Value - path extracted from the output of the `git status` command
* used when calling `git config --global --add safe.directory`
*/
class UnsafeRepositoryMap extends RepositoryMap<string> {
updateContextKey(): void {
commands.executeCommand('setContext', 'git.unsafeRepositoryCount', this.size);
}
}
/**
* Key - normalized path used in user interface
* Value - value indicating whether the repository should be opened
*/
class ParentRepositoryMap extends RepositoryMap {
updateContextKey(): void {
commands.executeCommand('setContext', 'git.parentRepositoryCount', this.size);
}
}
export interface ModelChangeEvent {
repository: Repository;
uri: Uri;
@@ -51,7 +92,7 @@ interface OpenRepository extends Disposable {
repository: Repository;
}
export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry {
export class Model implements IBranchProtectionProviderRegistry, IRemoteSourcePublisherRegistry, IPostCommitCommandsProviderRegistry, IPushErrorHandlerRegistry {
private _onDidOpenRepository = new EventEmitter<Repository>();
readonly onDidOpenRepository: Event<Repository> = this._onDidOpenRepository.event;
@@ -111,12 +152,36 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
private _onDidChangePostCommitCommandsProviders = new EventEmitter<void>();
readonly onDidChangePostCommitCommandsProviders = this._onDidChangePostCommitCommandsProviders.event;
private showRepoOnHomeDriveRootWarning = true;
private branchProtectionProviders = new Map<string, Set<BranchProtectionProvider>>();
private _onDidChangeBranchProtectionProviders = new EventEmitter<Uri>();
readonly onDidChangeBranchProtectionProviders = this._onDidChangeBranchProtectionProviders.event;
private pushErrorHandlers = new Set<PushErrorHandler>();
private _unsafeRepositories = new UnsafeRepositoryMap();
get unsafeRepositories(): UnsafeRepositoryMap {
return this._unsafeRepositories;
}
private _parentRepositories = new ParentRepositoryMap();
get parentRepositories(): ParentRepositoryMap {
return this._parentRepositories;
}
/**
* We maintain a map containing both the path and the canonical path of the
* workspace folders. We are doing this as `git.exe` expands the symbolic links
* while there are scenarios in which VS Code does not.
*
* Key - path of the workspace folder
* Value - canonical path of the workspace folder
*/
private _workspaceFolders = new Map<string, string>();
private disposables: Disposable[] = [];
constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private outputChannelLogger: OutputChannelLogger, private telemetryReporter: TelemetryReporter) {
constructor(readonly git: Git, private readonly askpass: Askpass, private globalState: Memento, private logger: LogOutputChannel, private telemetryReporter: TelemetryReporter) {
workspace.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders, this, this.disposables);
window.onDidChangeVisibleTextEditors(this.onDidChangeVisibleTextEditors, this, this.disposables);
workspace.onDidChangeConfiguration(this.onDidChangeConfiguration, this, this.disposables);
@@ -134,11 +199,40 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
}
private async doInitialScan(): Promise<void> {
await Promise.all([
const config = workspace.getConfiguration('git');
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt');
// Initial repository scan function
const initialScanFn = () => Promise.all([
this.onDidChangeWorkspaceFolders({ added: workspace.workspaceFolders || [], removed: [] }),
this.onDidChangeVisibleTextEditors(window.visibleTextEditors),
this.scanWorkspaceFolders()
]);
if (config.get<boolean>('showProgress', true)) {
await window.withProgress({ location: ProgressLocation.SourceControl }, initialScanFn);
} else {
await initialScanFn();
}
if (this._parentRepositories.size !== 0 &&
parentRepositoryConfig === 'prompt') {
// Parent repositories notification
this.showParentRepositoryNotification();
} else if (this._unsafeRepositories.size !== 0) {
// Unsafe repositories notification
this.showUnsafeRepositoryNotification();
}
/* __GDPR__
"git.repositoryInitialScan" : {
"owner": "lszomoru",
"autoRepositoryDetection": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Setting that controls the initial repository scan" },
"repositoryCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Number of repositories opened during initial repository scan" }
}
*/
this.telemetryReporter.sendTelemetryEvent('git.repositoryInitialScan', { autoRepositoryDetection: String(autoRepositoryDetection) }, { repositoryCount: this.openRepositories.length });
}
/**
@@ -149,7 +243,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
private async scanWorkspaceFolders(): Promise<void> {
const config = workspace.getConfiguration('git');
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
this.outputChannelLogger.logTrace(`[swsf] Scan workspace sub folders. autoRepositoryDetection=${autoRepositoryDetection}`);
this.logger.trace(`[swsf] Scan workspace sub folders. autoRepositoryDetection=${autoRepositoryDetection}`);
if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'subFolders') {
return;
@@ -157,7 +251,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
await Promise.all((workspace.workspaceFolders || []).map(async folder => {
const root = folder.uri.fsPath;
this.outputChannelLogger.logTrace(`[swsf] Workspace folder: ${root}`);
this.logger.trace(`[swsf] Workspace folder: ${root}`);
// Workspace folder children
const repositoryScanMaxDepth = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get<number>('repositoryScanMaxDepth', 1);
@@ -167,17 +261,17 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
// Repository scan folders
const scanPaths = (workspace.isTrusted ? workspace.getConfiguration('git', folder.uri) : config).get<string[]>('scanRepositories') || [];
this.outputChannelLogger.logTrace(`[swsf] Workspace scan settings: repositoryScanMaxDepth=${repositoryScanMaxDepth}; repositoryScanIgnoredFolders=[${repositoryScanIgnoredFolders.join(', ')}]; scanRepositories=[${scanPaths.join(', ')}]`);
this.logger.trace(`[swsf] Workspace scan settings: repositoryScanMaxDepth=${repositoryScanMaxDepth}; repositoryScanIgnoredFolders=[${repositoryScanIgnoredFolders.join(', ')}]; scanRepositories=[${scanPaths.join(', ')}]`);
for (const scanPath of scanPaths) {
if (scanPath === '.git') {
this.outputChannelLogger.logTrace('[swsf] \'.git\' not supported in \'git.scanRepositories\' setting.');
this.logger.trace('[swsf] \'.git\' not supported in \'git.scanRepositories\' setting.');
continue;
}
if (path.isAbsolute(scanPath)) {
const notSupportedMessage = localize('not supported', "Absolute paths not supported in 'git.scanRepositories' setting.");
this.outputChannelLogger.logWarning(notSupportedMessage);
const notSupportedMessage = l10n.t('Absolute paths not supported in "git.scanRepositories" setting.');
this.logger.warn(notSupportedMessage);
console.warn(notSupportedMessage);
continue;
}
@@ -185,7 +279,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
subfolders.add(path.join(root, scanPath));
}
this.outputChannelLogger.logTrace(`[swsf] Workspace scan sub folders: [${[...subfolders].join(', ')}]`);
this.logger.trace(`[swsf] Workspace scan sub folders: [${[...subfolders].join(', ')}]`);
await Promise.all([...subfolders].map(f => this.openRepository(f)));
}));
}
@@ -256,7 +350,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
.filter(r => !(workspace.workspaceFolders || []).some(f => isDescendant(f.uri.fsPath, r!.repository.root))) as OpenRepository[];
openRepositoriesToDispose.forEach(r => r.dispose());
this.outputChannelLogger.logTrace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
this.logger.trace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
await Promise.all(possibleRepositoryFolders.map(p => this.openRepository(p.uri.fsPath)));
}
@@ -270,20 +364,20 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
.filter(({ root }) => workspace.getConfiguration('git', root).get<boolean>('enabled') !== true)
.map(({ repository }) => repository);
this.outputChannelLogger.logTrace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
this.logger.trace(`[swf] Scan workspace folders: [${possibleRepositoryFolders.map(p => p.uri.fsPath).join(', ')}]`);
possibleRepositoryFolders.forEach(p => this.openRepository(p.uri.fsPath));
openRepositoriesToDispose.forEach(r => r.dispose());
}
private async onDidChangeVisibleTextEditors(editors: readonly TextEditor[]): Promise<void> {
if (!workspace.isTrusted) {
this.outputChannelLogger.logTrace('[svte] Workspace is not trusted.');
this.logger.trace('[svte] Workspace is not trusted.');
return;
}
const config = workspace.getConfiguration('git');
const autoRepositoryDetection = config.get<boolean | 'subFolders' | 'openEditors'>('autoRepositoryDetection');
this.outputChannelLogger.logTrace(`[svte] Scan visible text editors. autoRepositoryDetection=${autoRepositoryDetection}`);
this.logger.trace(`[svte] Scan visible text editors. autoRepositoryDetection=${autoRepositoryDetection}`);
if (autoRepositoryDetection !== true && autoRepositoryDetection !== 'openEditors') {
return;
@@ -299,20 +393,20 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
const repository = this.getRepository(uri);
if (repository) {
this.outputChannelLogger.logTrace(`[svte] Repository for editor resource ${uri.fsPath} already exists: ${repository.root}`);
this.logger.trace(`[svte] Repository for editor resource ${uri.fsPath} already exists: ${repository.root}`);
return;
}
this.outputChannelLogger.logTrace(`[svte] Open repository for editor resource ${uri.fsPath}`);
this.logger.trace(`[svte] Open repository for editor resource ${uri.fsPath}`);
await this.openRepository(path.dirname(uri.fsPath));
}));
}
@sequentialize
async openRepository(repoPath: string): Promise<void> {
this.outputChannelLogger.logTrace(`Opening repository: ${repoPath}`);
if (this.getRepository(repoPath)) {
this.outputChannelLogger.logTrace(`Repository for path ${repoPath} already exists`);
this.logger.trace(`Opening repository: ${repoPath}`);
if (this.getRepositoryExact(repoPath)) {
this.logger.trace(`Repository for path ${repoPath} already exists`);
return;
}
@@ -320,7 +414,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
const enabled = config.get<boolean>('enabled') === true;
if (!enabled) {
this.outputChannelLogger.logTrace('Git is not enabled');
this.logger.trace('Git is not enabled');
return;
}
@@ -330,7 +424,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
fs.accessSync(path.join(repoPath, 'HEAD'), fs.constants.F_OK);
const result = await this.git.exec(repoPath, ['-C', repoPath, 'rev-parse', '--show-cdup']);
if (result.stderr.trim() === '' && result.stdout.trim() === '') {
this.outputChannelLogger.logTrace(`Bare repository: ${repoPath}`);
this.logger.trace(`Bare repository: ${repoPath}`);
return;
}
} catch {
@@ -339,49 +433,88 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
}
try {
const rawRoot = await this.git.getRepositoryRoot(repoPath);
const { repositoryRoot, unsafeRepositoryMatch } = await this.getRepositoryRoot(repoPath);
this.logger.trace(`Repository root for path ${repoPath} is: ${repositoryRoot}`);
// This can happen whenever `path` has the wrong case sensitivity in
// case insensitive file systems
// https://github.com/microsoft/vscode/issues/33498
const repositoryRoot = Uri.file(rawRoot).fsPath;
this.outputChannelLogger.logTrace(`Repository root: ${repositoryRoot}`);
if (this.getRepository(repositoryRoot)) {
this.outputChannelLogger.logTrace(`Repository for path ${repositoryRoot} already exists`);
if (this.getRepositoryExact(repositoryRoot)) {
this.logger.trace(`Repository for path ${repositoryRoot} already exists`);
return;
}
if (this.shouldRepositoryBeIgnored(rawRoot)) {
this.outputChannelLogger.logTrace(`Repository for path ${repositoryRoot} is ignored`);
if (this.shouldRepositoryBeIgnored(repositoryRoot)) {
this.logger.trace(`Repository for path ${repositoryRoot} is ignored`);
return;
}
// On Window, opening a git repository from the root of the HOMEDRIVE poses a security risk.
// We will only a open git repository from the root of the HOMEDRIVE if the user explicitly
// opens the HOMEDRIVE as a folder. Only show the warning once during repository discovery.
if (process.platform === 'win32' && process.env.HOMEDRIVE && pathEquals(`${process.env.HOMEDRIVE}\\`, repositoryRoot)) {
const isRepoInWorkspaceFolders = (workspace.workspaceFolders ?? []).find(f => pathEquals(f.uri.fsPath, repositoryRoot))!!;
// Handle git repositories that are in parent folders
const parentRepositoryConfig = config.get<'always' | 'never' | 'prompt'>('openRepositoryInParentFolders', 'prompt');
if (parentRepositoryConfig !== 'always' && this.globalState.get<boolean>(`parentRepository:${repositoryRoot}`) !== true) {
const isRepositoryOutsideWorkspace = await this.isRepositoryOutsideWorkspace(repositoryRoot);
if (isRepositoryOutsideWorkspace) {
this.logger.trace(`Repository in parent folder: ${repositoryRoot}`);
if (!isRepoInWorkspaceFolders) {
if (this.showRepoOnHomeDriveRootWarning) {
window.showWarningMessage(localize('repoOnHomeDriveRootWarning', "Unable to automatically open the git repository at '{0}'. To open that git repository, open it directly as a folder in VS Code.", repositoryRoot));
this.showRepoOnHomeDriveRootWarning = false;
if (!this._parentRepositories.has(repositoryRoot)) {
// Show a notification if the parent repository is opened after the initial scan
if (this.state === 'initialized' && parentRepositoryConfig === 'prompt') {
this.showParentRepositoryNotification();
}
this._parentRepositories.set(repositoryRoot);
}
this.outputChannelLogger.logTrace(`Repository for path ${repositoryRoot} is on the root of the HOMEDRIVE`);
return;
}
}
// Handle unsafe repositories
if (unsafeRepositoryMatch && unsafeRepositoryMatch.length === 3) {
this.logger.trace(`Unsafe repository: ${repositoryRoot}`);
// Show a notification if the unsafe repository is opened after the initial scan
if (this._state === 'initialized' && !this._unsafeRepositories.has(repositoryRoot)) {
this.showUnsafeRepositoryNotification();
}
this._unsafeRepositories.set(repositoryRoot, unsafeRepositoryMatch[2]);
return;
}
// Open repository
const dotGit = await this.git.getRepositoryDotGit(repositoryRoot);
const repository = new Repository(this.git.open(repositoryRoot, dotGit), this, this, this, this.globalState, this.outputChannelLogger, this.telemetryReporter);
const repository = new Repository(this.git.open(repositoryRoot, dotGit, this.logger), this, this, this, this, this.globalState, this.logger, this.telemetryReporter);
this.open(repository);
repository.status(); // do not await this, we want SCM to know about the repo asap
} catch (ex) {
} catch (err) {
// noop
this.outputChannelLogger.logTrace(`Opening repository for path='${repoPath}' failed; ex=${ex}`);
this.logger.trace(`Opening repository for path='${repoPath}' failed; ex=${err}`);
}
}
async openParentRepository(repoPath: string): Promise<void> {
// Mark the repository to be opened from the parent folders
this.globalState.update(`parentRepository:${repoPath}`, true);
await this.openRepository(repoPath);
this.parentRepositories.delete(repoPath);
}
private async getRepositoryRoot(repoPath: string): Promise<{ repositoryRoot: string; unsafeRepositoryMatch: RegExpMatchArray | null }> {
try {
const rawRoot = await this.git.getRepositoryRoot(repoPath);
// This can happen whenever `path` has the wrong case sensitivity in case
// insensitive file systems https://github.com/microsoft/vscode/issues/33498
return { repositoryRoot: Uri.file(rawRoot).fsPath, unsafeRepositoryMatch: null };
} catch (err) {
// Handle unsafe repository
const unsafeRepositoryMatch = /^fatal: detected dubious ownership in repository at \'([^']+)\'[\s\S]*git config --global --add safe\.directory '?([^'\n]+)'?$/m.exec(err.stderr);
if (unsafeRepositoryMatch && unsafeRepositoryMatch.length === 3) {
return { repositoryRoot: path.normalize(unsafeRepositoryMatch[1]), unsafeRepositoryMatch };
}
throw err;
}
}
@@ -407,7 +540,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
}
private open(repository: Repository): void {
this.outputChannelLogger.logInfo(`Open repository: ${repository.root}`);
this.logger.info(`Open repository: ${repository.root}`);
const onDidDisappearRepository = filterEvent(repository.onDidChangeState, state => state === RepositoryState.Disposed);
const disappearListener = onDidDisappearRepository(() => dispose());
@@ -424,28 +557,62 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
const checkForSubmodules = () => {
if (!shouldDetectSubmodules) {
this.logger.trace('Automatic detection of git submodules is not enabled.');
return;
}
if (repository.submodules.length > submodulesLimit) {
window.showWarningMessage(localize('too many submodules', "The '{0}' repository has {1} submodules which won't be opened automatically. You can still open each one individually by opening a file within.", path.basename(repository.root), repository.submodules.length));
window.showWarningMessage(l10n.t('The "{0}" repository has {1} submodules which won\'t be opened automatically. You can still open each one individually by opening a file within.', path.basename(repository.root), repository.submodules.length));
statusListener.dispose();
}
repository.submodules
.slice(0, submodulesLimit)
.map(r => path.join(repository.root, r.path))
.forEach(p => this.eventuallyScanPossibleGitRepository(p));
.forEach(p => {
this.logger.trace(`Opening submodule: '${p}'`);
this.eventuallyScanPossibleGitRepository(p);
});
};
const statusListener = repository.onDidRunGitStatus(checkForSubmodules);
const updateMergeChanges = () => {
// set mergeChanges context
const mergeChanges: Uri[] = [];
for (const { repository } of this.openRepositories.values()) {
for (const state of repository.mergeGroup.resourceStates) {
mergeChanges.push(state.resourceUri);
}
}
commands.executeCommand('setContext', 'git.mergeChanges', mergeChanges);
};
const statusListener = repository.onDidRunGitStatus(() => {
checkForSubmodules();
updateMergeChanges();
});
checkForSubmodules();
const updateOperationInProgressContext = () => {
let operationInProgress = false;
for (const { repository } of this.openRepositories.values()) {
if (repository.operations.shouldDisableCommands()) {
operationInProgress = true;
}
}
commands.executeCommand('setContext', 'operationInProgress', operationInProgress);
};
const operationEvent = anyEvent(repository.onDidRunOperation as Event<any>, repository.onRunOperation as Event<any>);
const operationListener = operationEvent(() => updateOperationInProgressContext());
updateOperationInProgressContext();
const dispose = () => {
disappearListener.dispose();
changeListener.dispose();
originalResourceChangeListener.dispose();
statusListener.dispose();
operationListener.dispose();
repository.dispose();
this.openRepositories = this.openRepositories.filter(e => e !== openRepository);
@@ -454,6 +621,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
const openRepository = { repository, dispose };
this.openRepositories.push(openRepository);
updateMergeChanges();
this._onDidOpenRepository.fire(repository);
}
@@ -464,13 +632,13 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
return;
}
this.outputChannelLogger.logInfo(`Close repository: ${repository.root}`);
this.logger.info(`Close repository: ${repository.root}`);
openRepository.dispose();
}
async pickRepository(): Promise<Repository | undefined> {
if (this.openRepositories.length === 0) {
throw new Error(localize('no repositories', "There are no available repositories"));
throw new Error(l10n.t('There are no available repositories'));
}
const picks = this.openRepositories.map((e, index) => new RepositoryPick(e.repository, index));
@@ -483,7 +651,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
picks.unshift(...picks.splice(index, 1));
}
const placeHolder = localize('pick repo', "Choose a repository");
const placeHolder = l10n.t('Choose a repository');
const pick = await window.showQuickPick(picks, { placeHolder });
return pick && pick.repository;
@@ -498,6 +666,12 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
return liveRepository && liveRepository.repository;
}
private getRepositoryExact(repoPath: string): Repository | undefined {
const openRepository = this.openRepositories
.find(r => pathEquals(r.repository.root, repoPath));
return openRepository?.repository;
}
private getOpenRepository(repository: Repository): OpenRepository | undefined;
private getOpenRepository(sourceControl: SourceControl): OpenRepository | undefined;
private getOpenRepository(resourceGroup: SourceControlResourceGroup): OpenRepository | undefined;
@@ -556,7 +730,7 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
return liveRepository;
}
if (hint === repository.mergeGroup || hint === repository.indexGroup || hint === repository.workingTreeGroup) {
if (hint === repository.mergeGroup || hint === repository.indexGroup || hint === repository.workingTreeGroup || hint === repository.untrackedGroup) {
return liveRepository;
}
}
@@ -592,6 +766,31 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
return [...this.remoteSourcePublishers.values()];
}
registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable {
const providerDisposables: Disposable[] = [];
this.branchProtectionProviders.set(root.toString(), (this.branchProtectionProviders.get(root.toString()) ?? new Set()).add(provider));
providerDisposables.push(provider.onDidChangeBranchProtection(uri => this._onDidChangeBranchProtectionProviders.fire(uri)));
this._onDidChangeBranchProtectionProviders.fire(root);
return toDisposable(() => {
const providers = this.branchProtectionProviders.get(root.toString());
if (providers && providers.has(provider)) {
providers.delete(provider);
this.branchProtectionProviders.set(root.toString(), providers);
this._onDidChangeBranchProtectionProviders.fire(root);
}
dispose(providerDisposables);
});
}
getBranchProtectionProviders(root: Uri): BranchProtectionProvider[] {
return [...(this.branchProtectionProviders.get(root.toString()) ?? new Set()).values()];
}
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable {
this.postCommitCommandsProviders.add(provider);
this._onDidChangePostCommitCommandsProviders.fire();
@@ -619,6 +818,89 @@ export class Model implements IRemoteSourcePublisherRegistry, IPostCommitCommand
return [...this.pushErrorHandlers];
}
private async isRepositoryOutsideWorkspace(repositoryPath: string): Promise<boolean> {
const workspaceFolders = (workspace.workspaceFolders || [])
.filter(folder => folder.uri.scheme === 'file');
if (workspaceFolders.length === 0) {
return true;
}
const result = await Promise.all(workspaceFolders.map(async folder => {
const workspaceFolderRealPath = await this.getWorkspaceFolderRealPath(folder);
return workspaceFolderRealPath ? pathEquals(workspaceFolderRealPath, repositoryPath) || isDescendant(workspaceFolderRealPath, repositoryPath) : undefined;
}));
return !result.some(r => r);
}
private async getWorkspaceFolderRealPath(workspaceFolder: WorkspaceFolder): Promise<string | undefined> {
let result = this._workspaceFolders.get(workspaceFolder.uri.fsPath);
if (!result) {
try {
result = await fs.promises.realpath(workspaceFolder.uri.fsPath, { encoding: 'utf8' });
this._workspaceFolders.set(workspaceFolder.uri.fsPath, result);
} catch (err) {
// noop - Workspace folder does not exist
this.logger.trace(`Failed to resolve workspace folder: "${workspaceFolder.uri.fsPath}". ${err}`);
}
}
return result;
}
private async showParentRepositoryNotification(): Promise<void> {
const message = this.parentRepositories.size === 1 ?
l10n.t('A git repository was found in the parent folders of the workspace or the open file(s). Would you like to open the repository?') :
l10n.t('Git repositories were found in the parent folders of the workspace or the open file(s). Would you like to open the repositories?');
const yes = l10n.t('Yes');
const always = l10n.t('Always');
const never = l10n.t('Never');
const choice = await window.showInformationMessage(message, yes, always, never);
if (choice === yes) {
// Open Parent Repositories
commands.executeCommand('git.openRepositoriesInParentFolders');
} else if (choice === always || choice === never) {
// Update setting
const config = workspace.getConfiguration('git');
await config.update('openRepositoryInParentFolders', choice === always ? 'always' : 'never', true);
if (choice === always) {
for (const parentRepository of [...this.parentRepositories.keys()]) {
await this.openParentRepository(parentRepository);
}
}
}
}
private async showUnsafeRepositoryNotification(): Promise<void> {
// If no repositories are open, we will use a welcome view to inform the user
// that a potentially unsafe repository was found so we do not have to show
// the notification
if (this.repositories.length === 0) {
return;
}
const message = this._unsafeRepositories.size === 1 ?
l10n.t('The git repository in the current folder is potentially unsafe as the folder is owned by someone other than the current user.') :
l10n.t('The git repositories in the current folder are potentially unsafe as the folders are owned by someone other than the current user.');
const manageUnsafeRepositories = l10n.t('Manage Unsafe Repositories');
const learnMore = l10n.t('Learn More');
const choice = await window.showErrorMessage(message, manageUnsafeRepositories, learnMore);
if (choice === manageUnsafeRepositories) {
// Manage Unsafe Repositories
commands.executeCommand('git.manageUnsafeRepositories');
} else if (choice === learnMore) {
// Learn More
commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-git-unsafe-repository'));
}
}
dispose(): void {
const openRepositories = [...this.openRepositories];
openRepositories.forEach(r => r.dispose());

View File

@@ -0,0 +1,273 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { LogOutputChannel } from 'vscode';
export const enum OperationKind {
Add = 'Add',
AddNoProgress = 'AddNoProgress',
Apply = 'Apply',
Blame = 'Blame',
Branch = 'Branch',
CheckIgnore = 'CheckIgnore',
Checkout = 'Checkout',
CheckoutTracking = 'CheckoutTracking',
CherryPick = 'CherryPick',
Clean = 'Clean',
CleanNoProgress = 'CleanNoProgress',
Commit = 'Commit',
Config = 'Config',
DeleteBranch = 'DeleteBranch',
DeleteRef = 'DeleteRef',
DeleteRemoteTag = 'DeleteRemoteTag',
DeleteTag = 'DeleteTag',
Diff = 'Diff',
Fetch = 'Fetch',
FetchNoProgress = 'FetchNoProgress',
FindTrackingBranches = 'GetTracking',
GetBranch = 'GetBranch',
GetBranches = 'GetBranches',
GetCommitTemplate = 'GetCommitTemplate',
GetObjectDetails = 'GetObjectDetails',
GetRefs = 'GetRefs',
GetRemoteRefs = 'GetRemoteRefs',
HashObject = 'HashObject',
Ignore = 'Ignore',
Log = 'Log',
LogFile = 'LogFile',
Merge = 'Merge',
MergeAbort = 'MergeAbort',
MergeBase = 'MergeBase',
Move = 'Move',
PostCommitCommand = 'PostCommitCommand',
Pull = 'Pull',
Push = 'Push',
Remote = 'Remote',
RenameBranch = 'RenameBranch',
Remove = 'Remove',
Reset = 'Reset',
Rebase = 'Rebase',
RebaseAbort = 'RebaseAbort',
RebaseContinue = 'RebaseContinue',
RevertFiles = 'RevertFiles',
RevertFilesNoProgress = 'RevertFilesNoProgress',
SetBranchUpstream = 'SetBranchUpstream',
Show = 'Show',
Stage = 'Stage',
Status = 'Status',
Stash = 'Stash',
SubmoduleUpdate = 'SubmoduleUpdate',
Sync = 'Sync',
Tag = 'Tag',
}
export type Operation = AddOperation | ApplyOperation | BlameOperation | BranchOperation | CheckIgnoreOperation | CherryPickOperation |
CheckoutOperation | CheckoutTrackingOperation | CleanOperation | CommitOperation | ConfigOperation | DeleteBranchOperation |
DeleteRefOperation | DeleteRemoteTagOperation | DeleteTagOperation | DiffOperation | FetchOperation | FindTrackingBranchesOperation |
GetBranchOperation | GetBranchesOperation | GetCommitTemplateOperation | GetObjectDetailsOperation | GetRefsOperation | GetRemoteRefsOperation |
HashObjectOperation | IgnoreOperation | LogOperation | LogFileOperation | MergeOperation | MergeAbortOperation | MergeBaseOperation |
MoveOperation | PostCommitCommandOperation | PullOperation | PushOperation | RemoteOperation | RenameBranchOperation | RemoveOperation |
ResetOperation | RebaseOperation | RebaseAbortOperation | RebaseContinueOperation | RevertFilesOperation | SetBranchUpstreamOperation |
ShowOperation | StageOperation | StatusOperation | StashOperation | SubmoduleUpdateOperation | SyncOperation | TagOperation;
type BaseOperation = { kind: OperationKind; blocking: boolean; readOnly: boolean; remote: boolean; retry: boolean; showProgress: boolean };
export type AddOperation = BaseOperation & { kind: OperationKind.Add };
export type ApplyOperation = BaseOperation & { kind: OperationKind.Apply };
export type BlameOperation = BaseOperation & { kind: OperationKind.Blame };
export type BranchOperation = BaseOperation & { kind: OperationKind.Branch };
export type CheckIgnoreOperation = BaseOperation & { kind: OperationKind.CheckIgnore };
export type CherryPickOperation = BaseOperation & { kind: OperationKind.CherryPick };
export type CheckoutOperation = BaseOperation & { kind: OperationKind.Checkout; refLabel: string };
export type CheckoutTrackingOperation = BaseOperation & { kind: OperationKind.CheckoutTracking; refLabel: string };
export type CleanOperation = BaseOperation & { kind: OperationKind.Clean };
export type CommitOperation = BaseOperation & { kind: OperationKind.Commit };
export type ConfigOperation = BaseOperation & { kind: OperationKind.Config };
export type DeleteBranchOperation = BaseOperation & { kind: OperationKind.DeleteBranch };
export type DeleteRefOperation = BaseOperation & { kind: OperationKind.DeleteRef };
export type DeleteRemoteTagOperation = BaseOperation & { kind: OperationKind.DeleteRemoteTag };
export type DeleteTagOperation = BaseOperation & { kind: OperationKind.DeleteTag };
export type DiffOperation = BaseOperation & { kind: OperationKind.Diff };
export type FetchOperation = BaseOperation & { kind: OperationKind.Fetch };
export type FindTrackingBranchesOperation = BaseOperation & { kind: OperationKind.FindTrackingBranches };
export type GetBranchOperation = BaseOperation & { kind: OperationKind.GetBranch };
export type GetBranchesOperation = BaseOperation & { kind: OperationKind.GetBranches };
export type GetCommitTemplateOperation = BaseOperation & { kind: OperationKind.GetCommitTemplate };
export type GetObjectDetailsOperation = BaseOperation & { kind: OperationKind.GetObjectDetails };
export type GetRefsOperation = BaseOperation & { kind: OperationKind.GetRefs };
export type GetRemoteRefsOperation = BaseOperation & { kind: OperationKind.GetRemoteRefs };
export type HashObjectOperation = BaseOperation & { kind: OperationKind.HashObject };
export type IgnoreOperation = BaseOperation & { kind: OperationKind.Ignore };
export type LogOperation = BaseOperation & { kind: OperationKind.Log };
export type LogFileOperation = BaseOperation & { kind: OperationKind.LogFile };
export type MergeOperation = BaseOperation & { kind: OperationKind.Merge };
export type MergeAbortOperation = BaseOperation & { kind: OperationKind.MergeAbort };
export type MergeBaseOperation = BaseOperation & { kind: OperationKind.MergeBase };
export type MoveOperation = BaseOperation & { kind: OperationKind.Move };
export type PostCommitCommandOperation = BaseOperation & { kind: OperationKind.PostCommitCommand };
export type PullOperation = BaseOperation & { kind: OperationKind.Pull };
export type PushOperation = BaseOperation & { kind: OperationKind.Push };
export type RemoteOperation = BaseOperation & { kind: OperationKind.Remote };
export type RenameBranchOperation = BaseOperation & { kind: OperationKind.RenameBranch };
export type RemoveOperation = BaseOperation & { kind: OperationKind.Remove };
export type ResetOperation = BaseOperation & { kind: OperationKind.Reset };
export type RebaseOperation = BaseOperation & { kind: OperationKind.Rebase };
export type RebaseAbortOperation = BaseOperation & { kind: OperationKind.RebaseAbort };
export type RebaseContinueOperation = BaseOperation & { kind: OperationKind.RebaseContinue };
export type RevertFilesOperation = BaseOperation & { kind: OperationKind.RevertFiles };
export type SetBranchUpstreamOperation = BaseOperation & { kind: OperationKind.SetBranchUpstream };
export type ShowOperation = BaseOperation & { kind: OperationKind.Show };
export type StageOperation = BaseOperation & { kind: OperationKind.Stage };
export type StatusOperation = BaseOperation & { kind: OperationKind.Status };
export type StashOperation = BaseOperation & { kind: OperationKind.Stash };
export type SubmoduleUpdateOperation = BaseOperation & { kind: OperationKind.SubmoduleUpdate };
export type SyncOperation = BaseOperation & { kind: OperationKind.Sync };
export type TagOperation = BaseOperation & { kind: OperationKind.Tag };
export const Operation = {
Add: (showProgress: boolean) => ({ kind: OperationKind.Add, blocking: false, readOnly: false, remote: false, retry: false, showProgress } as AddOperation),
Apply: { kind: OperationKind.Apply, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as ApplyOperation,
Blame: { kind: OperationKind.Blame, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as BlameOperation,
Branch: { kind: OperationKind.Branch, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as BranchOperation,
CheckIgnore: { kind: OperationKind.CheckIgnore, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as CheckIgnoreOperation,
CherryPick: { kind: OperationKind.CherryPick, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as CherryPickOperation,
Checkout: (refLabel: string) => ({ kind: OperationKind.Checkout, blocking: true, readOnly: false, remote: false, retry: false, showProgress: true, refLabel } as CheckoutOperation),
CheckoutTracking: (refLabel: string) => ({ kind: OperationKind.CheckoutTracking, blocking: true, readOnly: false, remote: false, retry: false, showProgress: true, refLabel } as CheckoutTrackingOperation),
Clean: (showProgress: boolean) => ({ kind: OperationKind.Clean, blocking: false, readOnly: false, remote: false, retry: false, showProgress } as CleanOperation),
Commit: { kind: OperationKind.Commit, blocking: true, readOnly: false, remote: false, retry: false, showProgress: true } as CommitOperation,
Config: { kind: OperationKind.Config, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as ConfigOperation,
DeleteBranch: { kind: OperationKind.DeleteBranch, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteBranchOperation,
DeleteRef: { kind: OperationKind.DeleteRef, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteRefOperation,
DeleteRemoteTag: { kind: OperationKind.DeleteRemoteTag, blocking: false, readOnly: false, remote: true, retry: false, showProgress: true } as DeleteRemoteTagOperation,
DeleteTag: { kind: OperationKind.DeleteTag, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as DeleteTagOperation,
Diff: { kind: OperationKind.Diff, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as DiffOperation,
Fetch: (showProgress: boolean) => ({ kind: OperationKind.Fetch, blocking: false, readOnly: false, remote: true, retry: true, showProgress } as FetchOperation),
FindTrackingBranches: { kind: OperationKind.FindTrackingBranches, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as FindTrackingBranchesOperation,
GetBranch: { kind: OperationKind.GetBranch, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetBranchOperation,
GetBranches: { kind: OperationKind.GetBranches, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetBranchesOperation,
GetCommitTemplate: { kind: OperationKind.GetCommitTemplate, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as GetCommitTemplateOperation,
GetObjectDetails: { kind: OperationKind.GetObjectDetails, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetObjectDetailsOperation,
GetRefs: { kind: OperationKind.GetRefs, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as GetRefsOperation,
GetRemoteRefs: { kind: OperationKind.GetRemoteRefs, blocking: false, readOnly: true, remote: true, retry: false, showProgress: false } as GetRemoteRefsOperation,
HashObject: { kind: OperationKind.HashObject, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as HashObjectOperation,
Ignore: { kind: OperationKind.Ignore, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as IgnoreOperation,
Log: { kind: OperationKind.Log, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as LogOperation,
LogFile: { kind: OperationKind.LogFile, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as LogFileOperation,
Merge: { kind: OperationKind.Merge, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as MergeOperation,
MergeAbort: { kind: OperationKind.MergeAbort, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as MergeAbortOperation,
MergeBase: { kind: OperationKind.MergeBase, blocking: false, readOnly: true, remote: false, retry: false, showProgress: true } as MergeBaseOperation,
Move: { kind: OperationKind.Move, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as MoveOperation,
PostCommitCommand: { kind: OperationKind.PostCommitCommand, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as PostCommitCommandOperation,
Pull: { kind: OperationKind.Pull, blocking: true, readOnly: false, remote: true, retry: true, showProgress: true } as PullOperation,
Push: { kind: OperationKind.Push, blocking: true, readOnly: false, remote: true, retry: false, showProgress: true } as PushOperation,
Remote: { kind: OperationKind.Remote, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RemoteOperation,
RenameBranch: { kind: OperationKind.RenameBranch, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RenameBranchOperation,
Remove: { kind: OperationKind.Remove, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RemoveOperation,
Reset: { kind: OperationKind.Reset, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as ResetOperation,
Rebase: { kind: OperationKind.Rebase, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RebaseOperation,
RebaseAbort: { kind: OperationKind.RebaseAbort, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RebaseAbortOperation,
RebaseContinue: { kind: OperationKind.RebaseContinue, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as RebaseContinueOperation,
RevertFiles: (showProgress: boolean) => ({ kind: OperationKind.RevertFiles, blocking: false, readOnly: false, remote: false, retry: false, showProgress } as RevertFilesOperation),
SetBranchUpstream: { kind: OperationKind.SetBranchUpstream, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as SetBranchUpstreamOperation,
Show: { kind: OperationKind.Show, blocking: false, readOnly: true, remote: false, retry: false, showProgress: false } as ShowOperation,
Stage: { kind: OperationKind.Stage, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StageOperation,
Status: { kind: OperationKind.Status, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StatusOperation,
Stash: { kind: OperationKind.Stash, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as StashOperation,
SubmoduleUpdate: { kind: OperationKind.SubmoduleUpdate, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as SubmoduleUpdateOperation,
Sync: { kind: OperationKind.Sync, blocking: true, readOnly: false, remote: true, retry: true, showProgress: true } as SyncOperation,
Tag: { kind: OperationKind.Tag, blocking: false, readOnly: false, remote: false, retry: false, showProgress: true } as TagOperation
};
export interface OperationResult {
operation: Operation;
error: any;
}
interface IOperationManager {
getOperations(operationKind: OperationKind): Operation[];
isIdle(): boolean;
isRunning(operationKind: OperationKind): boolean;
shouldDisableCommands(): boolean;
shouldShowProgress(): boolean;
}
export class OperationManager implements IOperationManager {
private operations = new Map<OperationKind, Set<Operation>>();
constructor(private readonly logger: LogOutputChannel) { }
start(operation: Operation): void {
if (this.operations.has(operation.kind)) {
this.operations.get(operation.kind)!.add(operation);
} else {
this.operations.set(operation.kind, new Set([operation]));
}
this.logger.trace(`Operation start: ${operation.kind} (blocking: ${operation.blocking}, readOnly: ${operation.readOnly}; retry: ${operation.retry}; showProgress: ${operation.showProgress})`);
}
end(operation: Operation): void {
const operationSet = this.operations.get(operation.kind);
if (operationSet) {
operationSet.delete(operation);
if (operationSet.size === 0) {
this.operations.delete(operation.kind);
}
}
this.logger.trace(`Operation end: ${operation.kind} (blocking: ${operation.blocking}, readOnly: ${operation.readOnly}; retry: ${operation.retry}; showProgress: ${operation.showProgress})`);
}
getOperations(operationKind: OperationKind): Operation[] {
const operationSet = this.operations.get(operationKind);
return operationSet ? Array.from(operationSet) : [];
}
isIdle(): boolean {
const operationSets = this.operations.values();
for (const operationSet of operationSets) {
for (const operation of operationSet) {
if (!operation.readOnly) {
return false;
}
}
}
return true;
}
isRunning(operationKind: OperationKind): boolean {
return this.operations.has(operationKind);
}
shouldDisableCommands(): boolean {
const operationSets = this.operations.values();
for (const operationSet of operationSets) {
for (const operation of operationSet) {
if (operation.blocking) {
return true;
}
}
}
return false;
}
shouldShowProgress(): boolean {
const operationSets = this.operations.values();
for (const operationSet of operationSets) {
for (const operation of operationSet) {
if (operation.showProgress) {
return true;
}
}
}
return false;
}
}

View File

@@ -3,9 +3,12 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { Command, Disposable, Event } from 'vscode';
import { Command, commands, Disposable, Event, EventEmitter, Memento, Uri, workspace, l10n } from 'vscode';
import { PostCommitCommandsProvider } from './api/git';
import { Repository } from './repository';
import { ApiRepository } from './api/api1';
import { dispose } from './util';
import { OperationKind } from './operation';
export interface IPostCommitCommandsProviderRegistry {
readonly onDidChangePostCommitCommandsProviders: Event<void>;
@@ -14,19 +17,214 @@ export interface IPostCommitCommandsProviderRegistry {
registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable;
}
const localize = nls.loadMessageBundle();
export class GitPostCommitCommandsProvider implements PostCommitCommandsProvider {
getCommands(): Command[] {
getCommands(apiRepository: ApiRepository): Command[] {
const config = workspace.getConfiguration('git', Uri.file(apiRepository.repository.root));
// Branch protection
const isBranchProtected = apiRepository.repository.isBranchProtected();
const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!;
const alwaysPrompt = isBranchProtected && branchProtectionPrompt === 'alwaysPrompt';
const alwaysCommitToNewBranch = isBranchProtected && branchProtectionPrompt === 'alwaysCommitToNewBranch';
// Icon
const repository = apiRepository.repository;
const isCommitInProgress = repository.operations.isRunning(OperationKind.Commit) || repository.operations.isRunning(OperationKind.PostCommitCommand);
const icon = isCommitInProgress ? '$(sync~spin)' : alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined;
// Tooltip (default)
let pushCommandTooltip = !alwaysCommitToNewBranch ?
l10n.t('Commit & Push Changes') :
l10n.t('Commit to New Branch & Push Changes');
let syncCommandTooltip = !alwaysCommitToNewBranch ?
l10n.t('Commit & Sync Changes') :
l10n.t('Commit to New Branch & Synchronize Changes');
// Tooltip (in progress)
if (isCommitInProgress) {
pushCommandTooltip = !alwaysCommitToNewBranch ?
l10n.t('Committing & Pushing Changes...') :
l10n.t('Committing to New Branch & Pushing Changes...');
syncCommandTooltip = !alwaysCommitToNewBranch ?
l10n.t('Committing & Synchronizing Changes...') :
l10n.t('Committing to New Branch & Synchronizing Changes...');
}
return [
{
command: 'git.push',
title: localize('scm secondary button commit and push', "Commit & Push")
title: l10n.t('{0} Commit & Push', icon ?? '$(arrow-up)'),
tooltip: pushCommandTooltip
},
{
command: 'git.sync',
title: localize('scm secondary button commit and sync', "Commit & Sync")
title: l10n.t('{0} Commit & Sync', icon ?? '$(sync)'),
tooltip: syncCommandTooltip
},
];
}
}
export class CommitCommandsCenter {
private _onDidChange = new EventEmitter<void>();
get onDidChange(): Event<void> { return this._onDidChange.event; }
private disposables: Disposable[] = [];
set postCommitCommand(command: string | null | undefined) {
if (command === undefined) {
// Commit WAS NOT initiated using the action button
// so there is no need to store the post-commit command
return;
}
this.globalState.update(this.getGlobalStateKey(), command)
.then(() => this._onDidChange.fire());
}
constructor(
private readonly globalState: Memento,
private readonly repository: Repository,
private readonly postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry
) {
const root = Uri.file(repository.root);
// Migrate post commit command storage
this.migratePostCommitCommandStorage()
.then(() => {
const onRememberPostCommitCommandChange = async () => {
const config = workspace.getConfiguration('git', root);
if (!config.get<boolean>('rememberPostCommitCommand')) {
await this.globalState.update(this.getGlobalStateKey(), undefined);
}
};
this.disposables.push(workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('git.rememberPostCommitCommand', root)) {
onRememberPostCommitCommandChange();
}
}));
onRememberPostCommitCommandChange();
this.disposables.push(postCommitCommandsProviderRegistry.onDidChangePostCommitCommandsProviders(() => this._onDidChange.fire()));
});
}
getPrimaryCommand(): Command {
const allCommands = this.getSecondaryCommands().map(c => c).flat();
const commandFromStorage = allCommands.find(c => c.arguments?.length === 2 && c.arguments[1] === this.getPostCommitCommandStringFromStorage());
const commandFromSetting = allCommands.find(c => c.arguments?.length === 2 && c.arguments[1] === this.getPostCommitCommandStringFromSetting());
return commandFromStorage ?? commandFromSetting ?? this.getCommitCommand();
}
getSecondaryCommands(): Command[][] {
const commandGroups: Command[][] = [];
for (const provider of this.postCommitCommandsProviderRegistry.getPostCommitCommandsProviders()) {
const commands = provider.getCommands(new ApiRepository(this.repository));
commandGroups.push((commands ?? []).map(c => {
return { command: 'git.commit', title: c.title, tooltip: c.tooltip, arguments: [this.repository.sourceControl, c.command] };
}));
}
if (commandGroups.length > 0) {
commandGroups[0].splice(0, 0, this.getCommitCommand());
}
return commandGroups;
}
async executePostCommitCommand(command: string | null | undefined): Promise<void> {
try {
if (command === null) {
// No post-commit command
return;
}
if (command === undefined) {
// Commit WAS NOT initiated using the action button (ex: keybinding, toolbar action,
// command palette) so we have to honour the default post commit command (memento/setting).
const primaryCommand = this.getPrimaryCommand();
command = primaryCommand.arguments?.length === 2 ? primaryCommand.arguments[1] : null;
}
if (command !== null) {
await commands.executeCommand(command!.toString(), new ApiRepository(this.repository));
}
} catch (err) {
throw err;
}
finally {
if (!this.isRememberPostCommitCommandEnabled()) {
await this.globalState.update(this.getGlobalStateKey(), undefined);
this._onDidChange.fire();
}
}
}
private getGlobalStateKey(): string {
return `postCommitCommand:${this.repository.root}`;
}
private getCommitCommand(): Command {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
// Branch protection
const isBranchProtected = this.repository.isBranchProtected();
const branchProtectionPrompt = config.get<'alwaysCommit' | 'alwaysCommitToNewBranch' | 'alwaysPrompt'>('branchProtectionPrompt')!;
const alwaysPrompt = isBranchProtected && branchProtectionPrompt === 'alwaysPrompt';
const alwaysCommitToNewBranch = isBranchProtected && branchProtectionPrompt === 'alwaysCommitToNewBranch';
// Icon
const icon = alwaysPrompt ? '$(lock)' : alwaysCommitToNewBranch ? '$(git-branch)' : undefined;
// Tooltip (default)
const branch = this.repository.HEAD?.name;
let tooltip = alwaysCommitToNewBranch ?
l10n.t('Commit Changes to New Branch') :
branch ?
l10n.t('Commit Changes on "{0}"', branch) :
l10n.t('Commit Changes');
// Tooltip (in progress)
if (this.repository.operations.isRunning(OperationKind.Commit)) {
tooltip = !alwaysCommitToNewBranch ?
l10n.t('Committing Changes...') :
l10n.t('Committing Changes to New Branch...');
}
return { command: 'git.commit', title: l10n.t('{0} Commit', icon ?? '$(check)'), tooltip, arguments: [this.repository.sourceControl, null] };
}
private getPostCommitCommandStringFromSetting(): string | undefined {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const postCommitCommandSetting = config.get<string>('postCommitCommand');
return postCommitCommandSetting === 'push' || postCommitCommandSetting === 'sync' ? `git.${postCommitCommandSetting}` : undefined;
}
private getPostCommitCommandStringFromStorage(): string | null | undefined {
return this.globalState.get<string | null>(this.getGlobalStateKey());
}
private async migratePostCommitCommandStorage(): Promise<void> {
const postCommitCommandString = this.globalState.get<string | null>(this.repository.root);
if (postCommitCommandString !== undefined) {
await this.globalState.update(this.getGlobalStateKey(), postCommitCommandString);
await this.globalState.update(this.repository.root, undefined);
}
}
private isRememberPostCommitCommandEnabled(): boolean {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
return config.get<boolean>('rememberPostCommitCommand') === true;
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
}

View File

@@ -3,36 +3,45 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { UriHandler, Uri, window, Disposable, commands } from 'vscode';
import { UriHandler, Uri, window, Disposable, commands, LogOutputChannel, l10n } from 'vscode';
import { dispose } from './util';
import * as querystring from 'querystring';
const schemes = new Set(['file', 'git', 'http', 'https', 'ssh']);
const refRegEx = /^$|[~\^:\\\*\s\[\]]|^-|^\.|\/\.|\.\.|\.lock\/|\.lock$|\/$|\.$/;
export class GitProtocolHandler implements UriHandler {
private disposables: Disposable[] = [];
constructor() {
constructor(private readonly logger: LogOutputChannel) {
this.disposables.push(window.registerUriHandler(this));
}
handleUri(uri: Uri): void {
this.logger.info(`GitProtocolHandler.handleUri(${uri.toString()})`);
switch (uri.path) {
case '/clone': this.clone(uri);
}
}
private clone(uri: Uri): void {
private async clone(uri: Uri): Promise<void> {
const data = querystring.parse(uri.query);
const ref = data.ref;
if (!data.url) {
console.warn('Failed to open URI:', uri);
this.logger.warn('Failed to open URI:' + uri.toString());
return;
}
if (Array.isArray(data.url) && data.url.length === 0) {
console.warn('Failed to open URI:', uri);
this.logger.warn('Failed to open URI:' + uri.toString());
return;
}
if (ref !== undefined && typeof ref !== 'string') {
this.logger.warn('Failed to open URI due to multiple references:' + uri.toString());
return;
}
@@ -50,13 +59,33 @@ export class GitProtocolHandler implements UriHandler {
if (!schemes.has(cloneUri.scheme.toLowerCase())) {
throw new Error('Unsupported scheme.');
}
// Validate the reference
if (typeof ref === 'string' && refRegEx.test(ref)) {
throw new Error('Invalid reference.');
}
}
catch (ex) {
console.warn('Invalid URI:', uri);
this.logger.warn('Invalid URI:' + uri.toString());
return;
}
commands.executeCommand('git.clone', cloneUri.toString(true));
if (!(await commands.getCommands(true)).includes('git.clone')) {
this.logger.error('Could not complete git clone operation as git installation was not found.');
const errorMessage = l10n.t('Could not clone your repository as Git is not installed.');
const downloadGit = l10n.t('Download Git');
if (await window.showErrorMessage(errorMessage, { modal: true }, downloadGit) === downloadGit) {
commands.executeCommand('vscode.open', Uri.parse('https://aka.ms/vscode-download-git'));
}
return;
} else {
const cloneTarget = cloneUri.toString(true);
this.logger.info(`Executing git.clone for ${cloneTarget}`);
commands.executeCommand('git.clone', cloneTarget, undefined, { ref: ref });
}
}
dispose(): void {

View File

@@ -11,3 +11,7 @@ export async function pickRemoteSource(options: PickRemoteSourceOptions & { bran
export async function pickRemoteSource(options: PickRemoteSourceOptions = {}): Promise<string | PickRemoteSourceResult | undefined> {
return GitBaseApi.getAPI().pickRemoteSource(options);
}
export async function getRemoteSourceActions(url: string) {
return GitBaseApi.getAPI().getRemoteSourceActions(url);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
#!/bin/sh
echo ''

View File

@@ -0,0 +1,5 @@
#!/bin/sh
VSCODE_GIT_ASKPASS_PIPE=`mktemp`
ELECTRON_RUN_AS_NODE="1" VSCODE_GIT_ASKPASS_PIPE="$VSCODE_GIT_ASKPASS_PIPE" VSCODE_GIT_ASKPASS_TYPE="ssh" "$VSCODE_GIT_ASKPASS_NODE" "$VSCODE_GIT_ASKPASS_MAIN" $VSCODE_GIT_ASKPASS_EXTRA_ARGS $*
cat $VSCODE_GIT_ASKPASS_PIPE
rm $VSCODE_GIT_ASKPASS_PIPE

View File

@@ -3,14 +3,18 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, Command, EventEmitter, Event, workspace, Uri } from 'vscode';
import { Repository, Operation } from './repository';
import { Disposable, Command, EventEmitter, Event, workspace, Uri, l10n } from 'vscode';
import { Repository } from './repository';
import { anyEvent, dispose, filterEvent } from './util';
import * as nls from 'vscode-nls';
import { Branch, RemoteSourcePublisher } from './api/git';
import { Branch, RefType, RemoteSourcePublisher } from './api/git';
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
import { CheckoutOperation, CheckoutTrackingOperation, OperationKind } from './operation';
const localize = nls.loadMessageBundle();
interface CheckoutStatusBarState {
readonly isCheckoutRunning: boolean;
readonly isCommitRunning: boolean;
readonly isSyncRunning: boolean;
}
class CheckoutStatusBar {
@@ -18,23 +22,95 @@ class CheckoutStatusBar {
get onDidChange(): Event<void> { return this._onDidChange.event; }
private disposables: Disposable[] = [];
private _state: CheckoutStatusBarState;
private get state() { return this._state; }
private set state(state: CheckoutStatusBarState) {
this._state = state;
this._onDidChange.fire();
}
constructor(private repository: Repository) {
this._state = {
isCheckoutRunning: false,
isCommitRunning: false,
isSyncRunning: false
};
repository.onDidChangeOperations(this.onDidChangeOperations, this, this.disposables);
repository.onDidRunGitStatus(this._onDidChange.fire, this._onDidChange, this.disposables);
repository.onDidChangeBranchProtection(this._onDidChange.fire, this._onDidChange, this.disposables);
}
get command(): Command | undefined {
const operationData = [
...this.repository.operations.getOperations(OperationKind.Checkout) as CheckoutOperation[],
...this.repository.operations.getOperations(OperationKind.CheckoutTracking) as CheckoutTrackingOperation[]
];
const rebasing = !!this.repository.rebaseCommit;
const isBranchProtected = this.repository.isBranchProtected();
const title = `${isBranchProtected ? '$(lock)' : '$(git-branch)'} ${this.repository.headLabel}${rebasing ? ` (${localize('rebasing', 'Rebasing')})` : ''}`;
const label = operationData[0]?.refLabel ?? `${this.repository.headLabel}${rebasing ? ` (${l10n.t('Rebasing')})` : ''}`;
const command = (this.state.isCheckoutRunning || this.state.isCommitRunning || this.state.isSyncRunning) ? '' : 'git.checkout';
return {
command: 'git.checkout',
tooltip: localize('checkout', "Checkout branch/tag..."),
title,
command,
tooltip: `${label}, ${this.getTooltip()}`,
title: `${this.getIcon()} ${label}`,
arguments: [this.repository.sourceControl]
};
}
private getIcon(): string {
if (!this.repository.HEAD) {
return '';
}
// Checkout
if (this.state.isCheckoutRunning) {
return '$(loading~spin)';
}
// Branch
if (this.repository.HEAD.type === RefType.Head && this.repository.HEAD.name) {
return this.repository.isBranchProtected() ? '$(lock)' : '$(git-branch)';
}
// Tag
if (this.repository.HEAD.type === RefType.Tag) {
return '$(tag)';
}
// Commit
return '$(git-commit)';
}
private getTooltip(): string {
if (this.state.isCheckoutRunning) {
return l10n.t('Checking Out Branch/Tag...');
}
if (this.state.isCommitRunning) {
return l10n.t('Committing Changes...');
}
if (this.state.isSyncRunning) {
return l10n.t('Synchronizing Changes...');
}
return l10n.t('Checkout Branch/Tag...');
}
private onDidChangeOperations(): void {
const isCommitRunning = this.repository.operations.isRunning(OperationKind.Commit);
const isCheckoutRunning = this.repository.operations.isRunning(OperationKind.Checkout) ||
this.repository.operations.isRunning(OperationKind.CheckoutTracking);
const isSyncRunning = this.repository.operations.isRunning(OperationKind.Sync) ||
this.repository.operations.isRunning(OperationKind.Push) ||
this.repository.operations.isRunning(OperationKind.Pull);
this.state = { ...this.state, isCheckoutRunning, isCommitRunning, isSyncRunning };
}
dispose(): void {
this.disposables.forEach(d => d.dispose());
}
@@ -42,6 +118,8 @@ class CheckoutStatusBar {
interface SyncStatusBarState {
readonly enabled: boolean;
readonly isCheckoutRunning: boolean;
readonly isCommitRunning: boolean;
readonly isSyncRunning: boolean;
readonly hasRemotes: boolean;
readonly HEAD: Branch | undefined;
@@ -64,6 +142,8 @@ class SyncStatusBar {
constructor(private repository: Repository, private remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry) {
this._state = {
enabled: true,
isCheckoutRunning: false,
isCommitRunning: false,
isSyncRunning: false,
hasRemotes: false,
HEAD: undefined,
@@ -89,11 +169,14 @@ class SyncStatusBar {
}
private onDidChangeOperations(): void {
const isSyncRunning = this.repository.operations.isRunning(Operation.Sync) ||
this.repository.operations.isRunning(Operation.Push) ||
this.repository.operations.isRunning(Operation.Pull);
const isCommitRunning = this.repository.operations.isRunning(OperationKind.Commit);
const isCheckoutRunning = this.repository.operations.isRunning(OperationKind.Checkout) ||
this.repository.operations.isRunning(OperationKind.CheckoutTracking);
const isSyncRunning = this.repository.operations.isRunning(OperationKind.Sync) ||
this.repository.operations.isRunning(OperationKind.Push) ||
this.repository.operations.isRunning(OperationKind.Pull);
this.state = { ...this.state, isSyncRunning };
this.state = { ...this.state, isCheckoutRunning, isCommitRunning, isSyncRunning };
}
private onDidRunGitStatus(): void {
@@ -121,12 +204,16 @@ class SyncStatusBar {
return;
}
const tooltip = this.state.remoteSourcePublishers.length === 1
? localize('publish to', "Publish to {0}", this.state.remoteSourcePublishers[0].name)
: localize('publish to...', "Publish to...");
const command = (this.state.isCheckoutRunning || this.state.isCommitRunning) ? '' : 'git.publish';
const tooltip =
this.state.isCheckoutRunning ? l10n.t('Checking Out Changes...') :
this.state.isCommitRunning ? l10n.t('Committing Changes...') :
this.state.remoteSourcePublishers.length === 1
? l10n.t('Publish to {0}', this.state.remoteSourcePublishers[0].name)
: l10n.t('Publish to...');
return {
command: 'git.publish',
command,
title: `$(cloud-upload)`,
tooltip,
arguments: [this.repository.sourceControl]
@@ -150,17 +237,27 @@ class SyncStatusBar {
} else {
icon = '$(cloud-upload)';
command = 'git.publish';
tooltip = localize('publish branch', "Publish Branch");
tooltip = l10n.t('Publish Branch');
}
} else {
command = '';
tooltip = '';
}
if (this.state.isCheckoutRunning) {
command = '';
tooltip = l10n.t('Checking Out Changes...');
}
if (this.state.isCommitRunning) {
command = '';
tooltip = l10n.t('Committing Changes...');
}
if (this.state.isSyncRunning) {
icon = '$(sync~spin)';
command = '';
tooltip = localize('syncing changes', "Synchronizing Changes...");
tooltip = l10n.t('Synchronizing Changes...');
}
return {

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import 'mocha';
import { GitStatusParser, parseGitCommits, parseGitmodules, parseLsTree, parseLsFiles } from '../git';
import { GitStatusParser, parseGitCommits, parseGitmodules, parseLsTree, parseLsFiles, parseGitRemotes } from '../git';
import * as assert from 'assert';
import { splitInChunks } from '../util';
@@ -197,6 +197,77 @@ suite('git', () => {
});
});
suite('parseGitRemotes', () => {
test('empty', () => {
assert.deepStrictEqual(parseGitRemotes(''), []);
});
test('single remote', () => {
const sample = `[remote "origin"]
url = https://github.com/microsoft/vscode.git
fetch = +refs/heads/*:refs/remotes/origin/*
`;
assert.deepStrictEqual(parseGitRemotes(sample), [
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode.git', isReadOnly: false }
]);
});
test('single remote (multiple urls)', () => {
const sample = `[remote "origin"]
url = https://github.com/microsoft/vscode.git
url = https://github.com/microsoft/vscode2.git
fetch = +refs/heads/*:refs/remotes/origin/*
`;
assert.deepStrictEqual(parseGitRemotes(sample), [
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode.git', isReadOnly: false }
]);
});
test('multiple remotes', () => {
const sample = `[remote "origin"]
url = https://github.com/microsoft/vscode.git
pushurl = https://github.com/microsoft/vscode1.git
fetch = +refs/heads/*:refs/remotes/origin/*
[remote "remote2"]
url = https://github.com/microsoft/vscode2.git
fetch = +refs/heads/*:refs/remotes/origin/*
`;
assert.deepStrictEqual(parseGitRemotes(sample), [
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode1.git', isReadOnly: false },
{ name: 'remote2', fetchUrl: 'https://github.com/microsoft/vscode2.git', pushUrl: 'https://github.com/microsoft/vscode2.git', isReadOnly: false }
]);
});
test('remotes (white space)', () => {
const sample = ` [remote "origin"]
url = https://github.com/microsoft/vscode.git
pushurl=https://github.com/microsoft/vscode1.git
fetch = +refs/heads/*:refs/remotes/origin/*
[ remote"remote2"]
url = https://github.com/microsoft/vscode2.git
fetch = +refs/heads/*:refs/remotes/origin/*
`;
assert.deepStrictEqual(parseGitRemotes(sample), [
{ name: 'origin', fetchUrl: 'https://github.com/microsoft/vscode.git', pushUrl: 'https://github.com/microsoft/vscode1.git', isReadOnly: false },
{ name: 'remote2', fetchUrl: 'https://github.com/microsoft/vscode2.git', pushUrl: 'https://github.com/microsoft/vscode2.git', isReadOnly: false }
]);
});
test('remotes (invalid section)', () => {
const sample = `[remote "origin"
url = https://github.com/microsoft/vscode.git
pushurl = https://github.com/microsoft/vscode1.git
fetch = +refs/heads/*:refs/remotes/origin/*
`;
assert.deepStrictEqual(parseGitRemotes(sample), []);
});
});
suite('parseGitCommit', () => {
test('single parent commit', function () {
const GIT_OUTPUT_SINGLE_PARENT = `52c293a05038d865604c2284aa8698bd087915a1

View File

@@ -3,10 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const testRunner = require('../../../../test/integration/electron/testrunner');
import * as path from 'path';
import * as testRunner from '../../../../test/integration/electron/testrunner';
const options: any = {
const options: import('mocha').MochaOptions = {
ui: 'tdd',
color: true,
timeout: 60000

View File

@@ -44,7 +44,7 @@ suite('git smoke test', function () {
fs.writeFileSync(file('index.pug'), 'hello', 'utf8');
cp.execSync('git init -b main', { cwd });
cp.execSync('git config user.name testuser', { cwd });
cp.execSync('git config user.email monacotools@microsoft.com', { cwd });
cp.execSync('git config user.email monacotools@example.com', { cwd });
cp.execSync('git config commit.gpgsign false', { cwd });
cp.execSync('git add .', { cwd });
cp.execSync('git commit -m "initial commit"', { cwd });

View File

@@ -3,15 +3,13 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
import { CancellationToken, ConfigurationChangeEvent, Disposable, env, Event, EventEmitter, MarkdownString, ThemeIcon, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider, Uri, workspace } from 'vscode';
import { CancellationToken, ConfigurationChangeEvent, Disposable, env, Event, EventEmitter, MarkdownString, ThemeIcon, Timeline, TimelineChangeEvent, TimelineItem, TimelineOptions, TimelineProvider, Uri, workspace, l10n } from 'vscode';
import { Model } from './model';
import { Repository, Resource } from './repository';
import { debounce } from './decorators';
import { emojify, ensureEmojis } from './emoji';
import { CommandCenter } from './commands';
const localize = nls.loadMessageBundle();
import { OperationKind, OperationResult } from './operation';
export class GitTimelineItem extends TimelineItem {
static is(item: TimelineItem): item is GitTimelineItem {
@@ -54,7 +52,7 @@ export class GitTimelineItem extends TimelineItem {
this.tooltip = new MarkdownString('', true);
if (email) {
const emailTitle = localize('git.timeline.email', "Email");
const emailTitle = l10n.t('Email');
this.tooltip.appendMarkdown(`$(account) [**${author}**](mailto:${email} "${emailTitle} ${author}")\n\n`);
} else {
this.tooltip.appendMarkdown(`$(account) **${author}**\n\n`);
@@ -79,14 +77,14 @@ export class GitTimelineProvider implements TimelineProvider {
}
readonly id = 'git-history';
readonly label = localize('git.timeline.source', 'Git History');
readonly label = l10n.t('Git History');
private readonly disposable: Disposable;
private providerDisposable: Disposable | undefined;
private repo: Repository | undefined;
private repoDisposable: Disposable | undefined;
private repoStatusDate: Date | undefined;
private repoOperationDate: Date | undefined;
constructor(private readonly model: Model, private commands: CommandCenter) {
this.disposable = Disposable.from(
@@ -105,12 +103,12 @@ export class GitTimelineProvider implements TimelineProvider {
}
async provideTimeline(uri: Uri, options: TimelineOptions, _token: CancellationToken): Promise<Timeline> {
// console.log(`GitTimelineProvider.provideTimeline: uri=${uri} state=${this._model.state}`);
// console.log(`GitTimelineProvider.provideTimeline: uri=${uri}`);
const repo = this.model.getRepository(uri);
if (!repo) {
this.repoDisposable?.dispose();
this.repoStatusDate = undefined;
this.repoOperationDate = undefined;
this.repo = undefined;
return { items: [] };
@@ -120,10 +118,11 @@ export class GitTimelineProvider implements TimelineProvider {
this.repoDisposable?.dispose();
this.repo = repo;
this.repoStatusDate = new Date();
this.repoOperationDate = new Date();
this.repoDisposable = Disposable.from(
repo.onDidChangeRepository(uri => this.onRepositoryChanged(repo, uri)),
repo.onDidRunGitStatus(() => this.onRepositoryStatusChanged(repo))
repo.onDidRunGitStatus(() => this.onRepositoryStatusChanged(repo)),
repo.onDidRunOperation(result => this.onRepositoryOperationRun(repo, result))
);
}
@@ -171,7 +170,7 @@ export class GitTimelineProvider implements TimelineProvider {
const showAuthor = config.get<boolean>('showAuthor');
const showUncommitted = config.get<boolean>('showUncommitted');
const openComparison = localize('git.timeline.openComparison', "Open Comparison");
const openComparison = l10n.t('Open Comparison');
const items = commits.map<GitTimelineItem>((c, i) => {
const date = dateType === 'authored' ? c.authorDate : c.commitDate;
@@ -199,13 +198,13 @@ export class GitTimelineProvider implements TimelineProvider {
});
if (options.cursor === undefined) {
const you = localize('git.timeline.you', 'You');
const you = l10n.t('You');
const index = repo.indexGroup.resourceStates.find(r => r.resourceUri.fsPath === uri.fsPath);
if (index) {
const date = this.repoStatusDate ?? new Date();
const date = this.repoOperationDate ?? new Date();
const item = new GitTimelineItem('~', 'HEAD', localize('git.timeline.stagedChanges', 'Staged Changes'), date.getTime(), 'index', 'git:file:index');
const item = new GitTimelineItem('~', 'HEAD', l10n.t('Staged Changes'), date.getTime(), 'index', 'git:file:index');
// TODO@eamodio: Replace with a better icon -- reflecting its status maybe?
item.iconPath = new ThemeIcon('git-commit');
item.description = '';
@@ -228,7 +227,7 @@ export class GitTimelineProvider implements TimelineProvider {
if (working) {
const date = new Date();
const item = new GitTimelineItem('', index ? '~' : 'HEAD', localize('git.timeline.uncommitedChanges', 'Uncommitted Changes'), date.getTime(), 'working', 'git:file:working');
const item = new GitTimelineItem('', index ? '~' : 'HEAD', l10n.t('Uncommitted Changes'), date.getTime(), 'working', 'git:file:working');
item.iconPath = new ThemeIcon('circle-outline');
item.description = '';
item.setItemDetails(you, undefined, dateFormatter.format(date), Resource.getStatusText(working.type));
@@ -255,7 +254,7 @@ export class GitTimelineProvider implements TimelineProvider {
private ensureProviderRegistration() {
if (this.providerDisposable === undefined) {
this.providerDisposable = workspace.registerTimelineProvider(['file', 'git', 'vscode-remote', 'gitlens-git', 'vscode-local-history'], this);
this.providerDisposable = workspace.registerTimelineProvider(['file', 'git', 'vscode-remote', 'vscode-local-history'], this);
}
}
@@ -283,10 +282,25 @@ export class GitTimelineProvider implements TimelineProvider {
private onRepositoryStatusChanged(_repo: Repository) {
// console.log(`GitTimelineProvider.onRepositoryStatusChanged`);
// 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();
const config = workspace.getConfiguration('git.timeline');
const showUncommitted = config.get<boolean>('showUncommitted') === true;
this.fireChanged();
if (showUncommitted) {
this.fireChanged();
}
}
private onRepositoryOperationRun(_repo: Repository, _result: OperationResult) {
// console.log(`GitTimelineProvider.onRepositoryOperationRun`);
// Successful operations that are not read-only and not status operations
if (!_result.error && !_result.operation.readOnly && _result.operation.kind !== OperationKind.Status) {
// This is less than ideal, but for now just save the last time an
// operation was run and use that as the timestamp for staged items
this.repoOperationDate = new Date();
this.fireChanged();
}
}
@debounce(500)

View File

@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'vscode' {
// https://github.com/microsoft/vscode/issues/180582
export namespace workspace {
/**
*
* @param scheme The URI scheme that this provider can provide canonical URIs for.
* A canonical URI represents the conversion of a resource's alias into a source of truth URI.
* Multiple aliases may convert to the same source of truth URI.
* @param provider A provider which can convert URIs of scheme @param scheme to
* a canonical URI which is stable across machines.
*/
export function registerCanonicalUriProvider(scheme: string, provider: CanonicalUriProvider): Disposable;
/**
*
* @param uri The URI to provide a canonical URI for.
* @param token A cancellation token for the request.
*/
export function getCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult<Uri>;
}
export interface CanonicalUriProvider {
/**
*
* @param uri The URI to provide a canonical URI for.
* @param options Options that the provider should honor in the URI it returns.
* @param token A cancellation token for the request.
* @returns The canonical URI for the requested URI or undefined if no canonical URI can be provided.
*/
provideCanonicalUri(uri: Uri, options: CanonicalUriRequestOptions, token: CancellationToken): ProviderResult<Uri>;
}
export interface CanonicalUriRequestOptions {
/**
*
* The desired scheme of the canonical URI.
*/
targetScheme: string;
}
}

View File

@@ -0,0 +1,71 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'vscode' {
// https://github.com/microsoft/vscode/issues/157734
export namespace workspace {
/**
* An event that is emitted when an edit session identity is about to be requested.
*/
export const onWillCreateEditSessionIdentity: Event<EditSessionIdentityWillCreateEvent>;
/**
*
* @param scheme The URI scheme that this provider can provide edit session identities for.
* @param provider A provider which can convert URIs for workspace folders of scheme @param scheme to
* an edit session identifier which is stable across machines. This enables edit sessions to be resolved.
*/
export function registerEditSessionIdentityProvider(scheme: string, provider: EditSessionIdentityProvider): Disposable;
}
export interface EditSessionIdentityProvider {
/**
*
* @param workspaceFolder The workspace folder to provide an edit session identity for.
* @param token A cancellation token for the request.
* @returns A string representing the edit session identity for the requested workspace folder.
*/
provideEditSessionIdentity(workspaceFolder: WorkspaceFolder, token: CancellationToken): ProviderResult<string>;
/**
*
* @param identity1 An edit session identity.
* @param identity2 A second edit session identity to compare to @param identity1.
* @param token A cancellation token for the request.
* @returns An {@link EditSessionIdentityMatch} representing the edit session identity match confidence for the provided identities.
*/
provideEditSessionIdentityMatch(identity1: string, identity2: string, token: CancellationToken): ProviderResult<EditSessionIdentityMatch>;
}
export enum EditSessionIdentityMatch {
Complete = 100,
Partial = 50,
None = 0
}
export interface EditSessionIdentityWillCreateEvent {
/**
* A cancellation token.
*/
readonly token: CancellationToken;
/**
* The workspace folder to create an edit session identity for.
*/
readonly workspaceFolder: WorkspaceFolder;
/**
* Allows to pause the event until the provided thenable resolves.
*
* *Note:* This function can only be called during event dispatch.
*
* @param thenable A thenable that delays saving.
*/
waitUntil(thenable: Thenable<any>): void;
}
}

View File

@@ -314,6 +314,13 @@ export function pathEquals(a: string, b: string): boolean {
* casing.
*/
export function relativePath(from: string, to: string): string {
// On Windows, there are cases in which `from` is a path that contains a trailing `\` character
// (ex: C:\, \\server\folder\) due to the implementation of `path.normalize()`. This behavior is
// by design as documented in https://github.com/nodejs/node/issues/1765.
if (isWindows) {
from = from.replace(/\\$/, '');
}
if (isDescendant(from, to) && from.length < to.length) {
return to.substring(from.length + 1);
}