Files
azuredatastudio/extensions/git/src/git.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

2504 lines
67 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 { promises as fs, exists, realpath } from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as cp from 'child_process';
import { fileURLToPath } from 'url';
import * as which from 'which';
import { EventEmitter } from 'events';
import * as iconv from '@vscode/iconv-lite-umd';
import * as filetype from 'file-type';
import { assign, groupBy, IDisposable, toDisposable, dispose, mkdirp, readBytes, detectUnicodeEncoding, Encoding, onceEvent, splitInChunks, Limiter, Versions, isWindows, pathEquals } from './util';
import { CancellationError, CancellationToken, ConfigurationChangeEvent, LogOutputChannel, Uri, workspace } from 'vscode'; // {{SQL CARBON EDIT}} remove Progress
import { detectEncoding } from './encoding';
import { Ref, RefType, Branch, Remote, ForcePushMode, GitErrorCodes, LogOptions, Change, Status, CommitOptions, RefQuery, InitOptions, ICloneOptions } from './api/git'; // {{SQL CARBON EDIT}} add ICloneOptions
import * as byline from 'byline';
import { StringDecoder } from 'string_decoder';
// https://github.com/microsoft/vscode/issues/65693
const MAX_CLI_LENGTH = 30000;
export interface IGit {
path: string;
version: string;
}
export interface IFileStatus {
x: string;
y: string;
path: string;
rename?: string;
}
export interface Stash {
index: number;
description: string;
}
interface MutableRemote extends Remote {
fetchUrl?: string;
pushUrl?: string;
isReadOnly: boolean;
}
// TODO@eamodio: Move to git.d.ts once we are good with the api
/**
* Log file options.
*/
export interface LogFileOptions {
/** Optional. The maximum number of log entries to retrieve. */
readonly maxEntries?: number | string;
/** Optional. The Git sha (hash) to start retrieving log entries from. */
readonly hash?: string;
/** Optional. Specifies whether to start retrieving log entries in reverse order. */
readonly reverse?: boolean;
readonly sortByAuthorDate?: boolean;
}
function parseVersion(raw: string): string {
return raw.replace(/^git version /, '');
}
function findSpecificGit(path: string, onValidate: (path: string) => boolean): Promise<IGit> {
return new Promise<IGit>((c, e) => {
if (!onValidate(path)) {
return e('git not found');
}
const buffers: Buffer[] = [];
const child = cp.spawn(path, ['--version']);
child.stdout.on('data', (b: Buffer) => buffers.push(b));
child.on('error', cpErrorHandler(e));
child.on('exit', code => code ? e(new Error('Not found')) : c({ path, version: parseVersion(Buffer.concat(buffers).toString('utf8').trim()) }));
});
}
function findGitDarwin(onValidate: (path: string) => boolean): Promise<IGit> {
return new Promise<IGit>((c, e) => {
cp.exec('which git', (err, gitPathBuffer) => {
if (err) {
return e('git not found');
}
const path = gitPathBuffer.toString().trim();
function getVersion(path: string) {
if (!onValidate(path)) {
return e('git not found');
}
// make sure git executes
cp.exec('git --version', (err, stdout) => {
if (err) {
return e('git not found');
}
return c({ path, version: parseVersion(stdout.trim()) });
});
}
if (path !== '/usr/bin/git') {
return getVersion(path);
}
// must check if XCode is installed
cp.exec('xcode-select -p', (err: any) => {
if (err && err.code === 2) {
// git is not installed, and launching /usr/bin/git
// will prompt the user to install it
return e('git not found');
}
getVersion(path);
});
});
});
}
function findSystemGitWin32(base: string, onValidate: (path: string) => boolean): Promise<IGit> {
if (!base) {
return Promise.reject<IGit>('Not found');
}
return findSpecificGit(path.join(base, 'Git', 'cmd', 'git.exe'), onValidate);
}
function findGitWin32InPath(onValidate: (path: string) => boolean): Promise<IGit> {
const whichPromise = new Promise<string>((c, e) => which('git.exe', (err, path) => err ? e(err) : c(path)));
return whichPromise.then(path => findSpecificGit(path, onValidate));
}
function findGitWin32(onValidate: (path: string) => boolean): Promise<IGit> {
return findSystemGitWin32(process.env['ProgramW6432'] as string, onValidate)
.then(undefined, () => findSystemGitWin32(process.env['ProgramFiles(x86)'] as string, onValidate))
.then(undefined, () => findSystemGitWin32(process.env['ProgramFiles'] as string, onValidate))
.then(undefined, () => findSystemGitWin32(path.join(process.env['LocalAppData'] as string, 'Programs'), onValidate))
.then(undefined, () => findGitWin32InPath(onValidate));
}
export async function findGit(hints: string[], onValidate: (path: string) => boolean): Promise<IGit> {
for (const hint of hints) {
try {
return await findSpecificGit(hint, onValidate);
} catch {
// noop
}
}
try {
switch (process.platform) {
case 'darwin': return await findGitDarwin(onValidate);
case 'win32': return await findGitWin32(onValidate);
default: return await findSpecificGit('git', onValidate);
}
} catch {
// noop
}
throw new Error('Git installation not found.');
}
export interface IExecutionResult<T extends string | Buffer> {
exitCode: number;
stdout: T;
stderr: string;
}
function cpErrorHandler(cb: (reason?: any) => void): (reason?: any) => void {
return err => {
if (/ENOENT/.test(err.message)) {
err = new GitError({
error: err,
message: 'Failed to execute git (ENOENT)',
gitErrorCode: GitErrorCodes.NotAGitRepository
});
}
cb(err);
};
}
export interface SpawnOptions extends cp.SpawnOptions {
input?: string;
encoding?: string;
log?: boolean;
cancellationToken?: CancellationToken;
onSpawn?: (childProcess: cp.ChildProcess) => void;
}
async function exec(child: cp.ChildProcess, cancellationToken?: CancellationToken): Promise<IExecutionResult<Buffer>> {
if (!child.stdout || !child.stderr) {
throw new GitError({ message: 'Failed to get stdout or stderr from git process.' });
}
if (cancellationToken && cancellationToken.isCancellationRequested) {
throw new CancellationError();
}
const disposables: IDisposable[] = [];
const once = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void) => {
ee.once(name, fn);
disposables.push(toDisposable(() => ee.removeListener(name, fn)));
};
const on = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void) => {
ee.on(name, fn);
disposables.push(toDisposable(() => ee.removeListener(name, fn)));
};
let result = Promise.all<any>([
new Promise<number>((c, e) => {
once(child, 'error', cpErrorHandler(e));
once(child, 'exit', c);
}),
new Promise<Buffer>(c => {
const buffers: Buffer[] = [];
on(child.stdout!, 'data', (b: Buffer) => buffers.push(b));
once(child.stdout!, 'close', () => c(Buffer.concat(buffers)));
}),
new Promise<string>(c => {
const buffers: Buffer[] = [];
on(child.stderr!, 'data', (b: Buffer) => buffers.push(b));
once(child.stderr!, 'close', () => c(Buffer.concat(buffers).toString('utf8')));
})
]) as Promise<[number, Buffer, string]>;
if (cancellationToken) {
const cancellationPromise = new Promise<[number, Buffer, string]>((_, e) => {
onceEvent(cancellationToken.onCancellationRequested)(() => {
try {
child.kill();
} catch (err) {
// noop
}
e(new CancellationError());
});
});
result = Promise.race([result, cancellationPromise]);
}
try {
const [exitCode, stdout, stderr] = await result;
return { exitCode, stdout, stderr };
} finally {
dispose(disposables);
}
}
export interface IGitErrorData {
error?: Error;
message?: string;
stdout?: string;
stderr?: string;
exitCode?: number;
gitErrorCode?: string;
gitCommand?: string;
gitArgs?: string[];
}
export class GitError {
error?: Error;
message: string;
stdout?: string;
stderr?: string;
exitCode?: number;
gitErrorCode?: string;
gitCommand?: string;
gitArgs?: string[];
constructor(data: IGitErrorData) {
if (data.error) {
this.error = data.error;
this.message = data.error.message;
} else {
this.error = undefined;
this.message = '';
}
this.message = this.message || data.message || 'Git error';
this.stdout = data.stdout;
this.stderr = data.stderr;
this.exitCode = data.exitCode;
this.gitErrorCode = data.gitErrorCode;
this.gitCommand = data.gitCommand;
this.gitArgs = data.gitArgs;
}
toString(): string {
let result = this.message + ' ' + JSON.stringify({
exitCode: this.exitCode,
gitErrorCode: this.gitErrorCode,
gitCommand: this.gitCommand,
stdout: this.stdout,
stderr: this.stderr
}, null, 2);
if (this.error) {
result += (<any>this.error).stack;
}
return result;
}
}
export interface IGitOptions {
gitPath: string;
userAgent: string;
version: string;
env?: any;
}
function getGitErrorCode(stderr: string): string | undefined {
if (/Another git process seems to be running in this repository|If no other git process is currently running/.test(stderr)) {
return GitErrorCodes.RepositoryIsLocked;
} else if (/Authentication failed/i.test(stderr)) {
return GitErrorCodes.AuthenticationFailed;
} else if (/Not a git repository/i.test(stderr)) {
return GitErrorCodes.NotAGitRepository;
} else if (/bad config file/.test(stderr)) {
return GitErrorCodes.BadConfigFile;
} else if (/cannot make pipe for command substitution|cannot create standard input pipe/.test(stderr)) {
return GitErrorCodes.CantCreatePipe;
} else if (/Repository not found/.test(stderr)) {
return GitErrorCodes.RepositoryNotFound;
} else if (/unable to access/.test(stderr)) {
return GitErrorCodes.CantAccessRemote;
} else if (/branch '.+' is not fully merged/.test(stderr)) {
return GitErrorCodes.BranchNotFullyMerged;
} else if (/Couldn\'t find remote ref/.test(stderr)) {
return GitErrorCodes.NoRemoteReference;
} else if (/A branch named '.+' already exists/.test(stderr)) {
return GitErrorCodes.BranchAlreadyExists;
} else if (/'.+' is not a valid branch name/.test(stderr)) {
return GitErrorCodes.InvalidBranchName;
} else if (/Please,? commit your changes or stash them/.test(stderr)) {
return GitErrorCodes.DirtyWorkTree;
}
return undefined;
}
// https://github.com/microsoft/vscode/issues/89373
// https://github.com/git-for-windows/git/issues/2478
function sanitizePath(path: string): string {
return path.replace(/^([a-z]):\\/i, (_, letter) => `${letter.toUpperCase()}:\\`);
}
const COMMIT_FORMAT = '%H%n%aN%n%aE%n%at%n%ct%n%P%n%D%n%B';
/*export interface ICloneOptions { {{SQL CARBON EDIT}} moved to git.d.ts
readonly parentPath: string;
readonly progress: Progress<{ increment: number }>;
readonly recursive?: boolean;
readonly ref?: string;
}*/
export class Git {
readonly path: string;
readonly userAgent: string;
readonly version: string;
private env: any;
private commandsToLog: string[] = [];
private _onOutput = new EventEmitter();
get onOutput(): EventEmitter { return this._onOutput; }
constructor(options: IGitOptions) {
this.path = options.gitPath;
this.version = options.version;
this.userAgent = options.userAgent;
this.env = options.env || {};
const onConfigurationChanged = (e?: ConfigurationChangeEvent) => {
if (e !== undefined && !e.affectsConfiguration('git.commandsToLog')) {
return;
}
const config = workspace.getConfiguration('git');
this.commandsToLog = config.get<string[]>('commandsToLog', []);
};
workspace.onDidChangeConfiguration(onConfigurationChanged, this);
onConfigurationChanged();
}
compareGitVersionTo(version: string): -1 | 0 | 1 {
return Versions.compare(Versions.fromString(this.version), Versions.fromString(version));
}
open(repository: string, dotGit: { path: string; commonPath?: string }, logger: LogOutputChannel): Repository {
return new Repository(this, repository, dotGit, logger);
}
async init(repository: string, options: InitOptions = {}): Promise<void> {
const args = ['init'];
if (options.defaultBranch && options.defaultBranch !== '') {
args.push('-b', options.defaultBranch);
}
await this.exec(repository, args);
}
async clone(url: string, options: ICloneOptions, cancellationToken?: CancellationToken): Promise<string> {
const baseFolderName = decodeURI(url).replace(/[\/]+$/, '').replace(/^.*[\/\\]/, '').replace(/\.git$/, '') || 'repository';
let folderName = baseFolderName;
let folderPath = path.join(options.parentPath, folderName);
let count = 1;
while (count < 20 && await new Promise(c => exists(folderPath, c))) {
folderName = `${baseFolderName}-${count++}`;
folderPath = path.join(options.parentPath, folderName);
}
await mkdirp(options.parentPath);
const onSpawn = (child: cp.ChildProcess) => {
const decoder = new StringDecoder('utf8');
const lineStream = new byline.LineStream({ encoding: 'utf8' });
child.stderr!.on('data', (buffer: Buffer) => lineStream.write(decoder.write(buffer)));
let totalProgress = 0;
let previousProgress = 0;
lineStream.on('data', (line: string) => {
let match: RegExpExecArray | null = null;
if (match = /Counting objects:\s*(\d+)%/i.exec(line)) {
totalProgress = Math.floor(parseInt(match[1]) * 0.1);
} else if (match = /Compressing objects:\s*(\d+)%/i.exec(line)) {
totalProgress = 10 + Math.floor(parseInt(match[1]) * 0.1);
} else if (match = /Receiving objects:\s*(\d+)%/i.exec(line)) {
totalProgress = 20 + Math.floor(parseInt(match[1]) * 0.4);
} else if (match = /Resolving deltas:\s*(\d+)%/i.exec(line)) {
totalProgress = 60 + Math.floor(parseInt(match[1]) * 0.4);
}
if (totalProgress !== previousProgress) {
options.progress.report({ increment: totalProgress - previousProgress });
previousProgress = totalProgress;
}
});
};
try {
const command = ['clone', url.includes(' ') ? encodeURI(url) : url, folderPath, '--progress'];
if (options.recursive) {
command.push('--recursive');
}
if (options.ref) {
command.push('--branch', options.ref);
}
await this.exec(options.parentPath, command, {
cancellationToken,
env: { 'GIT_HTTP_USER_AGENT': this.userAgent },
onSpawn,
});
} catch (err) {
if (err.stderr) {
err.stderr = err.stderr.replace(/^Cloning.+$/m, '').trim();
err.stderr = err.stderr.replace(/^ERROR:\s+/, '').trim();
}
throw err;
}
return folderPath;
}
async getRepositoryRoot(repositoryPath: string): Promise<string> {
const result = await this.exec(repositoryPath, ['rev-parse', '--show-toplevel']);
// Keep trailing spaces which are part of the directory name
const repoPath = path.normalize(result.stdout.trimLeft().replace(/[\r\n]+$/, ''));
if (isWindows) {
// On Git 2.25+ if you call `rev-parse --show-toplevel` on a mapped drive, instead of getting the mapped
// drive path back, you get the UNC path for the mapped drive. So we will try to normalize it back to the
// mapped drive path, if possible
const repoUri = Uri.file(repoPath);
const pathUri = Uri.file(repositoryPath);
if (repoUri.authority.length !== 0 && pathUri.authority.length === 0) {
// eslint-disable-next-line local/code-no-look-behind-regex
const match = /(?<=^\/?)([a-zA-Z])(?=:\/)/.exec(pathUri.path);
if (match !== null) {
const [, letter] = match;
try {
const networkPath = await new Promise<string | undefined>(resolve =>
realpath.native(`${letter}:\\`, { encoding: 'utf8' }, (err, resolvedPath) =>
resolve(err !== null ? undefined : resolvedPath),
),
);
if (networkPath !== undefined) {
// If the repository is at the root of the mapped drive then we
// have to append `\` (ex: D:\) otherwise the path is not valid.
const isDriveRoot = pathEquals(repoUri.fsPath, networkPath);
return path.normalize(
repoUri.fsPath.replace(
networkPath,
`${letter.toLowerCase()}:${isDriveRoot || networkPath.endsWith('\\') ? '\\' : ''}`
),
);
}
} catch { }
}
return path.normalize(pathUri.fsPath);
}
}
return repoPath;
}
async getRepositoryDotGit(repositoryPath: string): Promise<{ path: string; commonPath?: string }> {
const result = await this.exec(repositoryPath, ['rev-parse', '--git-dir', '--git-common-dir']);
let [dotGitPath, commonDotGitPath] = result.stdout.split('\n').map(r => r.trim());
if (!path.isAbsolute(dotGitPath)) {
dotGitPath = path.join(repositoryPath, dotGitPath);
}
dotGitPath = path.normalize(dotGitPath);
if (commonDotGitPath) {
if (!path.isAbsolute(commonDotGitPath)) {
commonDotGitPath = path.join(repositoryPath, commonDotGitPath);
}
commonDotGitPath = path.normalize(commonDotGitPath);
return { path: dotGitPath, commonPath: commonDotGitPath !== dotGitPath ? commonDotGitPath : undefined };
}
return { path: dotGitPath };
}
async exec(cwd: string, args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
options = assign({ cwd }, options || {});
return await this._exec(args, options);
}
async exec2(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
return await this._exec(args, options);
}
stream(cwd: string, args: string[], options: SpawnOptions = {}): cp.ChildProcess {
options = assign({ cwd }, options || {});
const child = this.spawn(args, options);
if (options.log !== false) {
const startTime = Date.now();
child.on('exit', (_) => {
this.log(`> git ${args.join(' ')} [${Date.now() - startTime}ms]${child.killed ? ' (cancelled)' : ''}\n`);
});
}
return child;
}
private async _exec(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
const child = this.spawn(args, options);
options.onSpawn?.(child);
if (options.input) {
child.stdin!.end(options.input, 'utf8');
}
const startExec = Date.now();
let bufferResult: IExecutionResult<Buffer>;
try {
bufferResult = await exec(child, options.cancellationToken);
} catch (ex) {
if (ex instanceof CancellationError) {
this.log(`> git ${args.join(' ')} [${Date.now() - startExec}ms] (cancelled)\n`);
}
throw ex;
}
if (options.log !== false) {
// command
this.log(`> git ${args.join(' ')} [${Date.now() - startExec}ms]\n`);
// stdout
if (bufferResult.stdout.length > 0 && args.find(a => this.commandsToLog.includes(a))) {
this.log(`${bufferResult.stdout}\n`);
}
// stderr
if (bufferResult.stderr.length > 0) {
this.log(`${bufferResult.stderr}\n`);
}
}
let encoding = options.encoding || 'utf8';
encoding = iconv.encodingExists(encoding) ? encoding : 'utf8';
const result: IExecutionResult<string> = {
exitCode: bufferResult.exitCode,
stdout: iconv.decode(bufferResult.stdout, encoding),
stderr: bufferResult.stderr
};
if (bufferResult.exitCode) {
return Promise.reject<IExecutionResult<string>>(new GitError({
message: 'Failed to execute git',
stdout: result.stdout,
stderr: result.stderr,
exitCode: result.exitCode,
gitErrorCode: getGitErrorCode(result.stderr),
gitCommand: args[0],
gitArgs: args
}));
}
return result;
}
spawn(args: string[], options: SpawnOptions = {}): cp.ChildProcess {
if (!this.path) {
throw new Error('git could not be found in the system.');
}
if (!options) {
options = {};
}
if (!options.stdio && !options.input) {
options.stdio = ['ignore', null, null]; // Unless provided, ignore stdin and leave default streams for stdout and stderr
}
options.env = assign({}, process.env, this.env, options.env || {}, {
VSCODE_GIT_COMMAND: args[0],
LC_ALL: 'en_US.UTF-8',
LANG: 'en_US.UTF-8',
GIT_PAGER: 'cat'
});
const cwd = this.getCwd(options);
if (cwd) {
options.cwd = sanitizePath(cwd);
}
return cp.spawn(this.path, args, options);
}
private getCwd(options: SpawnOptions): string | undefined {
const cwd = options.cwd;
if (typeof cwd === 'undefined' || typeof cwd === 'string') {
return cwd;
}
if (cwd.protocol === 'file:') {
return fileURLToPath(cwd);
}
return undefined;
}
private log(output: string): void {
this._onOutput.emit('log', output);
}
async mergeFile(options: { input1Path: string; input2Path: string; basePath: string; diff3?: boolean }): Promise<string> {
const args = ['merge-file', '-p', options.input1Path, options.basePath, options.input2Path];
if (options.diff3) {
args.push('--diff3');
} else {
args.push('--no-diff3');
}
try {
const result = await this.exec(os.homedir(), args);
return result.stdout;
} catch (err) {
if (typeof err.stdout === 'string') {
// The merge had conflicts, stdout still contains the merged result (with conflict markers)
return err.stdout;
} else {
throw err;
}
}
}
async addSafeDirectory(repositoryPath: string): Promise<void> {
await this.exec(os.homedir(), ['config', '--global', '--add', 'safe.directory', repositoryPath]);
return;
}
}
export interface Commit {
hash: string;
message: string;
parents: string[];
authorDate?: Date;
authorName?: string;
authorEmail?: string;
commitDate?: Date;
refNames: string[];
}
interface GitConfigSection {
name: string;
subSectionName?: string;
properties: { [key: string]: string };
}
class GitConfigParser {
private static readonly _lineSeparator = /\r?\n/;
private static readonly _propertyRegex = /^\s*(\w+)\s*=\s*"?([^"]+)"?$/;
private static readonly _sectionRegex = /^\s*\[\s*([^\]]+?)\s*(\"[^"]+\")*\]\s*$/;
static parse(raw: string): GitConfigSection[] {
const config: { sections: GitConfigSection[] } = { sections: [] };
let section: GitConfigSection = { name: 'DEFAULT', properties: {} };
const addSection = (section?: GitConfigSection) => {
if (!section) { return; }
config.sections.push(section);
};
for (const line of raw.split(GitConfigParser._lineSeparator)) {
// Section
const sectionMatch = line.match(GitConfigParser._sectionRegex);
if (sectionMatch?.length === 3) {
addSection(section);
section = { name: sectionMatch[1], subSectionName: sectionMatch[2]?.replaceAll('"', ''), properties: {} };
continue;
}
// Property
const propertyMatch = line.match(GitConfigParser._propertyRegex);
if (propertyMatch?.length === 3 && !Object.keys(section.properties).includes(propertyMatch[1])) {
section.properties[propertyMatch[1]] = propertyMatch[2];
}
}
addSection(section);
return config.sections;
}
}
export class GitStatusParser {
private lastRaw = '';
private result: IFileStatus[] = [];
get status(): IFileStatus[] {
return this.result;
}
update(raw: string): void {
let i = 0;
let nextI: number | undefined;
raw = this.lastRaw + raw;
while ((nextI = this.parseEntry(raw, i)) !== undefined) {
i = nextI;
}
this.lastRaw = raw.substr(i);
}
private parseEntry(raw: string, i: number): number | undefined {
if (i + 4 >= raw.length) {
return;
}
let lastIndex: number;
const entry: IFileStatus = {
x: raw.charAt(i++),
y: raw.charAt(i++),
rename: undefined,
path: ''
};
// space
i++;
if (entry.x === 'R' || entry.y === 'R' || entry.x === 'C') {
lastIndex = raw.indexOf('\0', i);
if (lastIndex === -1) {
return;
}
entry.rename = raw.substring(i, lastIndex);
i = lastIndex + 1;
}
lastIndex = raw.indexOf('\0', i);
if (lastIndex === -1) {
return;
}
entry.path = raw.substring(i, lastIndex);
// If path ends with slash, it must be a nested git repo
if (entry.path[entry.path.length - 1] !== '/') {
this.result.push(entry);
}
return lastIndex + 1;
}
}
export interface Submodule {
name: string;
path: string;
url: string;
}
export function parseGitmodules(raw: string): Submodule[] {
const result: Submodule[] = [];
for (const submoduleSection of GitConfigParser.parse(raw).filter(s => s.name === 'submodule')) {
if (submoduleSection.subSectionName && submoduleSection.properties['path'] && submoduleSection.properties['url']) {
result.push({
name: submoduleSection.subSectionName,
path: submoduleSection.properties['path'],
url: submoduleSection.properties['url']
});
}
}
return result;
}
export function parseGitRemotes(raw: string): MutableRemote[] {
const remotes: MutableRemote[] = [];
for (const remoteSection of GitConfigParser.parse(raw).filter(s => s.name === 'remote')) {
if (remoteSection.subSectionName) {
remotes.push({
name: remoteSection.subSectionName,
fetchUrl: remoteSection.properties['url'],
pushUrl: remoteSection.properties['pushurl'] ?? remoteSection.properties['url'],
isReadOnly: false
});
}
}
return remotes;
}
const commitRegex = /([0-9a-f]{40})\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)\n(.*)(?:\n([^]*?))?(?:\x00)/gm;
export function parseGitCommits(data: string): Commit[] {
const commits: Commit[] = [];
let ref;
let authorName;
let authorEmail;
let authorDate;
let commitDate;
let parents;
let refNames;
let message;
let match;
do {
match = commitRegex.exec(data);
if (match === null) {
break;
}
[, ref, authorName, authorEmail, authorDate, commitDate, parents, refNames, message] = match;
if (message[message.length - 1] === '\n') {
message = message.substr(0, message.length - 1);
}
// Stop excessive memory usage by using substr -- https://bugs.chromium.org/p/v8/issues/detail?id=2869
commits.push({
hash: ` ${ref}`.substr(1),
message: ` ${message}`.substr(1),
parents: parents ? parents.split(' ') : [],
authorDate: new Date(Number(authorDate) * 1000),
authorName: ` ${authorName}`.substr(1),
authorEmail: ` ${authorEmail}`.substr(1),
commitDate: new Date(Number(commitDate) * 1000),
refNames: refNames.split(',').map(s => s.trim())
});
} while (true);
return commits;
}
interface LsTreeElement {
mode: string;
type: string;
object: string;
size: string;
file: string;
}
export function parseLsTree(raw: string): LsTreeElement[] {
return raw.split('\n')
.filter(l => !!l)
.map(line => /^(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line)!)
.filter(m => !!m)
.map(([, mode, type, object, size, file]) => ({ mode, type, object, size, file }));
}
interface LsFilesElement {
mode: string;
object: string;
stage: string;
file: string;
}
export function parseLsFiles(raw: string): LsFilesElement[] {
return raw.split('\n')
.filter(l => !!l)
.map(line => /^(\S+)\s+(\S+)\s+(\S+)\s+(.*)$/.exec(line)!)
.filter(m => !!m)
.map(([, mode, object, stage, file]) => ({ mode, object, stage, file }));
}
export interface PullOptions {
unshallow?: boolean;
tags?: boolean;
readonly cancellationToken?: CancellationToken;
}
export class Repository {
constructor(
private _git: Git,
private repositoryRoot: string,
readonly dotGit: { path: string; commonPath?: string },
private logger: LogOutputChannel
) { }
get git(): Git {
return this._git;
}
get root(): string {
return this.repositoryRoot;
}
async exec(args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
return await this.git.exec(this.repositoryRoot, args, options);
}
stream(args: string[], options: SpawnOptions = {}): cp.ChildProcess {
return this.git.stream(this.repositoryRoot, args, options);
}
spawn(args: string[], options: SpawnOptions = {}): cp.ChildProcess {
return this.git.spawn(args, options);
}
async config(scope: string, key: string, value: any = null, options: SpawnOptions = {}): Promise<string> {
const args = ['config'];
if (scope) {
args.push('--' + scope);
}
args.push(key);
if (value) {
args.push(value);
}
const result = await this.exec(args, options);
return result.stdout.trim();
}
async getConfigs(scope: string): Promise<{ key: string; value: string }[]> {
const args = ['config'];
if (scope) {
args.push('--' + scope);
}
args.push('-l');
const result = await this.exec(args);
const lines = result.stdout.trim().split(/\r|\r\n|\n/);
return lines.map(entry => {
const equalsIndex = entry.indexOf('=');
return { key: entry.substr(0, equalsIndex), value: entry.substr(equalsIndex + 1) };
});
}
async log(options?: LogOptions): Promise<Commit[]> {
const maxEntries = options?.maxEntries ?? 32;
const args = ['log', `-n${maxEntries}`, `--format=${COMMIT_FORMAT}`, '-z', '--'];
if (options?.path) {
args.push(options.path);
}
const result = await this.exec(args);
if (result.exitCode) {
// An empty repo
return [];
}
return parseGitCommits(result.stdout);
}
async logFile(uri: Uri, options?: LogFileOptions): Promise<Commit[]> {
const args = ['log', `--format=${COMMIT_FORMAT}`, '-z'];
if (options?.maxEntries && !options?.reverse) {
args.push(`-n${options.maxEntries}`);
}
if (options?.hash) {
// If we are reversing, we must add a range (with HEAD) because we are using --ancestry-path for better reverse walking
if (options?.reverse) {
args.push('--reverse', '--ancestry-path', `${options.hash}..HEAD`);
} else {
args.push(options.hash);
}
}
if (options?.sortByAuthorDate) {
args.push('--author-date-order');
}
args.push('--', uri.fsPath);
const result = await this.exec(args);
if (result.exitCode) {
// No file history, e.g. a new file or untracked
return [];
}
return parseGitCommits(result.stdout);
}
async bufferString(object: string, encoding: string = 'utf8', autoGuessEncoding = false): Promise<string> {
const stdout = await this.buffer(object);
if (autoGuessEncoding) {
encoding = detectEncoding(stdout) || encoding;
}
encoding = iconv.encodingExists(encoding) ? encoding : 'utf8';
return iconv.decode(stdout, encoding);
}
async buffer(object: string): Promise<Buffer> {
const child = this.stream(['show', '--textconv', object]);
if (!child.stdout) {
return Promise.reject<Buffer>('Can\'t open file from git');
}
const { exitCode, stdout, stderr } = await exec(child);
if (exitCode) {
const err = new GitError({
message: 'Could not show object.',
exitCode
});
if (/exists on disk, but not in/.test(stderr)) {
err.gitErrorCode = GitErrorCodes.WrongCase;
}
return Promise.reject<Buffer>(err);
}
return stdout;
}
async getObjectDetails(treeish: string, path: string): Promise<{ mode: string; object: string; size: number }> {
if (!treeish) { // index
const elements = await this.lsfiles(path);
if (elements.length === 0) {
throw new GitError({ message: 'Path not known by git', gitErrorCode: GitErrorCodes.UnknownPath });
}
const { mode, object } = elements[0];
const catFile = await this.exec(['cat-file', '-s', object]);
const size = parseInt(catFile.stdout);
return { mode, object, size };
}
const elements = await this.lstree(treeish, path);
if (elements.length === 0) {
throw new GitError({ message: 'Path not known by git', gitErrorCode: GitErrorCodes.UnknownPath });
}
const { mode, object, size } = elements[0];
return { mode, object, size: parseInt(size) };
}
async lstree(treeish: string, path: string): Promise<LsTreeElement[]> {
const { stdout } = await this.exec(['ls-tree', '-l', treeish, '--', sanitizePath(path)]);
return parseLsTree(stdout);
}
async lsfiles(path: string): Promise<LsFilesElement[]> {
const { stdout } = await this.exec(['ls-files', '--stage', '--', sanitizePath(path)]);
return parseLsFiles(stdout);
}
async getGitRelativePath(ref: string, relativePath: string): Promise<string> {
const relativePathLowercase = relativePath.toLowerCase();
const dirname = path.posix.dirname(relativePath) + '/';
const elements: { file: string }[] = ref ? await this.lstree(ref, dirname) : await this.lsfiles(dirname);
const element = elements.filter(file => file.file.toLowerCase() === relativePathLowercase)[0];
if (!element) {
throw new GitError({ message: 'Git relative path not found.' });
}
return element.file;
}
async detectObjectType(object: string): Promise<{ mimetype: string; encoding?: string }> {
const child = await this.stream(['show', '--textconv', object]);
const buffer = await readBytes(child.stdout!, 4100);
try {
child.kill();
} catch (err) {
// noop
}
const encoding = detectUnicodeEncoding(buffer);
let isText = true;
if (encoding !== Encoding.UTF16be && encoding !== Encoding.UTF16le) {
for (let i = 0; i < buffer.length; i++) {
if (buffer.readInt8(i) === 0) {
isText = false;
break;
}
}
}
if (!isText) {
const result = await filetype.fromBuffer(buffer);
if (!result) {
return { mimetype: 'application/octet-stream' };
} else {
return { mimetype: result.mime };
}
}
if (encoding) {
return { mimetype: 'text/plain', encoding };
} else {
// TODO@JOAO: read the setting OUTSIDE!
return { mimetype: 'text/plain' };
}
}
async apply(patch: string, reverse?: boolean): Promise<void> {
const args = ['apply', patch];
if (reverse) {
args.push('-R');
}
try {
await this.exec(args);
} catch (err) {
if (/patch does not apply/.test(err.stderr)) {
err.gitErrorCode = GitErrorCodes.PatchDoesNotApply;
}
throw err;
}
}
async diff(cached = false): Promise<string> {
const args = ['diff'];
if (cached) {
args.push('--cached');
}
const result = await this.exec(args);
return result.stdout;
}
diffWithHEAD(): Promise<Change[]>;
diffWithHEAD(path: string): Promise<string>;
diffWithHEAD(path?: string | undefined): Promise<string | Change[]>;
async diffWithHEAD(path?: string | undefined): Promise<string | Change[]> {
if (!path) {
return await this.diffFiles(false);
}
const args = ['diff', '--', sanitizePath(path)];
const result = await this.exec(args);
return result.stdout;
}
diffWith(ref: string): Promise<Change[]>;
diffWith(ref: string, path: string): Promise<string>;
diffWith(ref: string, path?: string | undefined): Promise<string | Change[]>;
async diffWith(ref: string, path?: string): Promise<string | Change[]> {
if (!path) {
return await this.diffFiles(false, ref);
}
const args = ['diff', ref, '--', sanitizePath(path)];
const result = await this.exec(args);
return result.stdout;
}
diffIndexWithHEAD(): Promise<Change[]>;
diffIndexWithHEAD(path: string): Promise<string>;
diffIndexWithHEAD(path?: string | undefined): Promise<string | Change[]>;
async diffIndexWithHEAD(path?: string): Promise<string | Change[]> {
if (!path) {
return await this.diffFiles(true);
}
const args = ['diff', '--cached', '--', sanitizePath(path)];
const result = await this.exec(args);
return result.stdout;
}
diffIndexWith(ref: string): Promise<Change[]>;
diffIndexWith(ref: string, path: string): Promise<string>;
diffIndexWith(ref: string, path?: string | undefined): Promise<string | Change[]>;
async diffIndexWith(ref: string, path?: string): Promise<string | Change[]> {
if (!path) {
return await this.diffFiles(true, ref);
}
const args = ['diff', '--cached', ref, '--', sanitizePath(path)];
const result = await this.exec(args);
return result.stdout;
}
async diffBlobs(object1: string, object2: string): Promise<string> {
const args = ['diff', object1, object2];
const result = await this.exec(args);
return result.stdout;
}
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[]>;
async diffBetween(ref1: string, ref2: string, path?: string): Promise<string | Change[]> {
const range = `${ref1}...${ref2}`;
if (!path) {
return await this.diffFiles(false, range);
}
const args = ['diff', range, '--', sanitizePath(path)];
const result = await this.exec(args);
return result.stdout.trim();
}
private async diffFiles(cached: boolean, ref?: string): Promise<Change[]> {
const args = ['diff', '--name-status', '-z', '--diff-filter=ADMR'];
if (cached) {
args.push('--cached');
}
if (ref) {
args.push(ref);
}
const gitResult = await this.exec(args);
if (gitResult.exitCode) {
return [];
}
const entries = gitResult.stdout.split('\x00');
let index = 0;
const result: Change[] = [];
entriesLoop:
while (index < entries.length - 1) {
const change = entries[index++];
const resourcePath = entries[index++];
if (!change || !resourcePath) {
break;
}
const originalUri = Uri.file(path.isAbsolute(resourcePath) ? resourcePath : path.join(this.repositoryRoot, resourcePath));
let status: Status = Status.UNTRACKED;
// Copy or Rename status comes with a number, e.g. 'R100'. We don't need the number, so we use only first character of the status.
switch (change[0]) {
case 'M':
status = Status.MODIFIED;
break;
case 'A':
status = Status.INDEX_ADDED;
break;
case 'D':
status = Status.DELETED;
break;
// Rename contains two paths, the second one is what the file is renamed/copied to.
case 'R': {
if (index >= entries.length) {
break;
}
const newPath = entries[index++];
if (!newPath) {
break;
}
const uri = Uri.file(path.isAbsolute(newPath) ? newPath : path.join(this.repositoryRoot, newPath));
result.push({
uri,
renameUri: uri,
originalUri,
status: Status.INDEX_RENAMED
});
continue;
}
default:
// Unknown status
break entriesLoop;
}
result.push({
status,
originalUri,
uri: originalUri,
renameUri: originalUri,
});
}
return result;
}
async getMergeBase(ref1: string, ref2: string): Promise<string> {
const args = ['merge-base', ref1, ref2];
const result = await this.exec(args);
return result.stdout.trim();
}
async hashObject(data: string): Promise<string> {
const args = ['hash-object', '-w', '--stdin'];
const result = await this.exec(args, { input: data });
return result.stdout.trim();
}
async add(paths: string[], opts?: { update?: boolean }): Promise<void> {
const args = ['add'];
if (opts && opts.update) {
args.push('-u');
} else {
args.push('-A');
}
if (paths && paths.length) {
for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) {
await this.exec([...args, '--', ...chunk]);
}
} else {
await this.exec([...args, '--', '.']);
}
}
async rm(paths: string[]): Promise<void> {
const args = ['rm', '--'];
if (!paths || !paths.length) {
return;
}
args.push(...paths.map(sanitizePath));
await this.exec(args);
}
async stage(path: string, data: string): Promise<void> {
const child = this.stream(['hash-object', '--stdin', '-w', '--path', sanitizePath(path)], { stdio: [null, null, null] });
child.stdin!.end(data, 'utf8');
const { exitCode, stdout } = await exec(child);
const hash = stdout.toString('utf8');
if (exitCode) {
throw new GitError({
message: 'Could not hash object.',
exitCode: exitCode
});
}
const treeish = await this.getCommit('HEAD').then(() => 'HEAD', () => '');
let mode: string;
let add: string = '';
try {
const details = await this.getObjectDetails(treeish, path);
mode = details.mode;
} catch (err) {
if (err.gitErrorCode !== GitErrorCodes.UnknownPath) {
throw err;
}
mode = '100644';
add = '--add';
}
await this.exec(['update-index', add, '--cacheinfo', mode, hash, path]);
}
async checkout(treeish: string, paths: string[], opts: { track?: boolean; detached?: boolean } = Object.create(null)): Promise<void> {
const args = ['checkout', '-q'];
if (opts.track) {
args.push('--track');
}
if (opts.detached) {
args.push('--detach');
}
if (treeish) {
args.push(treeish);
}
try {
if (paths && paths.length > 0) {
for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) {
await this.exec([...args, '--', ...chunk]);
}
} else {
await this.exec(args);
}
} catch (err) {
if (/Please,? commit your changes or stash them/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.DirtyWorkTree;
err.gitTreeish = treeish;
} else if (/You are on a branch yet to be born/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.BranchNotYetBorn;
}
throw err;
}
}
async commit(message: string | undefined, opts: CommitOptions = Object.create(null)): Promise<void> {
const args = ['commit', '--quiet'];
const options: SpawnOptions = {};
if (message) {
options.input = message;
args.push('--allow-empty-message', '--file', '-');
}
if (opts.verbose) {
args.push('--verbose');
}
if (opts.all) {
args.push('--all');
}
if (opts.amend) {
args.push('--amend');
}
if (!opts.useEditor) {
if (!message) {
if (opts.amend) {
args.push('--no-edit');
} else {
options.input = '';
args.push('--file', '-');
}
}
args.push('--allow-empty-message');
}
if (opts.signoff) {
args.push('--signoff');
}
if (opts.signCommit) {
args.push('-S');
}
if (opts.empty) {
args.push('--allow-empty');
}
if (opts.noVerify) {
args.push('--no-verify');
}
if (opts.requireUserConfig ?? true) {
// Stops git from guessing at user/email
args.splice(0, 0, '-c', 'user.useConfigOnly=true');
}
try {
await this.exec(args, options);
} catch (commitErr) {
await this.handleCommitError(commitErr);
}
}
async rebaseAbort(): Promise<void> {
await this.exec(['rebase', '--abort']);
}
async rebaseContinue(): Promise<void> {
const args = ['rebase', '--continue'];
try {
await this.exec(args, { env: { GIT_EDITOR: 'true' } });
} catch (commitErr) {
await this.handleCommitError(commitErr);
}
}
private async handleCommitError(commitErr: any): Promise<void> {
if (/not possible because you have unmerged files/.test(commitErr.stderr || '')) {
commitErr.gitErrorCode = GitErrorCodes.UnmergedChanges;
throw commitErr;
} else if (/Aborting commit due to empty commit message/.test(commitErr.stderr || '')) {
commitErr.gitErrorCode = GitErrorCodes.EmptyCommitMessage;
throw commitErr;
}
try {
await this.exec(['config', '--get-all', 'user.name']);
} catch (err) {
err.gitErrorCode = GitErrorCodes.NoUserNameConfigured;
throw err;
}
try {
await this.exec(['config', '--get-all', 'user.email']);
} catch (err) {
err.gitErrorCode = GitErrorCodes.NoUserEmailConfigured;
throw err;
}
throw commitErr;
}
async branch(name: string, checkout: boolean, ref?: string): Promise<void> {
const args = checkout ? ['checkout', '-q', '-b', name, '--no-track'] : ['branch', '-q', name];
if (ref) {
args.push(ref);
}
await this.exec(args);
}
async deleteBranch(name: string, force?: boolean): Promise<void> {
const args = ['branch', force ? '-D' : '-d', name];
await this.exec(args);
}
async renameBranch(name: string): Promise<void> {
const args = ['branch', '-m', name];
await this.exec(args);
}
async move(from: string, to: string): Promise<void> {
const args = ['mv', from, to];
await this.exec(args);
}
async setBranchUpstream(name: string, upstream: string): Promise<void> {
const args = ['branch', '--set-upstream-to', upstream, name];
await this.exec(args);
}
async deleteRef(ref: string): Promise<void> {
const args = ['update-ref', '-d', ref];
await this.exec(args);
}
async merge(ref: string): Promise<void> {
const args = ['merge', ref];
try {
await this.exec(args);
} catch (err) {
if (/^CONFLICT /m.test(err.stdout || '')) {
err.gitErrorCode = GitErrorCodes.Conflict;
}
throw err;
}
}
async mergeAbort(): Promise<void> {
await this.exec(['merge', '--abort']);
}
async tag(name: string, message?: string): Promise<void> {
let args = ['tag'];
if (message) {
args = [...args, '-a', name, '-m', message];
} else {
args = [...args, name];
}
await this.exec(args);
}
async deleteTag(name: string): Promise<void> {
const args = ['tag', '-d', name];
await this.exec(args);
}
async deleteRemoteTag(remoteName: string, tagName: string): Promise<void> {
const args = ['push', '--delete', remoteName, tagName];
await this.exec(args);
}
async clean(paths: string[]): Promise<void> {
const pathsByGroup = groupBy(paths.map(sanitizePath), p => path.dirname(p));
const groups = Object.keys(pathsByGroup).map(k => pathsByGroup[k]);
const limiter = new Limiter(5);
const promises: Promise<any>[] = [];
const args = ['clean', '-f', '-q'];
for (const paths of groups) {
for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) {
promises.push(limiter.queue(() => this.exec([...args, '--', ...chunk])));
}
}
await Promise.all(promises);
}
async undo(): Promise<void> {
await this.exec(['clean', '-fd']);
try {
await this.exec(['checkout', '--', '.']);
} catch (err) {
if (/did not match any file\(s\) known to git\./.test(err.stderr || '')) {
return;
}
throw err;
}
}
async reset(treeish: string, hard: boolean = false): Promise<void> {
const args = ['reset', hard ? '--hard' : '--soft', treeish];
await this.exec(args);
}
async revert(treeish: string, paths: string[]): Promise<void> {
const result = await this.exec(['branch']);
let args: string[];
// In case there are no branches, we must use rm --cached
if (!result.stdout) {
args = ['rm', '--cached', '-r'];
} else {
args = ['reset', '-q', treeish];
}
try {
if (paths && paths.length > 0) {
for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) {
await this.exec([...args, '--', ...chunk]);
}
} else {
await this.exec([...args, '--', '.']);
}
} catch (err) {
// In case there are merge conflicts to be resolved, git reset will output
// some "needs merge" data. We try to get around that.
if (/([^:]+: needs merge\n)+/m.test(err.stdout || '')) {
return;
}
throw err;
}
}
async addRemote(name: string, url: string): Promise<void> {
const args = ['remote', 'add', name, url];
await this.exec(args);
}
async removeRemote(name: string): Promise<void> {
const args = ['remote', 'remove', name];
await this.exec(args);
}
async renameRemote(name: string, newName: string): Promise<void> {
const args = ['remote', 'rename', name, newName];
await this.exec(args);
}
async fetch(options: { remote?: string; ref?: string; all?: boolean; prune?: boolean; depth?: number; silent?: boolean; readonly cancellationToken?: CancellationToken } = {}): Promise<void> {
const args = ['fetch'];
const spawnOptions: SpawnOptions = {
cancellationToken: options.cancellationToken,
env: { 'GIT_HTTP_USER_AGENT': this.git.userAgent }
};
if (options.remote) {
args.push(options.remote);
if (options.ref) {
args.push(options.ref);
}
} else if (options.all) {
args.push('--all');
}
if (options.prune) {
args.push('--prune');
}
if (typeof options.depth === 'number') {
args.push(`--depth=${options.depth}`);
}
if (options.silent) {
spawnOptions.env!['VSCODE_GIT_FETCH_SILENT'] = 'true';
}
try {
await this.exec(args, spawnOptions);
} catch (err) {
if (/No remote repository specified\./.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.NoRemoteRepositorySpecified;
} else if (/Could not read from remote repository/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
} else if (/! \[rejected\].*\(non-fast-forward\)/m.test(err.stderr || '')) {
// The local branch has outgoing changes and it cannot be fast-forwarded.
err.gitErrorCode = GitErrorCodes.BranchFastForwardRejected;
}
throw err;
}
}
async fetchTags(options: { remote: string; tags: string[]; force?: boolean }): Promise<void> {
const args = ['fetch'];
const spawnOptions: SpawnOptions = {
env: { 'GIT_HTTP_USER_AGENT': this.git.userAgent }
};
args.push(options.remote);
for (const tag of options.tags) {
args.push(`refs/tags/${tag}:refs/tags/${tag}`);
}
if (options.force) {
args.push('--force');
}
await this.exec(args, spawnOptions);
}
async pull(rebase?: boolean, remote?: string, branch?: string, options: PullOptions = {}): Promise<void> {
const args = ['pull'];
if (options.tags) {
args.push('--tags');
}
if (options.unshallow) {
args.push('--unshallow');
}
if (rebase) {
args.push('-r');
}
if (remote && branch) {
args.push(remote);
args.push(branch);
}
try {
await this.exec(args, {
cancellationToken: options.cancellationToken,
env: { 'GIT_HTTP_USER_AGENT': this.git.userAgent }
});
} catch (err) {
if (/^CONFLICT \([^)]+\): \b/m.test(err.stdout || '')) {
err.gitErrorCode = GitErrorCodes.Conflict;
} else if (/Please tell me who you are\./.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.NoUserNameConfigured;
} else if (/Could not read from remote repository/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
} else if (/Pull(?:ing)? is not possible because you have unmerged files|Cannot pull with rebase: You have unstaged changes|Your local changes to the following files would be overwritten|Please, commit your changes before you can merge/i.test(err.stderr)) {
err.stderr = err.stderr.replace(/Cannot pull with rebase: You have unstaged changes/i, 'Cannot pull with rebase, you have unstaged changes');
err.gitErrorCode = GitErrorCodes.DirtyWorkTree;
} else if (/cannot lock ref|unable to update local ref/i.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.CantLockRef;
} else if (/cannot rebase onto multiple branches/i.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.CantRebaseMultipleBranches;
} else if (/! \[rejected\].*\(would clobber existing tag\)/m.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.TagConflict;
}
throw err;
}
}
async rebase(branch: string, options: PullOptions = {}): Promise<void> {
const args = ['rebase'];
args.push(branch);
try {
await this.exec(args, options);
} catch (err) {
if (/^CONFLICT \([^)]+\): \b/m.test(err.stdout || '')) {
err.gitErrorCode = GitErrorCodes.Conflict;
} else if (/cannot rebase onto multiple branches/i.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.CantRebaseMultipleBranches;
}
throw err;
}
}
async push(remote?: string, name?: string, setUpstream: boolean = false, followTags = false, forcePushMode?: ForcePushMode, tags = false): Promise<void> {
const args = ['push'];
if (forcePushMode === ForcePushMode.ForceWithLease) {
args.push('--force-with-lease');
} else if (forcePushMode === ForcePushMode.Force) {
args.push('--force');
}
if (setUpstream) {
args.push('-u');
}
if (followTags) {
args.push('--follow-tags');
}
if (tags) {
args.push('--tags');
}
if (remote) {
args.push(remote);
}
if (name) {
args.push(name);
}
try {
await this.exec(args, { env: { 'GIT_HTTP_USER_AGENT': this.git.userAgent } });
} catch (err) {
if (/^error: failed to push some refs to\b/m.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.PushRejected;
} else if (/Permission.*denied/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.PermissionDenied;
} else if (/Could not read from remote repository/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.RemoteConnectionError;
} else if (/^fatal: The current branch .* has no upstream branch/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.NoUpstreamBranch;
}
throw err;
}
}
async cherryPick(commitHash: string): Promise<void> {
const args = ['cherry-pick', commitHash];
await this.exec(args);
}
async blame(path: string): Promise<string> {
try {
const args = ['blame', sanitizePath(path)];
const result = await this.exec(args);
return result.stdout.trim();
} catch (err) {
if (/^fatal: no such path/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.NoPathFound;
}
throw err;
}
}
async createStash(message?: string, includeUntracked?: boolean, staged?: boolean): Promise<void> {
try {
const args = ['stash', 'push'];
if (includeUntracked) {
args.push('-u');
}
if (staged) {
args.push('-S');
}
if (message) {
args.push('-m', message);
}
await this.exec(args);
} catch (err) {
if (/No local changes to save/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.NoLocalChanges;
}
throw err;
}
}
async popStash(index?: number): Promise<void> {
const args = ['stash', 'pop'];
await this.popOrApplyStash(args, index);
}
async applyStash(index?: number): Promise<void> {
const args = ['stash', 'apply'];
await this.popOrApplyStash(args, index);
}
private async popOrApplyStash(args: string[], index?: number): Promise<void> {
try {
if (typeof index === 'number') {
args.push(`stash@{${index}}`);
}
await this.exec(args);
} catch (err) {
if (/No stash found/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.NoStashFound;
} else if (/error: Your local changes to the following files would be overwritten/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.LocalChangesOverwritten;
} else if (/^CONFLICT/m.test(err.stdout || '')) {
err.gitErrorCode = GitErrorCodes.StashConflict;
}
throw err;
}
}
async dropStash(index?: number): Promise<void> {
const args = ['stash'];
if (typeof index === 'number') {
args.push('drop');
args.push(`stash@{${index}}`);
} else {
args.push('clear');
}
try {
await this.exec(args);
} catch (err) {
if (/No stash found/.test(err.stderr || '')) {
err.gitErrorCode = GitErrorCodes.NoStashFound;
}
throw err;
}
}
async getStatus(opts?: { limit?: number; ignoreSubmodules?: boolean; similarityThreshold?: number; untrackedChanges?: 'mixed' | 'separate' | 'hidden'; cancellationToken?: CancellationToken }): Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean }> {
if (opts?.cancellationToken && opts?.cancellationToken.isCancellationRequested) {
throw new CancellationError();
}
const disposables: IDisposable[] = [];
const env = { GIT_OPTIONAL_LOCKS: '0' };
const args = ['status', '-z'];
if (opts?.untrackedChanges === 'hidden') {
args.push('-uno');
} else {
args.push('-uall');
}
if (opts?.ignoreSubmodules) {
args.push('--ignore-submodules');
}
// --find-renames option is only available starting with git 2.18.0
if (opts?.similarityThreshold && opts.similarityThreshold !== 50 && this._git.compareGitVersionTo('2.18.0') !== -1) {
args.push(`--find-renames=${opts.similarityThreshold}%`);
}
const child = this.stream(args, { env });
let result = new Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean }>((c, e) => {
const parser = new GitStatusParser();
const onClose = (exitCode: number) => {
if (exitCode !== 0) {
const stderr = stderrData.join('');
return e(new GitError({
message: 'Failed to execute git',
stderr,
exitCode,
gitErrorCode: getGitErrorCode(stderr),
gitCommand: 'status',
gitArgs: args
}));
}
c({ status: parser.status, statusLength: parser.status.length, didHitLimit: false });
};
const limit = opts?.limit ?? 10000;
const onStdoutData = (raw: string) => {
parser.update(raw);
if (limit !== 0 && parser.status.length > limit) {
child.removeListener('close', onClose);
child.stdout!.removeListener('data', onStdoutData);
child.kill();
c({ status: parser.status.slice(0, limit), statusLength: parser.status.length, didHitLimit: true });
}
};
child.stdout!.setEncoding('utf8');
child.stdout!.on('data', onStdoutData);
const stderrData: string[] = [];
child.stderr!.setEncoding('utf8');
child.stderr!.on('data', raw => stderrData.push(raw as string));
child.on('error', cpErrorHandler(e));
child.on('close', onClose);
});
if (opts?.cancellationToken) {
const cancellationPromise = new Promise<{ status: IFileStatus[]; statusLength: number; didHitLimit: boolean }>((_, e) => {
disposables.push(onceEvent(opts.cancellationToken!.onCancellationRequested)(() => {
try {
child.kill();
} catch (err) {
// noop
}
e(new CancellationError());
}));
});
result = Promise.race([result, cancellationPromise]);
}
try {
const { status, statusLength, didHitLimit } = await result;
return { status, statusLength, didHitLimit };
}
finally {
dispose(disposables);
}
}
async getHEADRef(): Promise<Branch | undefined> {
let HEAD: Branch | undefined;
try {
HEAD = await this.getHEAD();
if (HEAD.name) {
// Branch
HEAD = await this.getBranch(HEAD.name);
} else if (HEAD.commit) {
// Tag || Commit
const tags = await this.getRefs({ pattern: 'refs/tags' });
const tag = tags.find(tag => tag.commit === HEAD!.commit);
if (tag) {
HEAD = { ...HEAD, name: tag.name, type: RefType.Tag };
}
}
} catch (err) {
// noop
}
return HEAD;
}
async getHEAD(): Promise<Ref> {
try {
// Attempt to parse the HEAD file
const result = await this.getHEADFS();
return result;
}
catch (err) {
this.logger.warn(err.message);
}
try {
// Fallback to using git to determine HEAD
const result = await this.exec(['symbolic-ref', '--short', 'HEAD']);
if (!result.stdout) {
throw new Error('Not in a branch');
}
return { name: result.stdout.trim(), commit: undefined, type: RefType.Head };
}
catch (err) { }
// Detached HEAD
const result = await this.exec(['rev-parse', 'HEAD']);
if (!result.stdout) {
throw new Error('Error parsing HEAD');
}
return { name: undefined, commit: result.stdout.trim(), type: RefType.Head };
}
async getHEADFS(): Promise<Ref> {
const raw = await fs.readFile(path.join(this.dotGit.path, 'HEAD'), 'utf8');
// Branch
const branchMatch = raw.match(/^ref: refs\/heads\/(?<name>.*)$/m);
if (branchMatch?.groups?.name) {
return { name: branchMatch.groups.name, commit: undefined, type: RefType.Head };
}
// Detached
const commitMatch = raw.match(/^(?<commit>[0-9a-f]{40})$/m);
if (commitMatch?.groups?.commit) {
return { name: undefined, commit: commitMatch.groups.commit, type: RefType.Head };
}
throw new Error(`Unable to parse HEAD file. HEAD file contents: ${raw}.`);
}
async findTrackingBranches(upstreamBranch: string): Promise<Branch[]> {
const result = await this.exec(['for-each-ref', '--format', '%(refname:short)%00%(upstream:short)', 'refs/heads']);
return result.stdout.trim().split('\n')
.map(line => line.trim().split('\0'))
.filter(([_, upstream]) => upstream === upstreamBranch)
.map(([ref]) => ({ name: ref, type: RefType.Head } as Branch));
}
async getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise<Ref[]> {
if (cancellationToken && cancellationToken.isCancellationRequested) {
throw new CancellationError();
}
const args = ['for-each-ref'];
if (query.count) {
args.push(`--count=${query.count}`);
}
if (query.sort && query.sort !== 'alphabetically') {
args.push('--sort', `-${query.sort}`);
}
args.push('--format', '%(refname) %(objectname) %(*objectname)');
if (query.pattern) {
args.push(query.pattern.startsWith('refs/') ? query.pattern : `refs/${query.pattern}`);
}
if (query.contains) {
args.push('--contains', query.contains);
}
const result = await this.exec(args, { cancellationToken });
const fn = (line: string): Ref | null => {
let match: RegExpExecArray | null;
if (match = /^refs\/heads\/([^ ]+) ([0-9a-f]{40}) ([0-9a-f]{40})?$/.exec(line)) {
return { name: match[1], commit: match[2], type: RefType.Head };
} else if (match = /^refs\/remotes\/([^/]+)\/([^ ]+) ([0-9a-f]{40}) ([0-9a-f]{40})?$/.exec(line)) {
return { name: `${match[1]}/${match[2]}`, commit: match[3], type: RefType.RemoteHead, remote: match[1] };
} else if (match = /^refs\/tags\/([^ ]+) ([0-9a-f]{40}) ([0-9a-f]{40})?$/.exec(line)) {
return { name: match[1], commit: match[3] ?? match[2], type: RefType.Tag };
}
return null;
};
return result.stdout.split('\n')
.filter(line => !!line)
.map(fn)
.filter(ref => !!ref) as Ref[];
}
async getRemoteRefs(remote: string, opts?: { heads?: boolean; tags?: boolean; cancellationToken?: CancellationToken }): Promise<Ref[]> {
if (opts?.cancellationToken && opts?.cancellationToken.isCancellationRequested) {
throw new CancellationError();
}
const args = ['ls-remote'];
if (opts?.heads) {
args.push('--heads');
}
if (opts?.tags) {
args.push('--tags');
}
args.push(remote);
const result = await this.exec(args, { cancellationToken: opts?.cancellationToken });
const fn = (line: string): Ref | null => {
let match: RegExpExecArray | null;
if (match = /^([0-9a-f]{40})\trefs\/heads\/([^ ]+)$/.exec(line)) {
return { name: match[1], commit: match[2], type: RefType.Head };
} else if (match = /^([0-9a-f]{40})\trefs\/tags\/([^ ]+)$/.exec(line)) {
return { name: match[2], commit: match[1], type: RefType.Tag };
}
return null;
};
return result.stdout.split('\n')
.filter(line => !!line)
.map(fn)
.filter(ref => !!ref) as Ref[];
}
async getStashes(): Promise<Stash[]> {
const result = await this.exec(['stash', 'list']);
const regex = /^stash@{(\d+)}:(.+)$/;
const rawStashes = result.stdout.trim().split('\n')
.filter(b => !!b)
.map(line => regex.exec(line) as RegExpExecArray)
.filter(g => !!g)
.map(([, index, description]: RegExpExecArray) => ({ index: parseInt(index), description }));
return rawStashes;
}
async getRemotes(): Promise<Remote[]> {
const remotes: MutableRemote[] = [];
try {
// Attempt to parse the config file
remotes.push(...await this.getRemotesFS());
if (remotes.length === 0) {
this.logger.info('No remotes found in the git config file.');
}
}
catch (err) {
this.logger.warn(`getRemotes() - ${err.message}`);
// Fallback to using git to get the remotes
remotes.push(...await this.getRemotesGit());
}
for (const remote of remotes) {
// https://github.com/microsoft/vscode/issues/45271
remote.isReadOnly = remote.pushUrl === undefined || remote.pushUrl === 'no_push';
}
return remotes;
}
private async getRemotesFS(): Promise<MutableRemote[]> {
const raw = await fs.readFile(path.join(this.dotGit.commonPath ?? this.dotGit.path, 'config'), 'utf8');
return parseGitRemotes(raw);
}
private async getRemotesGit(): Promise<MutableRemote[]> {
const remotes: MutableRemote[] = [];
const result = await this.exec(['remote', '--verbose']);
const lines = result.stdout.trim().split('\n').filter(l => !!l);
for (const line of lines) {
const parts = line.split(/\s/);
const [name, url, type] = parts;
let remote = remotes.find(r => r.name === name);
if (!remote) {
remote = { name, isReadOnly: false };
remotes.push(remote);
}
if (/fetch/i.test(type)) {
remote.fetchUrl = url;
} else if (/push/i.test(type)) {
remote.pushUrl = url;
} else {
remote.fetchUrl = url;
remote.pushUrl = url;
}
}
return remotes;
}
async getBranch(name: string): Promise<Branch> {
if (name === 'HEAD') {
return this.getHEAD();
}
const args = ['for-each-ref'];
let supportsAheadBehind = true;
if (this._git.compareGitVersionTo('1.9.0') === -1) {
args.push('--format=%(refname)%00%(upstream:short)%00%(objectname)');
supportsAheadBehind = false;
} else if (this._git.compareGitVersionTo('2.16.0') === -1) {
args.push('--format=%(refname)%00%(upstream:short)%00%(objectname)%00%(upstream:track)');
} else {
args.push('--format=%(refname)%00%(upstream:short)%00%(objectname)%00%(upstream:track)%00%(upstream:remotename)%00%(upstream:remoteref)');
}
if (/^refs\/(head|remotes)\//i.test(name)) {
args.push(name);
} else {
args.push(`refs/heads/${name}`, `refs/remotes/${name}`);
}
const result = await this.exec(args);
const branches: Branch[] = result.stdout.trim().split('\n').map<Branch | undefined>(line => {
let [branchName, upstream, ref, status, remoteName, upstreamRef] = line.trim().split('\0');
if (branchName.startsWith('refs/heads/')) {
branchName = branchName.substring(11);
const index = upstream.indexOf('/');
let ahead;
let behind;
const match = /\[(?:ahead ([0-9]+))?[,\s]*(?:behind ([0-9]+))?]|\[gone]/.exec(status);
if (match) {
[, ahead, behind] = match;
}
return {
type: RefType.Head,
name: branchName,
upstream: upstream ? {
name: upstreamRef ? upstreamRef.substring(11) : upstream.substring(index + 1),
remote: remoteName ? remoteName : upstream.substring(0, index)
} : undefined,
commit: ref || undefined,
ahead: Number(ahead) || 0,
behind: Number(behind) || 0,
};
} else if (branchName.startsWith('refs/remotes/')) {
branchName = branchName.substring(13);
const index = branchName.indexOf('/');
return {
type: RefType.RemoteHead,
name: branchName.substring(index + 1),
remote: branchName.substring(0, index),
commit: ref,
};
} else {
return undefined;
}
}).filter((b?: Branch): b is Branch => !!b);
if (branches.length) {
const [branch] = branches;
if (!supportsAheadBehind && branch.upstream) {
try {
const result = await this.exec(['rev-list', '--left-right', '--count', `${branch.name}...${branch.upstream.remote}/${branch.upstream.name}`]);
const [ahead, behind] = result.stdout.trim().split('\t');
(branch as any).ahead = Number(ahead) || 0;
(branch as any).behind = Number(behind) || 0;
} catch { }
}
return branch;
}
return Promise.reject<Branch>(new Error('No such branch'));
}
// TODO: Support core.commentChar
stripCommitMessageComments(message: string): string {
return message.replace(/^\s*#.*$\n?/gm, '').trim();
}
async getSquashMessage(): Promise<string | undefined> {
const squashMsgPath = path.join(this.repositoryRoot, '.git', 'SQUASH_MSG');
try {
const raw = await fs.readFile(squashMsgPath, 'utf8');
return this.stripCommitMessageComments(raw);
} catch {
return undefined;
}
}
async getMergeMessage(): Promise<string | undefined> {
const mergeMsgPath = path.join(this.repositoryRoot, '.git', 'MERGE_MSG');
try {
const raw = await fs.readFile(mergeMsgPath, 'utf8');
return this.stripCommitMessageComments(raw);
} catch {
return undefined;
}
}
async getCommitTemplate(): Promise<string> {
try {
const result = await this.exec(['config', '--get', 'commit.template']);
if (!result.stdout) {
return '';
}
// https://github.com/git/git/blob/3a0f269e7c82aa3a87323cb7ae04ac5f129f036b/path.c#L612
const homedir = os.homedir();
let templatePath = result.stdout.trim()
.replace(/^~([^\/]*)\//, (_, user) => `${user ? path.join(path.dirname(homedir), user) : homedir}/`);
if (!path.isAbsolute(templatePath)) {
templatePath = path.join(this.repositoryRoot, templatePath);
}
const raw = await fs.readFile(templatePath, 'utf8');
return this.stripCommitMessageComments(raw);
} catch (err) {
return '';
}
}
async getCommit(ref: string): Promise<Commit> {
const result = await this.exec(['show', '-s', `--format=${COMMIT_FORMAT}`, '-z', ref]);
const commits = parseGitCommits(result.stdout);
if (commits.length === 0) {
return Promise.reject<Commit>('bad commit format');
}
return commits[0];
}
async updateSubmodules(paths: string[]): Promise<void> {
const args = ['submodule', 'update'];
for (const chunk of splitInChunks(paths.map(sanitizePath), MAX_CLI_LENGTH)) {
await this.exec([...args, '--', ...chunk]);
}
}
async getSubmodules(): Promise<Submodule[]> {
const gitmodulesPath = path.join(this.root, '.gitmodules');
try {
const gitmodulesRaw = await fs.readFile(gitmodulesPath, 'utf8');
return parseGitmodules(gitmodulesRaw);
} catch (err) {
if (/ENOENT/.test(err.message)) {
return [];
}
throw err;
}
}
}