diff --git a/.changeset/silly-actors-peel.md b/.changeset/silly-actors-peel.md new file mode 100644 index 00000000..8ca64afc --- /dev/null +++ b/.changeset/silly-actors-peel.md @@ -0,0 +1,5 @@ +--- +'simple-git': minor +--- + +Add `.version` to return git version information, including whether the git binary is installed. diff --git a/examples/git-version.md b/examples/git-version.md new file mode 100644 index 00000000..e85be8f5 --- /dev/null +++ b/examples/git-version.md @@ -0,0 +1,32 @@ +## Check if git is installed + +To check if `git` (or the `customBinary` of your choosing) is accessible, use the +`git.version()` api: + +```typescript +import { simpleGit } from 'simple-git'; + +const {installed} = await simpleGit().version(); +if (!installed) { + throw new Error(`Exit: "git" not available.`); +} + +// ... continue using git commands here +``` + +## Check for a specific version of git + +Using the `git.version()` interface, you can query for the current `git` version +information split by `major`, `minor` and `patch`: + +```typescript +import { simpleGit } from 'simple-git'; +import { lt } from 'semver'; + +const versionResult = await simpleGit().version(); +if (lt(String(versionResult), '2.1.0')) { + throw new Error(`Exit: "git" must be at least version 2.1.0.`); +} + +// ... continue using git commands here compatible with 2.1.0 or higher +``` diff --git a/simple-git/readme.md b/simple-git/readme.md index 0bd2ca81..8c136383 100644 --- a/simple-git/readme.md +++ b/simple-git/readme.md @@ -387,9 +387,13 @@ For type details of the response for each of the tasks, please see the [TypeScri ## git stash -- `.stash([ options ])` Stash the working directory, optional first argument can be an array of string arguments or [options](#how-to-specify-options) object to pass to the [git stash](https://git-scm.com/docs/git-stash) command. +- `.stash([ options ])` Stash the working directory, optional first argument can be an array of string arguments or [options](#how-to-specify-options) object to pass to the [git stash](https://git-scm.com/docs/git-stash) command. -- `.stashList([ options ])` Retrieves the stash list, optional first argument can be an object in the same format as used in [git log](#git-log). +- `.stashList([ options ])` Retrieves the stash list, optional first argument can be an object in the same format as used in [git log](#git-log). + +## git version [examples](https://github.com/steveukx/git-js/blob/main/examples/git-version.md) + +- `.version()` retrieve the major, minor and patch for the currently installed `git`. Use the `.installed` property of the result to determine whether `git` is accessible on the path. ## changing the working directory [examples](https://github.com/steveukx/git-js/blob/main/examples/git-change-working-directory.md) diff --git a/simple-git/src/lib/simple-git-api.ts b/simple-git/src/lib/simple-git-api.ts index c106beb6..ee5182c2 100644 --- a/simple-git/src/lib/simple-git-api.ts +++ b/simple-git/src/lib/simple-git-api.ts @@ -11,6 +11,7 @@ import { mergeTask } from './tasks/merge'; import { pushTask } from './tasks/push'; import { statusTask } from './tasks/status'; import { configurationErrorTask, straightThroughStringTask } from './tasks/task'; +import version from './tasks/version'; import { outputHandler, SimpleGitExecutor, SimpleGitTask, SimpleGitTaskCallback } from './types'; import { asArray, @@ -136,4 +137,4 @@ export class SimpleGitApi implements SimpleGitBase { } } -Object.assign(SimpleGitApi.prototype, commit(), config(), grep(), log()); +Object.assign(SimpleGitApi.prototype, commit(), config(), grep(), log(), version()); diff --git a/simple-git/src/lib/tasks/config.ts b/simple-git/src/lib/tasks/config.ts index b1de7b31..7e4333a5 100644 --- a/simple-git/src/lib/tasks/config.ts +++ b/simple-git/src/lib/tasks/config.ts @@ -1,7 +1,7 @@ -import { ConfigGetResult, ConfigListSummary, SimpleGit } from '../../../typings'; +import type { ConfigGetResult, ConfigListSummary, SimpleGit } from '../../../typings'; import { configGetParser, configListParser } from '../responses/ConfigList'; -import { SimpleGitApi } from '../simple-git-api'; -import { StringTask } from '../types'; +import type { SimpleGitApi } from '../simple-git-api'; +import type { StringTask } from '../types'; import { trailingFunctionArgument } from '../utils'; export enum GitConfigScope { diff --git a/simple-git/src/lib/tasks/version.ts b/simple-git/src/lib/tasks/version.ts new file mode 100644 index 00000000..8143d9e9 --- /dev/null +++ b/simple-git/src/lib/tasks/version.ts @@ -0,0 +1,79 @@ +import type { SimpleGitApi } from '../simple-git-api'; +import type { SimpleGit } from '../../../typings'; +import { asNumber, ExitCodes } from '../utils'; + +export interface VersionResult { + major: number; + minor: number; + patch: number; + agent: string; + installed: boolean; +} + +const NOT_INSTALLED = 'installed=false'; + +function versionResponse( + major = 0, + minor = 0, + patch = 0, + agent = '', + installed = true +): VersionResult { + return Object.defineProperty( + { + major, + minor, + patch, + agent, + installed, + }, + 'toString', + { + value() { + return `${major}.${minor}.${patch}`; + }, + configurable: false, + enumerable: false, + } + ); +} + +function notInstalledResponse() { + return versionResponse(0, 0, 0, '', false); +} + +export default function (): Pick { + return { + version(this: SimpleGitApi) { + return this._runTask({ + commands: ['--version'], + format: 'utf-8', + parser(stdOut) { + if (stdOut === NOT_INSTALLED) { + return notInstalledResponse(); + } + + const version = /version (\d+)\.(\d+)\.(\d+)(?:\s*\((.+)\))?/.exec(stdOut); + + if (!version) { + return versionResponse(0, 0, 0, stdOut); + } + + return versionResponse( + asNumber(version[1]), + asNumber(version[2]), + asNumber(version[3]), + version[4] || '' + ); + }, + onError(result, error, done, fail) { + if (result.exitCode === ExitCodes.NOT_FOUND) { + return done(Buffer.from(NOT_INSTALLED)); + } + + fail(error); + }, + }); + }, + }; +} diff --git a/simple-git/src/lib/utils/exit-codes.ts b/simple-git/src/lib/utils/exit-codes.ts index f07681fa..20a08e88 100644 --- a/simple-git/src/lib/utils/exit-codes.ts +++ b/simple-git/src/lib/utils/exit-codes.ts @@ -5,5 +5,6 @@ export enum ExitCodes { SUCCESS, ERROR, + NOT_FOUND = -2, UNCLEAN = 128, } diff --git a/simple-git/test/integration/version.spec.ts b/simple-git/test/integration/version.spec.ts new file mode 100644 index 00000000..1e51a8fd --- /dev/null +++ b/simple-git/test/integration/version.spec.ts @@ -0,0 +1,29 @@ +import { createTestContext, newSimpleGit, SimpleGitTestContext } from '@simple-git/test-utils'; + +describe('version', () => { + let context: SimpleGitTestContext; + + beforeEach(async () => (context = await createTestContext())); + + it('gets the current version', async () => { + const git = newSimpleGit(context.root); + expect(await git.version()).toEqual({ + major: 2, + minor: expect.any(Number), + patch: expect.any(Number), + agent: expect.any(String), + installed: true, + }); + }); + + it('gets the current version when the binary is not installed', async () => { + const git = newSimpleGit(context.root).customBinary('bad'); + expect(await git.version()).toEqual({ + major: 0, + minor: 0, + patch: 0, + agent: '', + installed: false, + }); + }); +}); diff --git a/simple-git/typings/simple-git.d.ts b/simple-git/typings/simple-git.d.ts index 2f21810f..f4ecfdc2 100644 --- a/simple-git/typings/simple-git.d.ts +++ b/simple-git/typings/simple-git.d.ts @@ -995,4 +995,11 @@ export interface SimpleGit extends SimpleGitBase { * Updates repository server info */ updateServerInfo(callback?: types.SimpleGitTaskCallback): Response; + + /** + * Retrieves `git` version information, including whether `git` is installed on the `PATH` + */ + version( + callback?: types.SimpleGitTaskCallback + ): Response; } diff --git a/simple-git/typings/types.d.ts b/simple-git/typings/types.d.ts index 475fe730..92c5430e 100644 --- a/simple-git/typings/types.d.ts +++ b/simple-git/typings/types.d.ts @@ -1,7 +1,7 @@ -export { RemoteWithoutRefs, RemoteWithRefs } from '../src/lib/responses/GetRemoteSummary'; -export { LogOptions, DefaultLogFields } from '../src/lib/tasks/log'; +export type { RemoteWithoutRefs, RemoteWithRefs } from '../src/lib/responses/GetRemoteSummary'; +export type { LogOptions, DefaultLogFields } from '../src/lib/tasks/log'; -export { +export type { outputHandler, Options, TaskOptions, @@ -10,10 +10,11 @@ export { SimpleGitTaskCallback, } from '../src/lib/types'; -export { ApplyOptions } from '../src/lib/tasks/apply-patch'; +export type { ApplyOptions } from '../src/lib/tasks/apply-patch'; export { CheckRepoActions } from '../src/lib/tasks/check-is-repo'; export { CleanOptions, CleanMode } from '../src/lib/tasks/clean'; -export { CloneOptions } from '../src/lib/tasks/clone'; +export type { CloneOptions } from '../src/lib/tasks/clone'; export { GitConfigScope } from '../src/lib/tasks/config'; export { GitGrepQuery, grepQueryBuilder } from '../src/lib/tasks/grep'; export { ResetOptions, ResetMode } from '../src/lib/tasks/reset'; +export type { VersionResult } from '../src/lib/tasks/version';