Files
azuredatastudio/extensions/git/src/repository.ts
Karl Burtram 01e66ab3e6 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>
2023-06-27 15:26:51 -07:00

2477 lines
87 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as path from 'path';
import * as picomatch from 'picomatch';
import { CancellationToken, Command, Disposable, Event, EventEmitter, Memento, ProgressLocation, ProgressOptions, scm, SourceControl, SourceControlInputBox, SourceControlInputBoxValidation, SourceControlInputBoxValidationType, SourceControlResourceDecorations, SourceControlResourceGroup, SourceControlResourceState, ThemeColor, Uri, window, workspace, WorkspaceEdit, FileDecoration, commands, Tab, TabInputTextDiff, TabInputNotebookDiff, RelativePattern, CancellationTokenSource, LogOutputChannel, LogLevel, CancellationError, l10n } from 'vscode';
import TelemetryReporter from '@vscode/extension-telemetry';
import { Branch, Change, ForcePushMode, GitErrorCodes, LogOptions, Ref, Remote, Status, CommitOptions, BranchQuery, FetchOptions, RefQuery, RefType } from './api/git';
import { AutoFetcher } from './autofetch';
import { debounce, memoize, throttle } from './decorators';
import { Commit, GitError, Repository as BaseRepository, Stash, Submodule, LogFileOptions, PullOptions } from './git';
import { StatusBarCommands } from './statusbar';
import { toGitUri } from './uri';
import { anyEvent, combinedDisposable, debounceEvent, dispose, EmptyDisposable, eventToPromise, filterEvent, find, IDisposable, isDescendant, onceEvent, pathEquals, relativePath } from './util';
import { IFileWatcher, watch } from './watch';
import { IPushErrorHandlerRegistry } from './pushError';
import { ApiRepository } from './api/api1';
import { IRemoteSourcePublisherRegistry } from './remotePublisher';
import { ActionButtonCommand } from './actionButton';
import { IPostCommitCommandsProviderRegistry, CommitCommandsCenter } from './postCommitCommands';
import { Operation, OperationKind, OperationManager, OperationResult } from './operation';
import { GitBranchProtectionProvider, IBranchProtectionProviderRegistry } from './branchProtection';
const timeout = (millis: number) => new Promise(c => setTimeout(c, millis));
const iconsRootPath = path.join(path.dirname(__dirname), 'resources', 'icons');
function getIconUri(iconName: string, theme: string): Uri {
return Uri.file(path.join(iconsRootPath, theme, `${iconName}.svg`));
}
export const enum RepositoryState {
Idle,
Disposed
}
export const enum ResourceGroupType {
Merge,
Index,
WorkingTree,
Untracked
}
export class Resource implements SourceControlResourceState {
static getStatusText(type: Status) {
switch (type) {
case Status.INDEX_MODIFIED: return l10n.t('Index Modified');
case Status.MODIFIED: return l10n.t('Modified');
case Status.INDEX_ADDED: return l10n.t('Index Added');
case Status.INDEX_DELETED: return l10n.t('Index Deleted');
case Status.DELETED: return l10n.t('Deleted');
case Status.INDEX_RENAMED: return l10n.t('Index Renamed');
case Status.INDEX_COPIED: return l10n.t('Index Copied');
case Status.UNTRACKED: return l10n.t('Untracked');
case Status.IGNORED: return l10n.t('Ignored');
case Status.INTENT_TO_ADD: return l10n.t('Intent to Add');
case Status.INTENT_TO_RENAME: return l10n.t('Intent to Rename');
case Status.BOTH_DELETED: return l10n.t('Conflict: Both Deleted');
case Status.ADDED_BY_US: return l10n.t('Conflict: Added By Us');
case Status.DELETED_BY_THEM: return l10n.t('Conflict: Deleted By Them');
case Status.ADDED_BY_THEM: return l10n.t('Conflict: Added By Them');
case Status.DELETED_BY_US: return l10n.t('Conflict: Deleted By Us');
case Status.BOTH_ADDED: return l10n.t('Conflict: Both Added');
case Status.BOTH_MODIFIED: return l10n.t('Conflict: Both Modified');
default: return '';
}
}
@memoize
get resourceUri(): Uri {
if (this.renameResourceUri && (this._type === Status.MODIFIED || this._type === Status.DELETED || this._type === Status.INDEX_RENAMED || this._type === Status.INDEX_COPIED || this._type === Status.INTENT_TO_RENAME)) {
return this.renameResourceUri;
}
return this._resourceUri;
}
get leftUri(): Uri | undefined {
return this.resources[0];
}
get rightUri(): Uri | undefined {
return this.resources[1];
}
@memoize
get command(): Command {
return this._commandResolver.resolveDefaultCommand(this);
}
@memoize
private get resources(): [Uri | undefined, Uri | undefined] {
return this._commandResolver.getResources(this);
}
get resourceGroupType(): ResourceGroupType { return this._resourceGroupType; }
get type(): Status { return this._type; }
get original(): Uri { return this._resourceUri; }
get renameResourceUri(): Uri | undefined { return this._renameResourceUri; }
private static Icons: any = {
light: {
Modified: getIconUri('status-modified', 'light'),
Added: getIconUri('status-added', 'light'),
Deleted: getIconUri('status-deleted', 'light'),
Renamed: getIconUri('status-renamed', 'light'),
Copied: getIconUri('status-copied', 'light'),
Untracked: getIconUri('status-untracked', 'light'),
Ignored: getIconUri('status-ignored', 'light'),
Conflict: getIconUri('status-conflict', 'light'),
},
dark: {
Modified: getIconUri('status-modified', 'dark'),
Added: getIconUri('status-added', 'dark'),
Deleted: getIconUri('status-deleted', 'dark'),
Renamed: getIconUri('status-renamed', 'dark'),
Copied: getIconUri('status-copied', 'dark'),
Untracked: getIconUri('status-untracked', 'dark'),
Ignored: getIconUri('status-ignored', 'dark'),
Conflict: getIconUri('status-conflict', 'dark')
}
};
private getIconPath(theme: string): Uri {
switch (this.type) {
case Status.INDEX_MODIFIED: return Resource.Icons[theme].Modified;
case Status.MODIFIED: return Resource.Icons[theme].Modified;
case Status.INDEX_ADDED: return Resource.Icons[theme].Added;
case Status.INDEX_DELETED: return Resource.Icons[theme].Deleted;
case Status.DELETED: return Resource.Icons[theme].Deleted;
case Status.INDEX_RENAMED: return Resource.Icons[theme].Renamed;
case Status.INDEX_COPIED: return Resource.Icons[theme].Copied;
case Status.UNTRACKED: return Resource.Icons[theme].Untracked;
case Status.IGNORED: return Resource.Icons[theme].Ignored;
case Status.INTENT_TO_ADD: return Resource.Icons[theme].Added;
case Status.INTENT_TO_RENAME: return Resource.Icons[theme].Renamed;
case Status.BOTH_DELETED: return Resource.Icons[theme].Conflict;
case Status.ADDED_BY_US: return Resource.Icons[theme].Conflict;
case Status.DELETED_BY_THEM: return Resource.Icons[theme].Conflict;
case Status.ADDED_BY_THEM: return Resource.Icons[theme].Conflict;
case Status.DELETED_BY_US: return Resource.Icons[theme].Conflict;
case Status.BOTH_ADDED: return Resource.Icons[theme].Conflict;
case Status.BOTH_MODIFIED: return Resource.Icons[theme].Conflict;
default: throw new Error('Unknown git status: ' + this.type);
}
}
private get tooltip(): string {
return Resource.getStatusText(this.type);
}
private get strikeThrough(): boolean {
switch (this.type) {
case Status.DELETED:
case Status.BOTH_DELETED:
case Status.DELETED_BY_THEM:
case Status.DELETED_BY_US:
case Status.INDEX_DELETED:
return true;
default:
return false;
}
}
@memoize
private get faded(): boolean {
// TODO@joao
return false;
// const workspaceRootPath = this.workspaceRoot.fsPath;
// return this.resourceUri.fsPath.substr(0, workspaceRootPath.length) !== workspaceRootPath;
}
get decorations(): SourceControlResourceDecorations {
const light = this._useIcons ? { iconPath: this.getIconPath('light') } : undefined;
const dark = this._useIcons ? { iconPath: this.getIconPath('dark') } : undefined;
const tooltip = this.tooltip;
const strikeThrough = this.strikeThrough;
const faded = this.faded;
return { strikeThrough, faded, tooltip, light, dark };
}
get letter(): string {
switch (this.type) {
case Status.INDEX_MODIFIED:
case Status.MODIFIED:
return 'M';
case Status.INDEX_ADDED:
case Status.INTENT_TO_ADD:
return 'A';
case Status.INDEX_DELETED:
case Status.DELETED:
return 'D';
case Status.INDEX_RENAMED:
case Status.INTENT_TO_RENAME:
return 'R';
case Status.UNTRACKED:
return 'U';
case Status.IGNORED:
return 'I';
case Status.DELETED_BY_THEM:
return 'D';
case Status.DELETED_BY_US:
return 'D';
case Status.INDEX_COPIED:
return 'C';
case Status.BOTH_DELETED:
case Status.ADDED_BY_US:
case Status.ADDED_BY_THEM:
case Status.BOTH_ADDED:
case Status.BOTH_MODIFIED:
return '!'; // Using ! instead of ⚠, because the latter looks really bad on windows
default:
throw new Error('Unknown git status: ' + this.type);
}
}
get color(): ThemeColor {
switch (this.type) {
case Status.INDEX_MODIFIED:
return new ThemeColor('gitDecoration.stageModifiedResourceForeground');
case Status.MODIFIED:
return new ThemeColor('gitDecoration.modifiedResourceForeground');
case Status.INDEX_DELETED:
return new ThemeColor('gitDecoration.stageDeletedResourceForeground');
case Status.DELETED:
return new ThemeColor('gitDecoration.deletedResourceForeground');
case Status.INDEX_ADDED:
case Status.INTENT_TO_ADD:
return new ThemeColor('gitDecoration.addedResourceForeground');
case Status.INDEX_COPIED:
case Status.INDEX_RENAMED:
case Status.INTENT_TO_RENAME:
return new ThemeColor('gitDecoration.renamedResourceForeground');
case Status.UNTRACKED:
return new ThemeColor('gitDecoration.untrackedResourceForeground');
case Status.IGNORED:
return new ThemeColor('gitDecoration.ignoredResourceForeground');
case Status.BOTH_DELETED:
case Status.ADDED_BY_US:
case Status.DELETED_BY_THEM:
case Status.ADDED_BY_THEM:
case Status.DELETED_BY_US:
case Status.BOTH_ADDED:
case Status.BOTH_MODIFIED:
return new ThemeColor('gitDecoration.conflictingResourceForeground');
default:
throw new Error('Unknown git status: ' + this.type);
}
}
get priority(): number {
switch (this.type) {
case Status.INDEX_MODIFIED:
case Status.MODIFIED:
case Status.INDEX_COPIED:
return 2;
case Status.IGNORED:
return 3;
case Status.BOTH_DELETED:
case Status.ADDED_BY_US:
case Status.DELETED_BY_THEM:
case Status.ADDED_BY_THEM:
case Status.DELETED_BY_US:
case Status.BOTH_ADDED:
case Status.BOTH_MODIFIED:
return 4;
default:
return 1;
}
}
get resourceDecoration(): FileDecoration {
const res = new FileDecoration(this.letter, this.tooltip, this.color);
res.propagate = this.type !== Status.DELETED && this.type !== Status.INDEX_DELETED;
return res;
}
constructor(
private _commandResolver: ResourceCommandResolver,
private _resourceGroupType: ResourceGroupType,
private _resourceUri: Uri,
private _type: Status,
private _useIcons: boolean,
private _renameResourceUri?: Uri,
) { }
async open(): Promise<void> {
const command = this.command;
await commands.executeCommand<void>(command.command, ...(command.arguments || []));
}
async openFile(): Promise<void> {
const command = this._commandResolver.resolveFileCommand(this);
await commands.executeCommand<void>(command.command, ...(command.arguments || []));
}
async openChange(): Promise<void> {
const command = this._commandResolver.resolveChangeCommand(this);
await commands.executeCommand<void>(command.command, ...(command.arguments || []));
}
clone(resourceGroupType?: ResourceGroupType) {
return new Resource(this._commandResolver, resourceGroupType ?? this._resourceGroupType, this._resourceUri, this._type, this._useIcons, this._renameResourceUri);
}
}
export interface GitResourceGroup extends SourceControlResourceGroup {
resourceStates: Resource[];
}
interface GitResourceGroups {
indexGroup?: Resource[];
mergeGroup?: Resource[];
untrackedGroup?: Resource[];
workingTreeGroup?: Resource[];
}
class ProgressManager {
private enabled = false;
private disposable: IDisposable = EmptyDisposable;
constructor(private repository: Repository) {
const onDidChange = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git', Uri.file(this.repository.root)));
onDidChange(_ => this.updateEnablement());
this.updateEnablement();
this.repository.onDidChangeOperations(() => {
// Disable input box when the commit operation is running
this.repository.sourceControl.inputBox.enabled = !this.repository.operations.isRunning(OperationKind.Commit);
});
}
private updateEnablement(): void {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
if (config.get<boolean>('showProgress')) {
this.enable();
} else {
this.disable();
}
}
private enable(): void {
if (this.enabled) {
return;
}
const start = onceEvent(filterEvent(this.repository.onDidChangeOperations, () => this.repository.operations.shouldShowProgress()));
const end = onceEvent(filterEvent(debounceEvent(this.repository.onDidChangeOperations, 300), () => !this.repository.operations.shouldShowProgress()));
const setup = () => {
this.disposable = start(() => {
const promise = eventToPromise(end).then(() => setup());
window.withProgress({ location: ProgressLocation.SourceControl }, () => promise);
});
};
setup();
this.enabled = true;
}
private disable(): void {
if (!this.enabled) {
return;
}
this.disposable.dispose();
this.disposable = EmptyDisposable;
this.enabled = false;
}
dispose(): void {
this.disable();
}
}
class FileEventLogger {
private eventDisposable: IDisposable = EmptyDisposable;
private logLevelDisposable: IDisposable = EmptyDisposable;
constructor(
private onWorkspaceWorkingTreeFileChange: Event<Uri>,
private onDotGitFileChange: Event<Uri>,
private logger: LogOutputChannel
) {
this.logLevelDisposable = logger.onDidChangeLogLevel(this.onDidChangeLogLevel, this);
this.onDidChangeLogLevel(logger.logLevel);
}
private onDidChangeLogLevel(logLevel: LogLevel): void {
this.eventDisposable.dispose();
if (logLevel > LogLevel.Debug) {
return;
}
this.eventDisposable = combinedDisposable([
this.onWorkspaceWorkingTreeFileChange(uri => this.logger.debug(`[wt] Change: ${uri.fsPath}`)),
this.onDotGitFileChange(uri => this.logger.debug(`[.git] Change: ${uri.fsPath}`))
]);
}
dispose(): void {
this.eventDisposable.dispose();
this.logLevelDisposable.dispose();
}
}
class DotGitWatcher implements IFileWatcher {
readonly event: Event<Uri>;
private emitter = new EventEmitter<Uri>();
private transientDisposables: IDisposable[] = [];
private disposables: IDisposable[] = [];
constructor(
private repository: Repository,
private logger: LogOutputChannel
) {
const rootWatcher = watch(repository.dotGit.path);
this.disposables.push(rootWatcher);
// Ignore changes to the "index.lock" file, and watchman fsmonitor hook (https://git-scm.com/docs/githooks#_fsmonitor_watchman) cookie files.
// Watchman creates a cookie file inside the git directory whenever a query is run (https://facebook.github.io/watchman/docs/cookies.html).
const filteredRootWatcher = filterEvent(rootWatcher.event, uri => uri.scheme === 'file' && !/\/\.git(\/index\.lock)?$|\/\.watchman-cookie-/.test(uri.path));
this.event = anyEvent(filteredRootWatcher, this.emitter.event);
repository.onDidRunGitStatus(this.updateTransientWatchers, this, this.disposables);
this.updateTransientWatchers();
}
private updateTransientWatchers() {
this.transientDisposables = dispose(this.transientDisposables);
if (!this.repository.HEAD || !this.repository.HEAD.upstream) {
return;
}
this.transientDisposables = dispose(this.transientDisposables);
const { name, remote } = this.repository.HEAD.upstream;
const upstreamPath = path.join(this.repository.dotGit.commonPath ?? this.repository.dotGit.path, 'refs', 'remotes', remote, name);
try {
const upstreamWatcher = watch(upstreamPath);
this.transientDisposables.push(upstreamWatcher);
upstreamWatcher.event(this.emitter.fire, this.emitter, this.transientDisposables);
} catch (err) {
this.logger.warn(`Failed to watch ref '${upstreamPath}', is most likely packed.`);
}
}
dispose() {
this.emitter.dispose();
this.transientDisposables = dispose(this.transientDisposables);
this.disposables = dispose(this.disposables);
}
}
class ResourceCommandResolver {
constructor(private repository: Repository) { }
resolveDefaultCommand(resource: Resource): Command {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const openDiffOnClick = config.get<boolean>('openDiffOnClick', true);
return openDiffOnClick ? this.resolveChangeCommand(resource) : this.resolveFileCommand(resource);
}
resolveFileCommand(resource: Resource): Command {
return {
command: 'vscode.open',
title: l10n.t('Open'),
arguments: [resource.resourceUri]
};
}
resolveChangeCommand(resource: Resource): Command {
const title = this.getTitle(resource);
if (!resource.leftUri) {
const bothModified = resource.type === Status.BOTH_MODIFIED;
if (resource.rightUri && workspace.getConfiguration('git').get<boolean>('mergeEditor', false) && (bothModified || resource.type === Status.BOTH_ADDED)) {
return {
command: 'git.openMergeEditor',
title: l10n.t('Open Merge'),
arguments: [resource.rightUri]
};
} else {
return {
command: 'vscode.open',
title: l10n.t('Open'),
arguments: [resource.rightUri, { override: bothModified ? false : undefined }, title]
};
}
} else {
return {
command: 'vscode.diff',
title: l10n.t('Open'),
arguments: [resource.leftUri, resource.rightUri, title]
};
}
}
getResources(resource: Resource): [Uri | undefined, Uri | undefined] {
for (const submodule of this.repository.submodules) {
if (path.join(this.repository.root, submodule.path) === resource.resourceUri.fsPath) {
return [undefined, toGitUri(resource.resourceUri, resource.resourceGroupType === ResourceGroupType.Index ? 'index' : 'wt', { submoduleOf: this.repository.root })];
}
}
return [this.getLeftResource(resource), this.getRightResource(resource)];
}
private getLeftResource(resource: Resource): Uri | undefined {
switch (resource.type) {
case Status.INDEX_MODIFIED:
case Status.INDEX_RENAMED:
case Status.INDEX_ADDED:
case Status.INTENT_TO_RENAME:
return toGitUri(resource.original, 'HEAD');
case Status.MODIFIED:
case Status.UNTRACKED:
return toGitUri(resource.resourceUri, '~');
case Status.DELETED_BY_US:
case Status.DELETED_BY_THEM:
return toGitUri(resource.resourceUri, '~1');
}
return undefined;
}
private getRightResource(resource: Resource): Uri | undefined {
switch (resource.type) {
case Status.INDEX_MODIFIED:
case Status.INDEX_ADDED:
case Status.INDEX_COPIED:
case Status.INDEX_RENAMED:
return toGitUri(resource.resourceUri, '');
case Status.INDEX_DELETED:
case Status.DELETED:
return toGitUri(resource.resourceUri, 'HEAD');
case Status.DELETED_BY_US:
return toGitUri(resource.resourceUri, '~3');
case Status.DELETED_BY_THEM:
return toGitUri(resource.resourceUri, '~2');
case Status.MODIFIED:
case Status.UNTRACKED:
case Status.IGNORED:
case Status.INTENT_TO_ADD:
case Status.INTENT_TO_RENAME: {
const uriString = resource.resourceUri.toString();
const [indexStatus] = this.repository.indexGroup.resourceStates.filter(r => r.resourceUri.toString() === uriString);
if (indexStatus && indexStatus.renameResourceUri) {
return indexStatus.renameResourceUri;
}
return resource.resourceUri;
}
case Status.BOTH_ADDED:
case Status.BOTH_MODIFIED:
return resource.resourceUri;
}
return undefined;
}
private getTitle(resource: Resource): string {
const basename = path.basename(resource.resourceUri.fsPath);
switch (resource.type) {
case Status.INDEX_MODIFIED:
case Status.INDEX_RENAMED:
case Status.INDEX_ADDED:
return l10n.t('{0} (Index)', basename);
case Status.MODIFIED:
case Status.BOTH_ADDED:
case Status.BOTH_MODIFIED:
return l10n.t('{0} (Working Tree)', basename);
case Status.INDEX_DELETED:
case Status.DELETED:
return l10n.t('{0} (Deleted)', basename);
case Status.DELETED_BY_US:
return l10n.t('{0} (Theirs)', basename);
case Status.DELETED_BY_THEM:
return l10n.t('{0} (Ours)', basename);
case Status.UNTRACKED:
return l10n.t('{0} (Untracked)', basename);
case Status.INTENT_TO_ADD:
case Status.INTENT_TO_RENAME:
return l10n.t('{0} (Intent to add)', basename);
default:
return '';
}
}
}
interface BranchProtectionMatcher {
include?: picomatch.Matcher;
exclude?: picomatch.Matcher;
}
export class Repository implements Disposable {
private _onDidChangeRepository = new EventEmitter<Uri>();
readonly onDidChangeRepository: Event<Uri> = this._onDidChangeRepository.event;
private _onDidChangeState = new EventEmitter<RepositoryState>();
readonly onDidChangeState: Event<RepositoryState> = this._onDidChangeState.event;
private _onDidChangeStatus = new EventEmitter<void>();
readonly onDidRunGitStatus: Event<void> = this._onDidChangeStatus.event;
private _onDidChangeOriginalResource = new EventEmitter<Uri>();
readonly onDidChangeOriginalResource: Event<Uri> = this._onDidChangeOriginalResource.event;
private _onRunOperation = new EventEmitter<OperationKind>();
readonly onRunOperation: Event<OperationKind> = this._onRunOperation.event;
private _onDidRunOperation = new EventEmitter<OperationResult>();
readonly onDidRunOperation: Event<OperationResult> = this._onDidRunOperation.event;
private _onDidChangeBranchProtection = new EventEmitter<void>();
readonly onDidChangeBranchProtection: Event<void> = this._onDidChangeBranchProtection.event;
@memoize
get onDidChangeOperations(): Event<void> {
return anyEvent(this.onRunOperation as Event<any>, this.onDidRunOperation as Event<any>);
}
private _sourceControl: SourceControl;
get sourceControl(): SourceControl { return this._sourceControl; }
get inputBox(): SourceControlInputBox { return this._sourceControl.inputBox; }
private _mergeGroup: SourceControlResourceGroup;
get mergeGroup(): GitResourceGroup { return this._mergeGroup as GitResourceGroup; }
private _indexGroup: SourceControlResourceGroup;
get indexGroup(): GitResourceGroup { return this._indexGroup as GitResourceGroup; }
private _workingTreeGroup: SourceControlResourceGroup;
get workingTreeGroup(): GitResourceGroup { return this._workingTreeGroup as GitResourceGroup; }
private _untrackedGroup: SourceControlResourceGroup;
get untrackedGroup(): GitResourceGroup { return this._untrackedGroup as GitResourceGroup; }
private _HEAD: Branch | undefined;
get HEAD(): Branch | undefined {
return this._HEAD;
}
get headShortName(): string | undefined {
if (!this.HEAD) {
return;
}
const HEAD = this.HEAD;
if (HEAD.name) {
return HEAD.name;
}
return (HEAD.commit || '').substr(0, 8);
}
private _remotes: Remote[] = [];
get remotes(): Remote[] {
return this._remotes;
}
private _submodules: Submodule[] = [];
get submodules(): Submodule[] {
return this._submodules;
}
private _rebaseCommit: Commit | undefined = undefined;
set rebaseCommit(rebaseCommit: Commit | undefined) {
if (this._rebaseCommit && !rebaseCommit) {
this.inputBox.value = '';
} else if (rebaseCommit && (!this._rebaseCommit || this._rebaseCommit.hash !== rebaseCommit.hash)) {
this.inputBox.value = rebaseCommit.message;
}
const shouldUpdateContext = !!this._rebaseCommit !== !!rebaseCommit;
this._rebaseCommit = rebaseCommit;
if (shouldUpdateContext) {
commands.executeCommand('setContext', 'gitRebaseInProgress', !!this._rebaseCommit);
}
}
get rebaseCommit(): Commit | undefined {
return this._rebaseCommit;
}
private _mergeInProgress: boolean = false;
set mergeInProgress(value: boolean) {
if (this._mergeInProgress === value) {
return;
}
this._mergeInProgress = value;
commands.executeCommand('setContext', 'gitMergeInProgress', value);
}
get mergeInProgress() {
return this._mergeInProgress;
}
private _operations = new OperationManager(this.logger);
get operations(): OperationManager { return this._operations; }
private _state = RepositoryState.Idle;
get state(): RepositoryState { return this._state; }
set state(state: RepositoryState) {
this._state = state;
this._onDidChangeState.fire(state);
this._HEAD = undefined;
this._remotes = [];
this.mergeGroup.resourceStates = [];
this.indexGroup.resourceStates = [];
this.workingTreeGroup.resourceStates = [];
this.untrackedGroup.resourceStates = [];
this._sourceControl.count = 0;
}
get root(): string {
return this.repository.root;
}
get dotGit(): { path: string; commonPath?: string } {
return this.repository.dotGit;
}
private isRepositoryHuge: false | { limit: number } = false;
private didWarnAboutLimit = false;
private branchProtection = new Map<string, BranchProtectionMatcher[]>();
private commitCommandCenter: CommitCommandsCenter;
private resourceCommandResolver = new ResourceCommandResolver(this);
private updateModelStateCancellationTokenSource: CancellationTokenSource | undefined;
private disposables: Disposable[] = [];
constructor(
private readonly repository: BaseRepository,
private pushErrorHandlerRegistry: IPushErrorHandlerRegistry,
remoteSourcePublisherRegistry: IRemoteSourcePublisherRegistry,
postCommitCommandsProviderRegistry: IPostCommitCommandsProviderRegistry,
private readonly branchProtectionProviderRegistry: IBranchProtectionProviderRegistry,
globalState: Memento,
private readonly logger: LogOutputChannel,
private telemetryReporter: TelemetryReporter
) {
const repositoryWatcher = workspace.createFileSystemWatcher(new RelativePattern(Uri.file(repository.root), '**'));
this.disposables.push(repositoryWatcher);
const onRepositoryFileChange = anyEvent(repositoryWatcher.onDidChange, repositoryWatcher.onDidCreate, repositoryWatcher.onDidDelete);
const onRepositoryWorkingTreeFileChange = filterEvent(onRepositoryFileChange, uri => !/\.git($|\\|\/)/.test(relativePath(repository.root, uri.fsPath)));
let onRepositoryDotGitFileChange: Event<Uri>;
try {
const dotGitFileWatcher = new DotGitWatcher(this, logger);
onRepositoryDotGitFileChange = dotGitFileWatcher.event;
this.disposables.push(dotGitFileWatcher);
} catch (err) {
logger.error(`Failed to watch path:'${this.dotGit.path}' or commonPath:'${this.dotGit.commonPath}', reverting to legacy API file watched. Some events might be lost.\n${err.stack || err}`);
onRepositoryDotGitFileChange = filterEvent(onRepositoryFileChange, uri => /\.git($|\\|\/)/.test(uri.path));
}
// FS changes should trigger `git status`:
// - any change inside the repository working tree
// - any change whithin the first level of the `.git` folder, except the folder itself and `index.lock`
const onFileChange = anyEvent(onRepositoryWorkingTreeFileChange, onRepositoryDotGitFileChange);
onFileChange(this.onFileChange, this, this.disposables);
// Relevate repository changes should trigger virtual document change events
onRepositoryDotGitFileChange(this._onDidChangeRepository.fire, this._onDidChangeRepository, this.disposables);
this.disposables.push(new FileEventLogger(onRepositoryWorkingTreeFileChange, onRepositoryDotGitFileChange, logger));
const root = Uri.file(repository.root);
this._sourceControl = scm.createSourceControl('git', 'Git', root);
this._sourceControl.acceptInputCommand = { command: 'git.commit', title: l10n.t('Commit'), arguments: [this._sourceControl] };
this._sourceControl.quickDiffProvider = this;
this._sourceControl.inputBox.validateInput = this.validateInput.bind(this);
this.disposables.push(this._sourceControl);
this.updateInputBoxPlaceholder();
this.disposables.push(this.onDidRunGitStatus(() => this.updateInputBoxPlaceholder()));
this._mergeGroup = this._sourceControl.createResourceGroup('merge', l10n.t('Merge Changes'));
this._indexGroup = this._sourceControl.createResourceGroup('index', l10n.t('Staged Changes'));
this._workingTreeGroup = this._sourceControl.createResourceGroup('workingTree', l10n.t('Changes'));
this._untrackedGroup = this._sourceControl.createResourceGroup('untracked', l10n.t('Untracked Changes'));
const updateIndexGroupVisibility = () => {
const config = workspace.getConfiguration('git', root);
this.indexGroup.hideWhenEmpty = !config.get<boolean>('alwaysShowStagedChangesResourceGroup');
};
const onConfigListener = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.alwaysShowStagedChangesResourceGroup', root));
onConfigListener(updateIndexGroupVisibility, this, this.disposables);
updateIndexGroupVisibility();
workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('git.mergeEditor')) {
this.mergeGroup.resourceStates = this.mergeGroup.resourceStates.map(r => r.clone());
}
}, undefined, this.disposables);
filterEvent(workspace.onDidChangeConfiguration, e =>
e.affectsConfiguration('git.branchSortOrder', root)
|| e.affectsConfiguration('git.untrackedChanges', root)
|| e.affectsConfiguration('git.ignoreSubmodules', root)
|| e.affectsConfiguration('git.openDiffOnClick', root)
|| e.affectsConfiguration('git.showActionButton', root)
|| e.affectsConfiguration('git.similarityThreshold', root)
)(() => this.updateModelState(), this, this.disposables);
const updateInputBoxVisibility = () => {
const config = workspace.getConfiguration('git', root);
this._sourceControl.inputBox.visible = config.get<boolean>('showCommitInput', true);
};
const onConfigListenerForInputBoxVisibility = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.showCommitInput', root));
onConfigListenerForInputBoxVisibility(updateInputBoxVisibility, this, this.disposables);
updateInputBoxVisibility();
this.mergeGroup.hideWhenEmpty = true;
this.untrackedGroup.hideWhenEmpty = true;
this.disposables.push(this.mergeGroup);
this.disposables.push(this.indexGroup);
this.disposables.push(this.workingTreeGroup);
this.disposables.push(this.untrackedGroup);
// Don't allow auto-fetch in untrusted workspaces
if (workspace.isTrusted) {
this.disposables.push(new AutoFetcher(this, globalState));
} else {
const trustDisposable = workspace.onDidGrantWorkspaceTrust(() => {
trustDisposable.dispose();
this.disposables.push(new AutoFetcher(this, globalState));
});
this.disposables.push(trustDisposable);
}
// https://github.com/microsoft/vscode/issues/39039
const onSuccessfulPush = filterEvent(this.onDidRunOperation, e => e.operation.kind === OperationKind.Push && !e.error);
onSuccessfulPush(() => {
const gitConfig = workspace.getConfiguration('git');
if (gitConfig.get<boolean>('showPushSuccessNotification')) {
window.showInformationMessage(l10n.t('Successfully pushed.'));
}
}, null, this.disposables);
// Default branch protection provider
const onBranchProtectionProviderChanged = filterEvent(this.branchProtectionProviderRegistry.onDidChangeBranchProtectionProviders, e => pathEquals(e.fsPath, root.fsPath));
this.disposables.push(onBranchProtectionProviderChanged(root => this.updateBranchProtectionMatchers(root)));
this.disposables.push(this.branchProtectionProviderRegistry.registerBranchProtectionProvider(root, new GitBranchProtectionProvider(root)));
const statusBar = new StatusBarCommands(this, remoteSourcePublisherRegistry);
this.disposables.push(statusBar);
statusBar.onDidChange(() => this._sourceControl.statusBarCommands = statusBar.commands, null, this.disposables);
this._sourceControl.statusBarCommands = statusBar.commands;
this.commitCommandCenter = new CommitCommandsCenter(globalState, this, postCommitCommandsProviderRegistry);
this.disposables.push(this.commitCommandCenter);
const actionButton = new ActionButtonCommand(this, this.commitCommandCenter);
this.disposables.push(actionButton);
actionButton.onDidChange(() => this._sourceControl.actionButton = actionButton.button);
this._sourceControl.actionButton = actionButton.button;
const progressManager = new ProgressManager(this);
this.disposables.push(progressManager);
const onDidChangeCountBadge = filterEvent(workspace.onDidChangeConfiguration, e => e.affectsConfiguration('git.countBadge', root));
onDidChangeCountBadge(this.setCountBadge, this, this.disposables);
this.setCountBadge();
}
validateInput(text: string, position: number): SourceControlInputBoxValidation | undefined {
let tooManyChangesWarning: SourceControlInputBoxValidation | undefined;
if (this.isRepositoryHuge) {
tooManyChangesWarning = {
message: l10n.t('Too many changes were detected. Only the first {0} changes will be shown below.', this.isRepositoryHuge.limit),
type: SourceControlInputBoxValidationType.Warning
};
}
if (this.rebaseCommit) {
if (this.rebaseCommit.message !== text) {
return {
message: l10n.t('It\'s not possible to change the commit message in the middle of a rebase. Please complete the rebase operation and use interactive rebase instead.'),
type: SourceControlInputBoxValidationType.Warning
};
}
}
const config = workspace.getConfiguration('git');
const setting = config.get<'always' | 'warn' | 'off'>('inputValidation');
if (setting === 'off') {
return tooManyChangesWarning;
}
if (/^\s+$/.test(text)) {
return {
message: l10n.t('Current commit message only contains whitespace characters'),
type: SourceControlInputBoxValidationType.Warning
};
}
let lineNumber = 0;
let start = 0;
let match: RegExpExecArray | null;
const regex = /\r?\n/g;
while ((match = regex.exec(text)) && position > match.index) {
start = match.index + match[0].length;
lineNumber++;
}
const end = match ? match.index : text.length;
const line = text.substring(start, end);
let threshold = config.get<number>('inputValidationLength', 50);
if (lineNumber === 0) {
const inputValidationSubjectLength = config.get<number | null>('inputValidationSubjectLength', null);
if (inputValidationSubjectLength !== null) {
threshold = inputValidationSubjectLength;
}
}
if (line.length <= threshold) {
if (setting !== 'always') {
return tooManyChangesWarning;
}
return {
message: l10n.t('{0} characters left in current line', threshold - line.length),
type: SourceControlInputBoxValidationType.Information
};
} else {
return {
message: l10n.t('{0} characters over {1} in current line', line.length - threshold, threshold),
type: SourceControlInputBoxValidationType.Warning
};
}
}
/**
* Quick diff label
*/
get label(): string {
return l10n.t('Git local working changes');
}
provideOriginalResource(uri: Uri): Uri | undefined {
if (uri.scheme !== 'file') {
return;
}
// Ignore path that is inside a merge group
if (this.mergeGroup.resourceStates.some(r => r.resourceUri.path === uri.path)) {
return undefined;
}
// Ignore path that is inside a submodule
if (this.submodules.some(s => isDescendant(path.join(this.repository.root, s.path), uri.path))) {
return undefined;
}
return toGitUri(uri, '', { replaceFileExtension: true });
}
async getInputTemplate(): Promise<string> {
const commitMessage = (await Promise.all([this.repository.getMergeMessage(), this.repository.getSquashMessage()])).find(msg => !!msg);
if (commitMessage) {
return commitMessage;
}
return await this.repository.getCommitTemplate();
}
getConfigs(): Promise<{ key: string; value: string }[]> {
return this.run(Operation.Config, () => this.repository.getConfigs('local'));
}
getConfig(key: string): Promise<string> {
return this.run(Operation.Config, () => this.repository.config('local', key));
}
getGlobalConfig(key: string): Promise<string> {
return this.run(Operation.Config, () => this.repository.config('global', key));
}
setConfig(key: string, value: string): Promise<string> {
return this.run(Operation.Config, () => this.repository.config('local', key, value));
}
log(options?: LogOptions): Promise<Commit[]> {
return this.run(Operation.Log, () => this.repository.log(options));
}
logFile(uri: Uri, options?: LogFileOptions): Promise<Commit[]> {
// TODO: This probably needs per-uri granularity
return this.run(Operation.LogFile, () => this.repository.logFile(uri, options));
}
@throttle
async status(): Promise<void> {
await this.run(Operation.Status);
}
diff(cached?: boolean): Promise<string> {
return this.run(Operation.Diff, () => this.repository.diff(cached));
}
diffWithHEAD(): Promise<Change[]>;
diffWithHEAD(path: string): Promise<string>;
diffWithHEAD(path?: string | undefined): Promise<string | Change[]>;
diffWithHEAD(path?: string | undefined): Promise<string | Change[]> {
return this.run(Operation.Diff, () => this.repository.diffWithHEAD(path));
}
diffWith(ref: string): Promise<Change[]>;
diffWith(ref: string, path: string): Promise<string>;
diffWith(ref: string, path?: string | undefined): Promise<string | Change[]>;
diffWith(ref: string, path?: string): Promise<string | Change[]> {
return this.run(Operation.Diff, () => this.repository.diffWith(ref, path));
}
diffIndexWithHEAD(): Promise<Change[]>;
diffIndexWithHEAD(path: string): Promise<string>;
diffIndexWithHEAD(path?: string | undefined): Promise<string | Change[]>;
diffIndexWithHEAD(path?: string): Promise<string | Change[]> {
return this.run(Operation.Diff, () => this.repository.diffIndexWithHEAD(path));
}
diffIndexWith(ref: string): Promise<Change[]>;
diffIndexWith(ref: string, path: string): Promise<string>;
diffIndexWith(ref: string, path?: string | undefined): Promise<string | Change[]>;
diffIndexWith(ref: string, path?: string): Promise<string | Change[]> {
return this.run(Operation.Diff, () => this.repository.diffIndexWith(ref, path));
}
diffBlobs(object1: string, object2: string): Promise<string> {
return this.run(Operation.Diff, () => this.repository.diffBlobs(object1, object2));
}
diffBetween(ref1: string, ref2: string): Promise<Change[]>;
diffBetween(ref1: string, ref2: string, path: string): Promise<string>;
diffBetween(ref1: string, ref2: string, path?: string | undefined): Promise<string | Change[]>;
diffBetween(ref1: string, ref2: string, path?: string): Promise<string | Change[]> {
return this.run(Operation.Diff, () => this.repository.diffBetween(ref1, ref2, path));
}
getMergeBase(ref1: string, ref2: string): Promise<string> {
return this.run(Operation.MergeBase, () => this.repository.getMergeBase(ref1, ref2));
}
async hashObject(data: string): Promise<string> {
return this.run(Operation.HashObject, () => this.repository.hashObject(data));
}
async add(resources: Uri[], opts?: { update?: boolean }): Promise<void> {
await this.run(
Operation.Add(!this.optimisticUpdateEnabled()),
async () => {
await this.repository.add(resources.map(r => r.fsPath), opts);
this.closeDiffEditors([], [...resources.map(r => r.fsPath)]);
},
() => {
const resourcePaths = resources.map(r => r.fsPath);
const indexGroupResourcePaths = this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath);
// Collect added resources
const addedResourceStates: Resource[] = [];
for (const resource of [...this.mergeGroup.resourceStates, ...this.untrackedGroup.resourceStates, ...this.workingTreeGroup.resourceStates]) {
if (resourcePaths.includes(resource.resourceUri.fsPath) && !indexGroupResourcePaths.includes(resource.resourceUri.fsPath)) {
addedResourceStates.push(resource.clone(ResourceGroupType.Index));
}
}
// Add new resource(s) to index group
const indexGroup = [...this.indexGroup.resourceStates, ...addedResourceStates];
// Remove resource(s) from merge group
const mergeGroup = this.mergeGroup.resourceStates
.filter(r => !resourcePaths.includes(r.resourceUri.fsPath));
// Remove resource(s) from working group
const workingTreeGroup = this.workingTreeGroup.resourceStates
.filter(r => !resourcePaths.includes(r.resourceUri.fsPath));
// Remove resource(s) from untracked group
const untrackedGroup = this.untrackedGroup.resourceStates
.filter(r => !resourcePaths.includes(r.resourceUri.fsPath));
return { indexGroup, mergeGroup, workingTreeGroup, untrackedGroup };
});
}
async rm(resources: Uri[]): Promise<void> {
await this.run(Operation.Remove, () => this.repository.rm(resources.map(r => r.fsPath)));
}
async stage(resource: Uri, contents: string): Promise<void> {
const path = relativePath(this.repository.root, resource.fsPath).replace(/\\/g, '/');
await this.run(Operation.Stage, async () => {
await this.repository.stage(path, contents);
this.closeDiffEditors([], [...resource.fsPath]);
});
this._onDidChangeOriginalResource.fire(resource);
}
async revert(resources: Uri[]): Promise<void> {
await this.run(
Operation.RevertFiles(!this.optimisticUpdateEnabled()),
async () => {
await this.repository.revert('HEAD', resources.map(r => r.fsPath));
this.closeDiffEditors([...resources.length !== 0 ?
resources.map(r => r.fsPath) :
this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath)], []);
},
() => {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const untrackedChanges = config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges');
const untrackedChangesResourceGroupType = untrackedChanges === 'mixed' ? ResourceGroupType.WorkingTree : ResourceGroupType.Untracked;
const resourcePaths = resources.length === 0 ?
this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath) : resources.map(r => r.fsPath);
// Collect removed resources
const trackedResources: Resource[] = [];
const untrackedResources: Resource[] = [];
for (const resource of this.indexGroup.resourceStates) {
if (resourcePaths.includes(resource.resourceUri.fsPath)) {
if (resource.type === Status.INDEX_ADDED) {
untrackedResources.push(resource.clone(untrackedChangesResourceGroupType));
} else {
trackedResources.push(resource.clone(ResourceGroupType.WorkingTree));
}
}
}
// Remove resource(s) from index group
const indexGroup = this.indexGroup.resourceStates
.filter(r => !resourcePaths.includes(r.resourceUri.fsPath));
// Add resource(s) to working group
const workingTreeGroup = untrackedChanges === 'mixed' ?
[...this.workingTreeGroup.resourceStates, ...trackedResources, ...untrackedResources] :
[...this.workingTreeGroup.resourceStates, ...trackedResources];
// Add resource(s) to untracked group
const untrackedGroup = untrackedChanges === 'separate' ?
[...this.untrackedGroup.resourceStates, ...untrackedResources] : undefined;
return { indexGroup, workingTreeGroup, untrackedGroup };
});
}
async commit(message: string | undefined, opts: CommitOptions = Object.create(null)): Promise<void> {
if (this.rebaseCommit) {
await this.run(
Operation.RebaseContinue,
async () => {
if (opts.all) {
const addOpts = opts.all === 'tracked' ? { update: true } : {};
await this.repository.add([], addOpts);
}
await this.repository.rebaseContinue();
await this.commitOperationCleanup(message, opts);
},
() => this.commitOperationGetOptimisticResourceGroups(opts));
} else {
// Set post-commit command to render the correct action button
this.commitCommandCenter.postCommitCommand = opts.postCommitCommand;
await this.run(
Operation.Commit,
async () => {
if (opts.all) {
const addOpts = opts.all === 'tracked' ? { update: true } : {};
await this.repository.add([], addOpts);
}
delete opts.all;
if (opts.requireUserConfig === undefined || opts.requireUserConfig === null) {
const config = workspace.getConfiguration('git', Uri.file(this.root));
opts.requireUserConfig = config.get<boolean>('requireGitUserConfig');
}
await this.repository.commit(message, opts);
await this.commitOperationCleanup(message, opts);
},
() => this.commitOperationGetOptimisticResourceGroups(opts));
// Execute post-commit command
await this.run(Operation.PostCommitCommand, async () => {
await this.commitCommandCenter.executePostCommitCommand(opts.postCommitCommand);
});
}
}
private async commitOperationCleanup(message: string | undefined, opts: CommitOptions) {
if (message) {
this.inputBox.value = await this.getInputTemplate();
}
const indexResources = [...this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath)];
const workingGroupResources = opts.all && opts.all !== 'tracked' ?
[...this.workingTreeGroup.resourceStates.map(r => r.resourceUri.fsPath)] : [];
this.closeDiffEditors(indexResources, workingGroupResources);
}
private commitOperationGetOptimisticResourceGroups(opts: CommitOptions): GitResourceGroups {
let untrackedGroup: Resource[] | undefined = undefined,
workingTreeGroup: Resource[] | undefined = undefined;
if (opts.all === 'tracked') {
workingTreeGroup = this.workingTreeGroup.resourceStates
.filter(r => r.type === Status.UNTRACKED);
} else if (opts.all) {
untrackedGroup = workingTreeGroup = [];
}
return { indexGroup: [], mergeGroup: [], untrackedGroup, workingTreeGroup };
}
async clean(resources: Uri[]): Promise<void> {
await this.run(
Operation.Clean(!this.optimisticUpdateEnabled()),
async () => {
const toClean: string[] = [];
const toCheckout: string[] = [];
const submodulesToUpdate: string[] = [];
const resourceStates = [...this.workingTreeGroup.resourceStates, ...this.untrackedGroup.resourceStates];
resources.forEach(r => {
const fsPath = r.fsPath;
for (const submodule of this.submodules) {
if (path.join(this.root, submodule.path) === fsPath) {
submodulesToUpdate.push(fsPath);
return;
}
}
const raw = r.toString();
const scmResource = find(resourceStates, sr => sr.resourceUri.toString() === raw);
if (!scmResource) {
return;
}
switch (scmResource.type) {
case Status.UNTRACKED:
case Status.IGNORED:
toClean.push(fsPath);
break;
default:
toCheckout.push(fsPath);
break;
}
});
await this.repository.clean(toClean);
try {
await this.repository.checkout('', toCheckout);
} catch (err) {
if (err.gitErrorCode !== GitErrorCodes.BranchNotYetBorn) {
throw err;
}
}
await this.repository.updateSubmodules(submodulesToUpdate);
this.closeDiffEditors([], [...toClean, ...toCheckout]);
},
() => {
const resourcePaths = resources.map(r => r.fsPath);
// Remove resource(s) from working group
const workingTreeGroup = this.workingTreeGroup.resourceStates
.filter(r => !resourcePaths.includes(r.resourceUri.fsPath));
// Remove resource(s) from untracked group
const untrackedGroup = this.untrackedGroup.resourceStates
.filter(r => !resourcePaths.includes(r.resourceUri.fsPath));
return { workingTreeGroup, untrackedGroup };
});
}
closeDiffEditors(indexResources: string[] | undefined, workingTreeResources: string[] | undefined, ignoreSetting = false): void {
const config = workspace.getConfiguration('git', Uri.file(this.root));
if (!config.get<boolean>('closeDiffOnOperation', false) && !ignoreSetting) { return; }
const diffEditorTabsToClose: Tab[] = [];
for (const tab of window.tabGroups.all.map(g => g.tabs).flat()) {
const { input } = tab;
if (input instanceof TabInputTextDiff || input instanceof TabInputNotebookDiff) {
if (input.modified.scheme === 'git' && (indexResources === undefined || indexResources.some(r => pathEquals(r, input.modified.fsPath)))) {
// Index
diffEditorTabsToClose.push(tab);
}
if (input.modified.scheme === 'file' && input.original.scheme === 'git' && (workingTreeResources === undefined || workingTreeResources.some(r => pathEquals(r, input.modified.fsPath)))) {
// Working Tree
diffEditorTabsToClose.push(tab);
}
}
}
// Close editors
window.tabGroups.close(diffEditorTabsToClose, true);
}
async branch(name: string, _checkout: boolean, _ref?: string): Promise<void> {
await this.run(Operation.Branch, () => this.repository.branch(name, _checkout, _ref));
}
async deleteBranch(name: string, force?: boolean): Promise<void> {
await this.run(Operation.DeleteBranch, () => this.repository.deleteBranch(name, force));
}
async renameBranch(name: string): Promise<void> {
await this.run(Operation.RenameBranch, () => this.repository.renameBranch(name));
}
@throttle
async fastForwardBranch(name: string): Promise<void> {
// Get branch details
const branch = await this.getBranch(name);
if (!branch.upstream?.remote || !branch.upstream?.name || !branch.name) {
return;
}
try {
// Fast-forward the branch if possible
const options = { remote: branch.upstream.remote, ref: `${branch.upstream.name}:${branch.name}` };
await this.run(Operation.Fetch(true), async () => this.repository.fetch(options));
} catch (err) {
if (err.gitErrorCode === GitErrorCodes.BranchFastForwardRejected) {
return;
}
throw err;
}
}
async cherryPick(commitHash: string): Promise<void> {
await this.run(Operation.CherryPick, () => this.repository.cherryPick(commitHash));
}
async move(from: string, to: string): Promise<void> {
await this.run(Operation.Move, () => this.repository.move(from, to));
}
async getBranch(name: string): Promise<Branch> {
return await this.run(Operation.GetBranch, () => this.repository.getBranch(name));
}
async getBranches(query: BranchQuery = {}, cancellationToken?: CancellationToken): Promise<Ref[]> {
return await this.run(Operation.GetBranches, async () => {
const refs = await this.getRefs(query, cancellationToken);
return refs.filter(value => (value.type === RefType.Head || value.type === RefType.RemoteHead) && (query.remote || !value.remote));
});
}
async getRefs(query: RefQuery = {}, cancellationToken?: CancellationToken): Promise<Ref[]> {
const config = workspace.getConfiguration('git');
let defaultSort = config.get<'alphabetically' | 'committerdate'>('branchSortOrder');
if (defaultSort !== 'alphabetically' && defaultSort !== 'committerdate') {
defaultSort = 'alphabetically';
}
query = { ...query, sort: query?.sort ?? defaultSort };
return await this.run(Operation.GetRefs, () => this.repository.getRefs(query, cancellationToken));
}
async getRemoteRefs(remote: string, opts?: { heads?: boolean; tags?: boolean }): Promise<Ref[]> {
return await this.run(Operation.GetRemoteRefs, () => this.repository.getRemoteRefs(remote, opts));
}
async setBranchUpstream(name: string, upstream: string): Promise<void> {
await this.run(Operation.SetBranchUpstream, () => this.repository.setBranchUpstream(name, upstream));
}
async merge(ref: string): Promise<void> {
await this.run(Operation.Merge, () => this.repository.merge(ref));
}
async mergeAbort(): Promise<void> {
await this.run(Operation.MergeAbort, async () => await this.repository.mergeAbort());
}
async rebase(branch: string): Promise<void> {
await this.run(Operation.Rebase, () => this.repository.rebase(branch));
}
async tag(name: string, message?: string): Promise<void> {
await this.run(Operation.Tag, () => this.repository.tag(name, message));
}
async deleteTag(name: string): Promise<void> {
await this.run(Operation.DeleteTag, () => this.repository.deleteTag(name));
}
async deleteRemoteTag(remoteName: string, tagName: string): Promise<void> {
await this.run(Operation.DeleteRemoteTag, () => this.repository.deleteRemoteTag(remoteName, tagName));
}
async checkout(treeish: string, opts?: { detached?: boolean; pullBeforeCheckout?: boolean }): Promise<void> {
const refLabel = opts?.detached ? treeish.substring(0, 8) : treeish;
await this.run(Operation.Checkout(refLabel),
async () => {
if (opts?.pullBeforeCheckout && !opts?.detached) {
try {
await this.fastForwardBranch(treeish);
}
catch (err) {
// noop
}
}
await this.repository.checkout(treeish, [], opts);
});
}
async checkoutTracking(treeish: string, opts: { detached?: boolean } = {}): Promise<void> {
const refLabel = opts.detached ? treeish.substring(0, 8) : treeish;
await this.run(Operation.CheckoutTracking(refLabel), () => this.repository.checkout(treeish, [], { ...opts, track: true }));
}
async findTrackingBranches(upstreamRef: string): Promise<Branch[]> {
return await this.run(Operation.FindTrackingBranches, () => this.repository.findTrackingBranches(upstreamRef));
}
async getCommit(ref: string): Promise<Commit> {
return await this.repository.getCommit(ref);
}
async reset(treeish: string, hard?: boolean): Promise<void> {
await this.run(Operation.Reset, () => this.repository.reset(treeish, hard));
}
async deleteRef(ref: string): Promise<void> {
await this.run(Operation.DeleteRef, () => this.repository.deleteRef(ref));
}
async addRemote(name: string, url: string): Promise<void> {
await this.run(Operation.Remote, () => this.repository.addRemote(name, url));
}
async removeRemote(name: string): Promise<void> {
await this.run(Operation.Remote, () => this.repository.removeRemote(name));
}
async renameRemote(name: string, newName: string): Promise<void> {
await this.run(Operation.Remote, () => this.repository.renameRemote(name, newName));
}
@throttle
async fetchDefault(options: { silent?: boolean } = {}): Promise<void> {
await this._fetch({ silent: options.silent });
}
@throttle
async fetchPrune(): Promise<void> {
await this._fetch({ prune: true });
}
@throttle
async fetchAll(options: { silent?: boolean } = {}, cancellationToken?: CancellationToken): Promise<void> {
await this._fetch({ all: true, silent: options.silent, cancellationToken });
}
async fetch(options: FetchOptions): Promise<void> {
await this._fetch(options);
}
private async _fetch(options: { remote?: string; ref?: string; all?: boolean; prune?: boolean; depth?: number; silent?: boolean; cancellationToken?: CancellationToken } = {}): Promise<void> {
if (!options.prune) {
const config = workspace.getConfiguration('git', Uri.file(this.root));
const prune = config.get<boolean>('pruneOnFetch');
options.prune = prune;
}
await this.run(Operation.Fetch(options.silent !== true), async () => this.repository.fetch(options));
}
@throttle
async pullWithRebase(head: Branch | undefined): Promise<void> {
let remote: string | undefined;
let branch: string | undefined;
if (head && head.name && head.upstream) {
remote = head.upstream.remote;
branch = `${head.upstream.name}`;
}
return this.pullFrom(true, remote, branch);
}
@throttle
async pull(head?: Branch, unshallow?: boolean): Promise<void> {
let remote: string | undefined;
let branch: string | undefined;
if (head && head.name && head.upstream) {
remote = head.upstream.remote;
branch = `${head.upstream.name}`;
}
return this.pullFrom(false, remote, branch, unshallow);
}
async pullFrom(rebase?: boolean, remote?: string, branch?: string, unshallow?: boolean): Promise<void> {
await this.run(Operation.Pull, async () => {
await this.maybeAutoStash(async () => {
const config = workspace.getConfiguration('git', Uri.file(this.root));
const fetchOnPull = config.get<boolean>('fetchOnPull');
const tags = config.get<boolean>('pullTags');
// When fetchOnPull is enabled, fetch all branches when pulling
if (fetchOnPull) {
await this.fetchAll();
}
if (await this.checkIfMaybeRebased(this.HEAD?.name)) {
await this._pullAndHandleTagConflict(rebase, remote, branch, { unshallow, tags });
}
});
});
}
private async _pullAndHandleTagConflict(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise<void> {
try {
await this.repository.pull(rebase, remote, branch, options);
}
catch (err) {
if (err.gitErrorCode !== GitErrorCodes.TagConflict) {
throw err;
}
// Handle tag(s) conflict
if (await this.handleTagConflict(remote, err.stderr)) {
await this.repository.pull(rebase, remote, branch, options);
}
}
}
@throttle
async push(head: Branch, forcePushMode?: ForcePushMode): Promise<void> {
let remote: string | undefined;
let branch: string | undefined;
if (head && head.name && head.upstream) {
remote = head.upstream.remote;
branch = `${head.name}:${head.upstream.name}`;
}
await this.run(Operation.Push, () => this._push(remote, branch, undefined, undefined, forcePushMode));
}
async pushTo(remote?: string, name?: string, setUpstream = false, forcePushMode?: ForcePushMode): Promise<void> {
await this.run(Operation.Push, () => this._push(remote, name, setUpstream, undefined, forcePushMode));
}
async pushFollowTags(remote?: string, forcePushMode?: ForcePushMode): Promise<void> {
await this.run(Operation.Push, () => this._push(remote, undefined, false, true, forcePushMode));
}
async pushTags(remote?: string, forcePushMode?: ForcePushMode): Promise<void> {
await this.run(Operation.Push, () => this._push(remote, undefined, false, false, forcePushMode, true));
}
async blame(path: string): Promise<string> {
return await this.run(Operation.Blame, () => this.repository.blame(path));
}
@throttle
sync(head: Branch, rebase: boolean): Promise<void> {
return this._sync(head, rebase);
}
private async _sync(head: Branch, rebase: boolean): Promise<void> {
let remoteName: string | undefined;
let pullBranch: string | undefined;
let pushBranch: string | undefined;
if (head.name && head.upstream) {
remoteName = head.upstream.remote;
pullBranch = `${head.upstream.name}`;
pushBranch = `${head.name}:${head.upstream.name}`;
}
await this.run(Operation.Sync, async () => {
await this.maybeAutoStash(async () => {
const config = workspace.getConfiguration('git', Uri.file(this.root));
const fetchOnPull = config.get<boolean>('fetchOnPull');
const tags = config.get<boolean>('pullTags');
const followTags = config.get<boolean>('followTagsWhenSync');
const supportCancellation = config.get<boolean>('supportCancellation');
const fn = async (cancellationToken?: CancellationToken) => {
// When fetchOnPull is enabled, fetch all branches when pulling
if (fetchOnPull) {
await this.fetchAll({}, cancellationToken);
}
if (await this.checkIfMaybeRebased(this.HEAD?.name)) {
await this._pullAndHandleTagConflict(rebase, remoteName, pullBranch, { tags, cancellationToken });
}
};
if (supportCancellation) {
const opts: ProgressOptions = {
location: ProgressLocation.Notification,
title: l10n.t('Syncing. Cancelling may cause serious damages to the repository'),
cancellable: true
};
await window.withProgress(opts, (_, token) => fn(token));
} else {
await fn();
}
const remote = this.remotes.find(r => r.name === remoteName);
if (remote && remote.isReadOnly) {
return;
}
const shouldPush = this.HEAD && (typeof this.HEAD.ahead === 'number' ? this.HEAD.ahead > 0 : true);
if (shouldPush) {
await this._push(remoteName, pushBranch, false, followTags);
}
});
});
}
private async checkIfMaybeRebased(currentBranch?: string) {
const config = workspace.getConfiguration('git');
const shouldIgnore = config.get<boolean>('ignoreRebaseWarning') === true;
if (shouldIgnore) {
return true;
}
const maybeRebased = await this.run(Operation.Log, async () => {
try {
const result = await this.repository.exec(['log', '--oneline', '--cherry', `${currentBranch ?? ''}...${currentBranch ?? ''}@{upstream}`, '--']);
if (result.exitCode) {
return false;
}
return /^=/.test(result.stdout);
} catch {
return false;
}
});
if (!maybeRebased) {
return true;
}
const always = { title: l10n.t('Always Pull') };
const pull = { title: l10n.t('Pull') };
const cancel = { title: l10n.t('Don\'t Pull') };
const result = await window.showWarningMessage(
currentBranch
? l10n.t('It looks like the current branch "{0}" might have been rebased. Are you sure you still want to pull into it?', currentBranch)
: l10n.t('It looks like the current branch might have been rebased. Are you sure you still want to pull into it?'),
always, pull, cancel
);
if (result === pull) {
return true;
}
if (result === always) {
await config.update('ignoreRebaseWarning', true, true);
return true;
}
return false;
}
async show(ref: string, filePath: string): Promise<string> {
return await this.run(Operation.Show, async () => {
const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/');
const configFiles = workspace.getConfiguration('files', Uri.file(filePath));
const defaultEncoding = configFiles.get<string>('encoding');
const autoGuessEncoding = configFiles.get<boolean>('autoGuessEncoding');
try {
return await this.repository.bufferString(`${ref}:${path}`, defaultEncoding, autoGuessEncoding);
} catch (err) {
if (err.gitErrorCode === GitErrorCodes.WrongCase) {
const gitRelativePath = await this.repository.getGitRelativePath(ref, path);
return await this.repository.bufferString(`${ref}:${gitRelativePath}`, defaultEncoding, autoGuessEncoding);
}
throw err;
}
});
}
async buffer(ref: string, filePath: string): Promise<Buffer> {
return this.run(Operation.Show, () => {
const path = relativePath(this.repository.root, filePath).replace(/\\/g, '/');
return this.repository.buffer(`${ref}:${path}`);
});
}
getObjectDetails(ref: string, filePath: string): Promise<{ mode: string; object: string; size: number }> {
return this.run(Operation.GetObjectDetails, () => this.repository.getObjectDetails(ref, filePath));
}
detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string }> {
return this.run(Operation.Show, () => this.repository.detectObjectType(object));
}
async apply(patch: string, reverse?: boolean): Promise<void> {
return await this.run(Operation.Apply, () => this.repository.apply(patch, reverse));
}
async getStashes(): Promise<Stash[]> {
return await this.repository.getStashes();
}
async createStash(message?: string, includeUntracked?: boolean, staged?: boolean): Promise<void> {
const indexResources = [...this.indexGroup.resourceStates.map(r => r.resourceUri.fsPath)];
const workingGroupResources = [
...!staged ? this.workingTreeGroup.resourceStates.map(r => r.resourceUri.fsPath) : [],
...includeUntracked ? this.untrackedGroup.resourceStates.map(r => r.resourceUri.fsPath) : []];
return await this.run(Operation.Stash, async () => {
await this.repository.createStash(message, includeUntracked, staged);
this.closeDiffEditors(indexResources, workingGroupResources);
});
}
async popStash(index?: number): Promise<void> {
return await this.run(Operation.Stash, () => this.repository.popStash(index));
}
async dropStash(index?: number): Promise<void> {
return await this.run(Operation.Stash, () => this.repository.dropStash(index));
}
async applyStash(index?: number): Promise<void> {
return await this.run(Operation.Stash, () => this.repository.applyStash(index));
}
async getCommitTemplate(): Promise<string> {
return await this.run(Operation.GetCommitTemplate, async () => this.repository.getCommitTemplate());
}
async ignore(files: Uri[]): Promise<void> {
return await this.run(Operation.Ignore, async () => {
const ignoreFile = `${this.repository.root}${path.sep}.gitignore`;
const textToAppend = files
.map(uri => relativePath(this.repository.root, uri.fsPath).replace(/\\/g, '/'))
.join('\n');
const document = await new Promise(c => fs.exists(ignoreFile, c))
? await workspace.openTextDocument(ignoreFile)
: await workspace.openTextDocument(Uri.file(ignoreFile).with({ scheme: 'untitled' }));
await window.showTextDocument(document);
const edit = new WorkspaceEdit();
const lastLine = document.lineAt(document.lineCount - 1);
const text = lastLine.isEmptyOrWhitespace ? `${textToAppend}\n` : `\n${textToAppend}\n`;
edit.insert(document.uri, lastLine.range.end, text);
await workspace.applyEdit(edit);
await document.save();
});
}
async rebaseAbort(): Promise<void> {
await this.run(Operation.RebaseAbort, async () => await this.repository.rebaseAbort());
}
checkIgnore(filePaths: string[]): Promise<Set<string>> {
return this.run(Operation.CheckIgnore, () => {
return new Promise<Set<string>>((resolve, reject) => {
filePaths = filePaths
.filter(filePath => isDescendant(this.root, filePath));
if (filePaths.length === 0) {
// nothing left
return resolve(new Set<string>());
}
// https://git-scm.com/docs/git-check-ignore#git-check-ignore--z
const child = this.repository.stream(['check-ignore', '-v', '-z', '--stdin'], { stdio: [null, null, null] });
child.stdin!.end(filePaths.join('\0'), 'utf8');
const onExit = (exitCode: number) => {
if (exitCode === 1) {
// nothing ignored
resolve(new Set<string>());
} else if (exitCode === 0) {
resolve(new Set<string>(this.parseIgnoreCheck(data)));
} else {
if (/ is in submodule /.test(stderr)) {
reject(new GitError({ stdout: data, stderr, exitCode, gitErrorCode: GitErrorCodes.IsInSubmodule }));
} else {
reject(new GitError({ stdout: data, stderr, exitCode }));
}
}
};
let data = '';
const onStdoutData = (raw: string) => {
data += raw;
};
child.stdout!.setEncoding('utf8');
child.stdout!.on('data', onStdoutData);
let stderr: string = '';
child.stderr!.setEncoding('utf8');
child.stderr!.on('data', raw => stderr += raw);
child.on('error', reject);
child.on('exit', onExit);
});
});
}
// Parses output of `git check-ignore -v -z` and returns only those paths
// that are actually ignored by git.
// Matches to a negative pattern (starting with '!') are filtered out.
// See also https://git-scm.com/docs/git-check-ignore#_output.
private parseIgnoreCheck(raw: string): string[] {
const ignored = [];
const elements = raw.split('\0');
for (let i = 0; i < elements.length; i += 4) {
const pattern = elements[i + 2];
const path = elements[i + 3];
if (pattern && !pattern.startsWith('!')) {
ignored.push(path);
}
}
return ignored;
}
private async _push(remote?: string, refspec?: string, setUpstream = false, followTags = false, forcePushMode?: ForcePushMode, tags = false): Promise<void> {
try {
await this.repository.push(remote, refspec, setUpstream, followTags, forcePushMode, tags);
} catch (err) {
if (!remote || !refspec) {
throw err;
}
const repository = new ApiRepository(this);
const remoteObj = repository.state.remotes.find(r => r.name === remote);
if (!remoteObj) {
throw err;
}
for (const handler of this.pushErrorHandlerRegistry.getPushErrorHandlers()) {
if (await handler.handlePushError(repository, remoteObj, refspec, err)) {
return;
}
}
throw err;
}
}
private async run<T>(
operation: Operation,
runOperation: () => Promise<T> = () => Promise.resolve<any>(null),
getOptimisticResourceGroups: () => GitResourceGroups | undefined = () => undefined): Promise<T> {
if (this.state !== RepositoryState.Idle) {
throw new Error('Repository not initialized');
}
let error: any = null;
this._operations.start(operation);
this._onRunOperation.fire(operation.kind);
try {
const result = await this.retryRun(operation, runOperation);
if (!operation.readOnly) {
await this.updateModelState(this.optimisticUpdateEnabled() ? getOptimisticResourceGroups() : undefined);
}
return result;
} catch (err) {
error = err;
if (err.gitErrorCode === GitErrorCodes.NotAGitRepository) {
this.state = RepositoryState.Disposed;
}
if (!operation.readOnly) {
await this.updateModelState();
}
throw err;
} finally {
this._operations.end(operation);
this._onDidRunOperation.fire({ operation: operation, error });
}
}
private async retryRun<T>(operation: Operation, runOperation: () => Promise<T> = () => Promise.resolve<any>(null)): Promise<T> {
let attempt = 0;
while (true) {
try {
attempt++;
return await runOperation();
} catch (err) {
const shouldRetry = attempt <= 10 && (
(err.gitErrorCode === GitErrorCodes.RepositoryIsLocked)
|| (operation.retry && (err.gitErrorCode === GitErrorCodes.CantLockRef || err.gitErrorCode === GitErrorCodes.CantRebaseMultipleBranches))
);
if (shouldRetry) {
// quatratic backoff
await timeout(Math.pow(attempt, 2) * 50);
} else {
throw err;
}
}
}
}
private static KnownHugeFolderNames = ['node_modules'];
private async findKnownHugeFolderPathsToIgnore(): Promise<string[]> {
const folderPaths: string[] = [];
for (const folderName of Repository.KnownHugeFolderNames) {
const folderPath = path.join(this.repository.root, folderName);
if (await new Promise<boolean>(c => fs.exists(folderPath, c))) {
folderPaths.push(folderPath);
}
}
const ignored = await this.checkIgnore(folderPaths);
return folderPaths.filter(p => !ignored.has(p));
}
private async updateModelState(optimisticResourcesGroups?: GitResourceGroups) {
this.updateModelStateCancellationTokenSource?.cancel();
this.updateModelStateCancellationTokenSource = new CancellationTokenSource();
await this._updateModelState(optimisticResourcesGroups, this.updateModelStateCancellationTokenSource.token);
}
private async _updateModelState(optimisticResourcesGroups?: GitResourceGroups, cancellationToken?: CancellationToken): Promise<void> {
try {
// Optimistically update resource groups
if (optimisticResourcesGroups) {
this._updateResourceGroupsState(optimisticResourcesGroups);
}
const [HEAD, remotes, submodules, rebaseCommit, mergeInProgress, commitTemplate] =
await Promise.all([
this.repository.getHEADRef(),
this.repository.getRemotes(),
this.repository.getSubmodules(),
this.getRebaseCommit(),
this.isMergeInProgress(),
this.getInputTemplate()]);
this._HEAD = HEAD;
this._remotes = remotes!;
this._submodules = submodules!;
this.rebaseCommit = rebaseCommit;
this.mergeInProgress = mergeInProgress;
this._sourceControl.commitTemplate = commitTemplate;
// Execute cancellable long-running operation
const resourceGroups = await this.getStatus(cancellationToken);
this._updateResourceGroupsState(resourceGroups);
this._onDidChangeStatus.fire();
}
catch (err) {
if (err instanceof CancellationError) {
return;
}
throw err;
}
}
private _updateResourceGroupsState(resourcesGroups: GitResourceGroups): void {
// set resource groups
if (resourcesGroups.indexGroup) { this.indexGroup.resourceStates = resourcesGroups.indexGroup; }
if (resourcesGroups.mergeGroup) { this.mergeGroup.resourceStates = resourcesGroups.mergeGroup; }
if (resourcesGroups.untrackedGroup) { this.untrackedGroup.resourceStates = resourcesGroups.untrackedGroup; }
if (resourcesGroups.workingTreeGroup) { this.workingTreeGroup.resourceStates = resourcesGroups.workingTreeGroup; }
// set count badge
this.setCountBadge();
}
private async getStatus(cancellationToken?: CancellationToken): Promise<GitResourceGroups> {
if (cancellationToken && cancellationToken.isCancellationRequested) {
throw new CancellationError();
}
const scopedConfig = workspace.getConfiguration('git', Uri.file(this.repository.root));
const untrackedChanges = scopedConfig.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges');
const ignoreSubmodules = scopedConfig.get<boolean>('ignoreSubmodules');
const limit = scopedConfig.get<number>('statusLimit', 10000);
const similarityThreshold = scopedConfig.get<number>('similarityThreshold', 50);
const start = new Date().getTime();
const { status, statusLength, didHitLimit } = await this.repository.getStatus({ limit, ignoreSubmodules, similarityThreshold, untrackedChanges, cancellationToken });
const totalTime = new Date().getTime() - start;
this.isRepositoryHuge = didHitLimit ? { limit } : false;
if (didHitLimit) {
/* __GDPR__
"statusLimit" : {
"owner": "lszomoru",
"ignoreSubmodules": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Setting indicating whether submodules are ignored" },
"limit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Setting indicating the limit of status entries" },
"statusLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of status entries" },
"totalTime": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of ms the operation took" }
}
*/
this.telemetryReporter.sendTelemetryEvent('statusLimit', { ignoreSubmodules: String(ignoreSubmodules) }, { limit, statusLength, totalTime });
}
if (totalTime > 5000) {
/* __GDPR__
"statusSlow" : {
"owner": "digitarald",
"comment": "Reports when git status is slower than 5s",
"expiration": "1.73",
"ignoreSubmodules": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Setting indicating whether submodules are ignored" },
"didHitLimit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "Total number of status entries" },
"didWarnAboutLimit": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "comment": "True when the user was warned about slow git status" },
"statusLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of status entries" },
"totalTime": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true, "comment": "Total number of ms the operation took" }
}
*/
this.telemetryReporter.sendTelemetryEvent('statusSlow', { ignoreSubmodules: String(ignoreSubmodules), didHitLimit: String(didHitLimit), didWarnAboutLimit: String(this.didWarnAboutLimit) }, { statusLength, totalTime });
}
// Triggers or clears any validation warning
this._sourceControl.inputBox.validateInput = this._sourceControl.inputBox.validateInput;
const config = workspace.getConfiguration('git');
const shouldIgnore = config.get<boolean>('ignoreLimitWarning') === true;
const useIcons = !config.get<boolean>('decorations.enabled', true);
if (didHitLimit && !shouldIgnore && !this.didWarnAboutLimit) {
const knownHugeFolderPaths = await this.findKnownHugeFolderPathsToIgnore();
const gitWarn = l10n.t('The git repository at "{0}" has too many active changes, only a subset of Git features will be enabled.', this.repository.root);
const neverAgain = { title: l10n.t('Don\'t Show Again') };
if (knownHugeFolderPaths.length > 0) {
const folderPath = knownHugeFolderPaths[0];
const folderName = path.basename(folderPath);
const addKnown = l10n.t('Would you like to add "{0}" to .gitignore?', folderName);
const yes = { title: l10n.t('Yes') };
const no = { title: l10n.t('No') };
const result = await window.showWarningMessage(`${gitWarn} ${addKnown}`, yes, no, neverAgain);
if (result === yes) {
this.ignore([Uri.file(folderPath)]);
} else {
if (result === neverAgain) {
config.update('ignoreLimitWarning', true, false);
}
this.didWarnAboutLimit = true;
}
} else {
const ok = { title: l10n.t('OK') };
const result = await window.showWarningMessage(gitWarn, ok, neverAgain);
if (result === neverAgain) {
config.update('ignoreLimitWarning', true, false);
}
this.didWarnAboutLimit = true;
}
}
const indexGroup: Resource[] = [],
mergeGroup: Resource[] = [],
untrackedGroup: Resource[] = [],
workingTreeGroup: Resource[] = [];
status.forEach(raw => {
const uri = Uri.file(path.join(this.repository.root, raw.path));
const renameUri = raw.rename
? Uri.file(path.join(this.repository.root, raw.rename))
: undefined;
switch (raw.x + raw.y) {
case '??': switch (untrackedChanges) {
case 'mixed': return workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.UNTRACKED, useIcons));
case 'separate': return untrackedGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Untracked, uri, Status.UNTRACKED, useIcons));
default: return undefined;
}
case '!!': switch (untrackedChanges) {
case 'mixed': return workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.IGNORED, useIcons));
case 'separate': return untrackedGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Untracked, uri, Status.IGNORED, useIcons));
default: return undefined;
}
case 'DD': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.BOTH_DELETED, useIcons));
case 'AU': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.ADDED_BY_US, useIcons));
case 'UD': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.DELETED_BY_THEM, useIcons));
case 'UA': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.ADDED_BY_THEM, useIcons));
case 'DU': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.DELETED_BY_US, useIcons));
case 'AA': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.BOTH_ADDED, useIcons));
case 'UU': return mergeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Merge, uri, Status.BOTH_MODIFIED, useIcons));
}
switch (raw.x) {
case 'M': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_MODIFIED, useIcons)); break;
case 'A': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_ADDED, useIcons)); break;
case 'D': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_DELETED, useIcons)); break;
case 'R': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_RENAMED, useIcons, renameUri)); break;
case 'C': indexGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.Index, uri, Status.INDEX_COPIED, useIcons, renameUri)); break;
}
switch (raw.y) {
case 'M': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.MODIFIED, useIcons, renameUri)); break;
case 'D': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.DELETED, useIcons, renameUri)); break;
case 'A': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.INTENT_TO_ADD, useIcons, renameUri)); break;
case 'R': workingTreeGroup.push(new Resource(this.resourceCommandResolver, ResourceGroupType.WorkingTree, uri, Status.INTENT_TO_RENAME, useIcons, renameUri)); break;
}
return undefined;
});
return { indexGroup, mergeGroup, untrackedGroup, workingTreeGroup };
}
private setCountBadge(): void {
const config = workspace.getConfiguration('git', Uri.file(this.repository.root));
const countBadge = config.get<'all' | 'tracked' | 'off'>('countBadge');
const untrackedChanges = config.get<'mixed' | 'separate' | 'hidden'>('untrackedChanges');
let count =
this.mergeGroup.resourceStates.length +
this.indexGroup.resourceStates.length +
this.workingTreeGroup.resourceStates.length;
switch (countBadge) {
case 'off': count = 0; break;
case 'tracked':
if (untrackedChanges === 'mixed') {
count -= this.workingTreeGroup.resourceStates.filter(r => r.type === Status.UNTRACKED || r.type === Status.IGNORED).length;
}
break;
case 'all':
if (untrackedChanges === 'separate') {
count += this.untrackedGroup.resourceStates.length;
}
break;
}
this._sourceControl.count = count;
}
private async getRebaseCommit(): Promise<Commit | undefined> {
const rebaseHeadPath = path.join(this.repository.root, '.git', 'REBASE_HEAD');
const rebaseApplyPath = path.join(this.repository.root, '.git', 'rebase-apply');
const rebaseMergePath = path.join(this.repository.root, '.git', 'rebase-merge');
try {
const [rebaseApplyExists, rebaseMergePathExists, rebaseHead] = await Promise.all([
new Promise<boolean>(c => fs.exists(rebaseApplyPath, c)),
new Promise<boolean>(c => fs.exists(rebaseMergePath, c)),
new Promise<string>((c, e) => fs.readFile(rebaseHeadPath, 'utf8', (err, result) => err ? e(err) : c(result)))
]);
if (!rebaseApplyExists && !rebaseMergePathExists) {
return undefined;
}
return await this.getCommit(rebaseHead.trim());
} catch (err) {
return undefined;
}
}
private isMergeInProgress(): Promise<boolean> {
const mergeHeadPath = path.join(this.repository.root, '.git', 'MERGE_HEAD');
return new Promise<boolean>(resolve => fs.exists(mergeHeadPath, resolve));
}
private async maybeAutoStash<T>(runOperation: () => Promise<T>): Promise<T> {
const config = workspace.getConfiguration('git', Uri.file(this.root));
const shouldAutoStash = config.get<boolean>('autoStash')
&& this.workingTreeGroup.resourceStates.some(r => r.type !== Status.UNTRACKED && r.type !== Status.IGNORED);
if (!shouldAutoStash) {
return await runOperation();
}
await this.repository.createStash(undefined, true);
const result = await runOperation();
await this.repository.popStash();
return result;
}
private onFileChange(_uri: Uri): void {
const config = workspace.getConfiguration('git');
const autorefresh = config.get<boolean>('autorefresh');
if (!autorefresh) {
return;
}
if (this.isRepositoryHuge) {
return;
}
if (!this.operations.isIdle()) {
return;
}
this.eventuallyUpdateWhenIdleAndWait();
}
@debounce(1000)
private eventuallyUpdateWhenIdleAndWait(): void {
this.updateWhenIdleAndWait();
}
@throttle
private async updateWhenIdleAndWait(): Promise<void> {
await this.whenIdleAndFocused();
await this.status();
await timeout(5000);
}
async whenIdleAndFocused(): Promise<void> {
while (true) {
if (!this.operations.isIdle()) {
await eventToPromise(this.onDidRunOperation);
continue;
}
if (!window.state.focused) {
const onDidFocusWindow = filterEvent(window.onDidChangeWindowState, e => e.focused);
await eventToPromise(onDidFocusWindow);
continue;
}
return;
}
}
get headLabel(): string {
const HEAD = this.HEAD;
if (!HEAD) {
return '';
}
const head = HEAD.name || (HEAD.commit || '').substr(0, 8);
return head
+ (this.workingTreeGroup.resourceStates.length + this.untrackedGroup.resourceStates.length > 0 ? '*' : '')
+ (this.indexGroup.resourceStates.length > 0 ? '+' : '')
+ (this.mergeGroup.resourceStates.length > 0 ? '!' : '');
}
get syncLabel(): string {
if (!this.HEAD
|| !this.HEAD.name
|| !this.HEAD.commit
|| !this.HEAD.upstream
|| !(this.HEAD.ahead || this.HEAD.behind)
) {
return '';
}
const remoteName = this.HEAD && this.HEAD.remote || this.HEAD.upstream.remote;
const remote = this.remotes.find(r => r.name === remoteName);
if (remote && remote.isReadOnly) {
return `${this.HEAD.behind}`;
}
return `${this.HEAD.behind}${this.HEAD.ahead}`;
}
get syncTooltip(): string {
if (!this.HEAD
|| !this.HEAD.name
|| !this.HEAD.commit
|| !this.HEAD.upstream
|| !(this.HEAD.ahead || this.HEAD.behind)
) {
return l10n.t('Synchronize Changes');
}
const remoteName = this.HEAD && this.HEAD.remote || this.HEAD.upstream.remote;
const remote = this.remotes.find(r => r.name === remoteName);
if ((remote && remote.isReadOnly) || !this.HEAD.ahead) {
return l10n.t('Pull {0} commits from {1}/{2}', this.HEAD.behind!, this.HEAD.upstream.remote, this.HEAD.upstream.name);
} else if (!this.HEAD.behind) {
return l10n.t('Push {0} commits to {1}/{2}', this.HEAD.ahead, this.HEAD.upstream.remote, this.HEAD.upstream.name);
} else {
return l10n.t('Pull {0} and push {1} commits between {2}/{3}', this.HEAD.behind, this.HEAD.ahead, this.HEAD.upstream.remote, this.HEAD.upstream.name);
}
}
private updateInputBoxPlaceholder(): void {
const branchName = this.headShortName;
if (branchName) {
// '{0}' will be replaced by the corresponding key-command later in the process, which is why it needs to stay.
this._sourceControl.inputBox.placeholder = l10n.t('Message ({0} to commit on "{1}")', '{0}', branchName);
} else {
this._sourceControl.inputBox.placeholder = l10n.t('Message ({0} to commit)');
}
}
private updateBranchProtectionMatchers(root: Uri): void {
this.branchProtection.clear();
for (const provider of this.branchProtectionProviderRegistry.getBranchProtectionProviders(root)) {
for (const { remote, rules } of provider.provideBranchProtection()) {
const matchers: BranchProtectionMatcher[] = [];
for (const rule of rules) {
const include = rule.include && rule.include.length !== 0 ? picomatch(rule.include) : undefined;
const exclude = rule.exclude && rule.exclude.length !== 0 ? picomatch(rule.exclude) : undefined;
if (include || exclude) {
matchers.push({ include, exclude });
}
}
if (matchers.length !== 0) {
this.branchProtection.set(remote, matchers);
}
}
}
this._onDidChangeBranchProtection.fire();
}
private optimisticUpdateEnabled(): boolean {
const config = workspace.getConfiguration('git', Uri.file(this.root));
return config.get<boolean>('optimisticUpdate') === true;
}
private async handleTagConflict(remote: string | undefined, raw: string): Promise<boolean> {
// Ensure there is a remote
remote = remote ?? this.HEAD?.upstream?.remote;
if (!remote) {
throw new Error('Unable to resolve tag conflict due to missing remote.');
}
// Extract tag names from message
const tags: string[] = [];
for (const match of raw.matchAll(/^ ! \[rejected\]\s+([^\s]+)\s+->\s+([^\s]+)\s+\(would clobber existing tag\)$/gm)) {
if (match.length === 3) {
tags.push(match[1]);
}
}
if (tags.length === 0) {
throw new Error(`Unable to extract tag names from error message: ${raw}`);
}
// Notification
const replaceLocalTags = l10n.t('Replace Local Tag(s)');
const message = l10n.t('Unable to pull from remote repository due to conflicting tag(s): {0}. Would you like to resolve the conflict by replacing the local tag(s)?', tags.join(', '));
const choice = await window.showErrorMessage(message, { modal: true }, replaceLocalTags);
if (choice !== replaceLocalTags) {
return false;
}
// Force fetch tags
await this.repository.fetchTags({ remote, tags, force: true });
return true;
}
public isBranchProtected(branch = this.HEAD): boolean {
if (branch?.name) {
// Default branch protection (settings)
const defaultBranchProtectionMatcher = this.branchProtection.get('');
if (defaultBranchProtectionMatcher?.length === 1 &&
defaultBranchProtectionMatcher[0].include &&
defaultBranchProtectionMatcher[0].include(branch.name)) {
return true;
}
if (branch.upstream?.remote) {
// Branch protection (contributed)
const remoteBranchProtectionMatcher = this.branchProtection.get(branch.upstream.remote);
if (remoteBranchProtectionMatcher && remoteBranchProtectionMatcher?.length !== 0) {
return remoteBranchProtectionMatcher.some(matcher => {
const include = matcher.include ? matcher.include(branch.name!) : true;
const exclude = matcher.exclude ? matcher.exclude(branch.name!) : false;
return include && !exclude;
});
}
}
}
return false;
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
}