├── .changeset ├── README.md └── config.json ├── .editorconfig ├── .git-blame-ignore-revs ├── .github ├── FUNDING.yml ├── delete-merged-branch-config.yml ├── dependabot.yml └── workflows │ ├── changesets.yml │ ├── ci.yml │ ├── no-response.yml │ └── quality.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .yarn └── releases │ └── yarn-4.7.0.cjs ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── docs ├── CHANGELOG-HISTORICAL.md ├── DEBUG-LOGGING-GUIDE.md ├── ISSUES_AND_PULL_REQUESTS.md ├── LEGACY_NODE_VERSIONS.md ├── NON_ENGLISH_LOCALE.md ├── PLUGIN-ABORT-CONTROLLER.md ├── PLUGIN-COMPLETION-DETECTION.md ├── PLUGIN-CUSTOM-BINARY.md ├── PLUGIN-ERRORS.md ├── PLUGIN-PROGRESS-EVENTS.md ├── PLUGIN-SPAWN-OPTIONS.md ├── PLUGIN-TIMEOUT.md ├── PLUGIN-UNSAFE-ACTIONS.md └── RELEASE-NOTES-V2.md ├── examples ├── git-change-working-directory.md ├── git-grep.md ├── git-output-handler.md └── git-version.md ├── lerna.json ├── package.json ├── packages ├── babel-config │ ├── babel.config.js │ └── package.json ├── test-es-module-consumer │ ├── CHANGELOG.md │ ├── package.json │ ├── suite.mjs │ ├── test-default-as.mjs │ ├── test-default.mjs │ ├── test-named.mjs │ └── test.mjs ├── test-javascript-consumer │ ├── CHANGELOG.md │ ├── package.json │ ├── suite.js │ ├── test-default-as.js │ ├── test-default.js │ ├── test-named.js │ └── test.js ├── test-typescript-consumer │ ├── CHANGELOG.md │ ├── babel.config.js │ ├── package.json │ ├── test │ │ ├── ts-default-from-root.spec.ts │ │ └── ts-named-import.spec.ts │ └── tsconfig.json ├── test-typescript-esm-consumer │ ├── CHANGELOG.md │ ├── babel.config.js │ ├── package.json │ ├── test │ │ ├── ts-default-renamed-import.spec.ts │ │ └── ts-named-import.spec.ts │ └── tsconfig.json └── test-utils │ ├── CHANGELOG.md │ ├── index.ts │ ├── package.json │ └── src │ ├── create-abort-controller.ts │ ├── create-test-context.ts │ ├── expectations.ts │ ├── instance.ts │ ├── like.ts │ ├── setup │ ├── setup-conflicted.ts │ ├── setup-files.ts │ └── setup-init.ts │ └── wait.ts ├── readme.md ├── simple-git ├── CHANGELOG.md ├── babel.config.js ├── jest.config.js ├── package.json ├── promise.js ├── readme.md ├── scripts │ ├── build.js │ ├── log.js │ └── package-json.js ├── src │ ├── esm.mjs │ ├── git.js │ ├── index.js │ └── lib │ │ ├── .gitignore │ │ ├── api.ts │ │ ├── args │ │ ├── log-format.ts │ │ └── pathspec.ts │ │ ├── errors │ │ ├── git-construct-error.ts │ │ ├── git-error.ts │ │ ├── git-plugin-error.ts │ │ ├── git-response-error.ts │ │ └── task-configuration-error.ts │ │ ├── git-factory.ts │ │ ├── git-logger.ts │ │ ├── parsers │ │ ├── parse-branch-delete.ts │ │ ├── parse-branch.ts │ │ ├── parse-commit.ts │ │ ├── parse-diff-summary.ts │ │ ├── parse-fetch.ts │ │ ├── parse-list-log-summary.ts │ │ ├── parse-merge.ts │ │ ├── parse-move.ts │ │ ├── parse-pull.ts │ │ ├── parse-push.ts │ │ ├── parse-remote-messages.ts │ │ └── parse-remote-objects.ts │ │ ├── plugins │ │ ├── abort-plugin.ts │ │ ├── block-unsafe-operations-plugin.ts │ │ ├── command-config-prefixing-plugin.ts │ │ ├── completion-detection.plugin.ts │ │ ├── custom-binary.plugin.ts │ │ ├── error-detection.plugin.ts │ │ ├── index.ts │ │ ├── plugin-store.ts │ │ ├── progress-monitor-plugin.ts │ │ ├── simple-git-plugin.ts │ │ ├── spawn-options-plugin.ts │ │ ├── suffix-paths.plugin.ts │ │ └── timout-plugin.ts │ │ ├── responses │ │ ├── BranchDeleteSummary.ts │ │ ├── BranchSummary.ts │ │ ├── CheckIgnore.ts │ │ ├── CleanSummary.ts │ │ ├── ConfigList.ts │ │ ├── DiffSummary.ts │ │ ├── FileStatusSummary.ts │ │ ├── GetRemoteSummary.ts │ │ ├── InitSummary.ts │ │ ├── MergeSummary.ts │ │ ├── PullSummary.ts │ │ ├── StatusSummary.ts │ │ └── TagList.ts │ │ ├── runners │ │ ├── git-executor-chain.ts │ │ ├── git-executor.ts │ │ ├── promise-wrapped.ts │ │ ├── scheduler.ts │ │ └── tasks-pending-queue.ts │ │ ├── simple-git-api.ts │ │ ├── task-callback.ts │ │ ├── tasks │ │ ├── apply-patch.ts │ │ ├── branch.ts │ │ ├── change-working-directory.ts │ │ ├── check-ignore.ts │ │ ├── check-is-repo.ts │ │ ├── checkout.ts │ │ ├── clean.ts │ │ ├── clone.ts │ │ ├── commit.ts │ │ ├── config.ts │ │ ├── count-objects.ts │ │ ├── diff-name-status.ts │ │ ├── diff.ts │ │ ├── fetch.ts │ │ ├── first-commit.ts │ │ ├── grep.ts │ │ ├── hash-object.ts │ │ ├── init.ts │ │ ├── log.ts │ │ ├── merge.ts │ │ ├── move.ts │ │ ├── pull.ts │ │ ├── push.ts │ │ ├── remote.ts │ │ ├── reset.ts │ │ ├── show.ts │ │ ├── stash-list.ts │ │ ├── status.ts │ │ ├── sub-module.ts │ │ ├── tag.ts │ │ ├── task.ts │ │ └── version.ts │ │ ├── types │ │ ├── handlers.ts │ │ ├── index.ts │ │ └── tasks.ts │ │ └── utils │ │ ├── argument-filters.ts │ │ ├── exit-codes.ts │ │ ├── git-output-streams.ts │ │ ├── index.ts │ │ ├── line-parser.ts │ │ ├── simple-git-options.ts │ │ ├── task-options.ts │ │ ├── task-parser.ts │ │ └── util.ts ├── test │ ├── integration │ │ ├── add.spec.ts │ │ ├── bad-initial-path.spec.ts │ │ ├── branches.spec.ts │ │ ├── broken-chains.spec.ts │ │ ├── change-directory.spec.ts │ │ ├── check-is-repo.spec.ts │ │ ├── checkout.spec.ts │ │ ├── clean.spec.ts │ │ ├── commit.spec.ts │ │ ├── concurrent-commands.spec.ts │ │ ├── config.spec.ts │ │ ├── diff.spec.ts │ │ ├── exec.spec.ts │ │ ├── fetch.spec.ts │ │ ├── grep.spec.ts │ │ ├── log-name-status.spec.ts │ │ ├── log-numstat.spec.ts │ │ ├── log.spec.ts │ │ ├── merge-integration.spec.ts │ │ ├── output-handler.spec.ts │ │ ├── plugin.abort.spec.ts │ │ ├── plugin.completion.spec.ts │ │ ├── plugin.progress.spec.ts │ │ ├── plugin.timeout.spec.ts │ │ ├── plugin.unsafe.spec.ts │ │ ├── promise-from-root.spec.ts │ │ ├── promise.spec.ts │ │ ├── pull-fails-ff.spec.ts │ │ ├── remote.spec.ts │ │ ├── reset.spec.ts │ │ ├── rev-parse.spec.ts │ │ ├── status.spec.ts │ │ ├── tag.spec.ts │ │ └── version.spec.ts │ └── unit │ │ ├── __fixtures__ │ │ ├── child-processes.ts │ │ ├── create-fixture.ts │ │ ├── debug.ts │ │ ├── expectations.ts │ │ ├── file-exists.ts │ │ ├── index.ts │ │ ├── push │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ ├── push-deleted-branch.ts │ │ │ ├── push-new-branch-remote-says-vulnerabilities.ts │ │ │ ├── push-new-branch-with-tags.ts │ │ │ ├── push-new-branch.ts │ │ │ └── push-update-existing-branch.ts │ │ └── responses │ │ │ ├── branch.ts │ │ │ ├── commit.ts │ │ │ ├── diff.ts │ │ │ ├── merge.ts │ │ │ ├── remote-messages.ts │ │ │ ├── show.ts │ │ │ └── status.ts │ │ ├── __mocks__ │ │ └── mock-child-process.ts │ │ ├── add.spec.ts │ │ ├── apply-patch.spec.ts │ │ ├── args.log-format.spec.ts │ │ ├── branch.spec.ts │ │ ├── catfile.spec.ts │ │ ├── check-ignore.spec.ts │ │ ├── check-is-repo.spec.ts │ │ ├── checkout.spec.ts │ │ ├── child-process.spec.ts │ │ ├── clean.spec.ts │ │ ├── clone.spec.ts │ │ ├── commit.spec.ts │ │ ├── config.spec.ts │ │ ├── count-objects.spec.ts │ │ ├── cwd.spec.ts │ │ ├── diff.spec.ts │ │ ├── fetch.spec.ts │ │ ├── first-commit.spec.ts │ │ ├── git-executor.spec.ts │ │ ├── git.spec.ts │ │ ├── grep.spec.ts │ │ ├── hash-object.spec.ts │ │ ├── init.spec.ts │ │ ├── log.spec.ts │ │ ├── logging.spec.ts │ │ ├── merge.spec.ts │ │ ├── mv.spec.ts │ │ ├── output-handler.spec.ts │ │ ├── plugins │ │ ├── plugin.abort.spec.ts │ │ ├── plugin.binary.spec.ts │ │ ├── plugin.completion-detection.spec.ts │ │ ├── plugin.error.spec.ts │ │ ├── plugin.pathspec.spec.ts │ │ ├── plugin.unsafe.spec.ts │ │ └── plugins.spec.ts │ │ ├── promises.spec.ts │ │ ├── pull.spec.ts │ │ ├── push.spec.ts │ │ ├── raw.spec.ts │ │ ├── rebase.spec.ts │ │ ├── remote-messages.spec.ts │ │ ├── remote.spec.ts │ │ ├── reset.spec.ts │ │ ├── rev-parse.spec.ts │ │ ├── revert.spec.ts │ │ ├── rm.spec.ts │ │ ├── scheduler.spec.ts │ │ ├── show.spec.ts │ │ ├── stash-list.spec.ts │ │ ├── stash.spec.ts │ │ ├── status.spec.ts │ │ ├── sub-module.spec.ts │ │ ├── tags.spec.ts │ │ ├── task-options.spec.ts │ │ ├── update-server-info.spec.ts │ │ ├── utils.spec.ts │ │ └── version.spec.ts ├── tsconfig.json ├── tsconfig.release.json └── typings │ ├── errors.d.ts │ ├── index.d.ts │ ├── response.d.ts │ ├── simple-git.d.ts │ └── types.d.ts └── yarn.lock /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.4/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "main", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [ 10 | "@simple-git/test-utils" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 3 8 | trim_trailing_whitespace = true 9 | 10 | [*.json] 11 | indent_size = 2 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Format with prettier 2 | 4f19f3b4caf1ae41f3d5ec39f86b432ab893587c 3 | aa6b78ec22f2675f24d011444865ae87fac815e7 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [steveukx] 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | # custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/delete-merged-branch-config.yml: -------------------------------------------------------------------------------- 1 | exclude: 2 | - changeset-release/* 3 | delete_closed_pr: false 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /.github/workflows/changesets.yml: -------------------------------------------------------------------------------- 1 | name: release-changesets 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout Repo 13 | uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | 17 | - uses: actions/setup-node@v4 18 | with: 19 | node-version: 20 20 | 21 | - run: yarn install --immutable 22 | - run: yarn build 23 | 24 | - uses: changesets/action@v1 25 | with: 26 | publish: yarn changeset publish 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | NPM_TOKEN: ${{secrets.NPM_AUTH_TOKEN}} 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | matrix: 17 | node-version: [18, 20, 22, 23] 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | cache: yarn 25 | - run: yarn install --immutable 26 | - run: yarn build 27 | - name: Test 28 | env: 29 | GIT_AUTHOR_NAME: 'Simple Git Tests' 30 | GIT_AUTHOR_EMAIL: 'tests@simple-git.dev' 31 | run: yarn test 32 | -------------------------------------------------------------------------------- /.github/workflows/no-response.yml: -------------------------------------------------------------------------------- 1 | name: No Response 2 | 3 | # When a comment is created and every day at 08:05 4 | on: 5 | issue_comment: 6 | types: [created] 7 | schedule: 8 | - cron: '5 8 * * *' 9 | 10 | jobs: 11 | noResponse: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: lee-dohm/no-response@v0.5.0 15 | with: 16 | token: ${{ github.token }} 17 | daysUntilClose: 21 18 | responseRequiredLabel: more-info-needed 19 | closeComment: > 20 | This issue has been automatically closed due to a lack of response. 21 | If your problem persists please open a new issue including any additional detail 22 | requested from this issue. 23 | For more detail on the issues/pull requests see [ISSUES_AND_PULL_REQUESTS](https://github.com/steveukx/git-js/blob/main/docs/ISSUES_AND_PULL_REQUESTS.md) 24 | -------------------------------------------------------------------------------- /.github/workflows/quality.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Use Node.js 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 20 21 | cache: yarn 22 | - run: yarn install --immutable 23 | - run: yarn prettier --check . 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | 4 | node_modules/ 5 | .pnp.* 6 | .yarn/* 7 | !.yarn/patches 8 | !.yarn/plugins 9 | !.yarn/releases 10 | !.yarn/sdks 11 | !.yarn/versions 12 | 13 | # Identify the release and plugin bundles as binary content. 14 | # This way Git won't bother showing massive diffs when each time you subsequently add or update them 15 | /.yarn/releases/** binary 16 | /.yarn/plugins/** binary 17 | 18 | coverage/ 19 | dist/ 20 | *.bak 21 | *.log 22 | *.tsbuildinfo 23 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.md 2 | .changeset 3 | .yarn 4 | dist 5 | coverage 6 | node_modules 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "endOfLine": "lf", 5 | "printWidth": 100, 6 | "proseWrap": "preserve", 7 | "quoteProps": "consistent", 8 | "tabWidth": 3, 9 | "trailingComma": "es5", 10 | "semi": true, 11 | "singleQuote": true, 12 | "useTabs": false, 13 | "overrides": [ 14 | { 15 | "files": "*.json", 16 | "options": { 17 | "tabWidth": 2 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | yarnPath: .yarn/releases/yarn-4.7.0.cjs 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Steve King 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /docs/CHANGELOG-HISTORICAL.md: -------------------------------------------------------------------------------- 1 | # Change History & Release Notes 2 | 3 | ## 1.110.0 - ListLogLine 4 | 5 | - The default format expression used in `.log` splits ref data out of the `message` into a property of its own: `{ message: 'Some commit message (some-branch-name)' }` becomes `{ message: 'Some commit message', refs: 'some-branch-name' }` | 6 | - The commit body content is now included in the default format expression and can be used to identify the content of merge conflicts eg: `{ body: '# Conflicts:\n# some-file.txt' }` | 7 | 8 | 9 | ## 1.0.0 10 | 11 | Bumped to a new major revision in the 1.x branch, now uses `ChildProcess.spawn` in place of `ChildProcess.exec` to 12 | add escaping to the arguments passed to each of the tasks. 13 | 14 | -------------------------------------------------------------------------------- /docs/ISSUES_AND_PULL_REQUESTS.md: -------------------------------------------------------------------------------- 1 | # Issue Management 2 | 3 | To help keep the open issues representative of the most relevant and active set of issues with 4 | this project and its users there are some automated processes you may see interacting with 5 | your issues or pull requests: 6 | 7 | ## Automated "No Response" Closures 8 | 9 | Any issues where the project maintainers ask for more detail will automatically be closed after a 10 | set time unless the additional detail has been supplied. 11 | 12 | > My issue has been closed, but I now I have the extra detail 13 | 14 | If the issue has been closed but not locked, you can re-open the issue and add your additional 15 | detail. 16 | 17 | If the issue has been locked, create a new issue and link the old one by its ID or URL if it is 18 | still relevant. 19 | 20 | > Why close issues in this way? 21 | 22 | To keep the most relevant issues visible, historical issues that aren't being actively investigated 23 | lose their relevance as the library's implementation moves on, bugs get fixed and documentation 24 | improves. 25 | -------------------------------------------------------------------------------- /docs/LEGACY_NODE_VERSIONS.md: -------------------------------------------------------------------------------- 1 | 2 | # Legacy Node Versions 3 | 4 | From `v3.x`, `simple-git` will drop support for `node.js` version 10 or below. 5 | From `v3.8`, `simple-git` will no longer be tested against node version 12 or below. 6 | 7 | To use in lower versions of node, ensure you are also including the necessary polyfills from `core-js`: 8 | 9 | ## Example - JavaScript 10 | 11 | ```javascript 12 | require('core-js/stable/array/flat-map'); 13 | require('core-js/stable/object/from-entries'); 14 | require('core-js/stable/object/from-entries'); 15 | 16 | const simpleGit = require('simple-git'); 17 | ``` 18 | 19 | ## Example - TypeScript 20 | 21 | ```typescript 22 | import 'core-js/stable/array/flat-map'; 23 | import 'core-js/stable/object/from-entries'; 24 | import 'core-js/stable/object/from-entries'; 25 | 26 | import simpleGit, { SimpleGit } from 'simple-git'; 27 | 28 | const git: SimpleGit = simpleGit(); 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/NON_ENGLISH_LOCALE.md: -------------------------------------------------------------------------------- 1 | # Non-English Locales 2 | 3 | Some `simple-git` tasks return the `stdout` of the `git` binary directly (for example `git.raw()`) 4 | whereas some parse the `stdout` to pick out the useful data and build a structured response 5 | (for example `git.branchLocal()` which returns a `BranchSummary`). 6 | 7 | If your locale is set to any language other than English, please ensure you use the `LANG` and 8 | `LC_ALL` environment variables to ensure the `simple-git` parsers match correctly: 9 | 10 | ```typescript 11 | import { simpleGit } from 'simple-git'; 12 | 13 | const git = simpleGit().env({ 14 | LANG: 'C', 15 | LC_ALL: 'C', 16 | }); 17 | const branches = await git.branchLocal(); 18 | ``` 19 | 20 | > I've set a locale environment variable and now my auth is failing 21 | 22 | `simple-git` uses a `ChildProcess` to run the `git` commands, which will uses the same environment 23 | variables available in the node process running your script. When the environment variables are 24 | customised though, only those variables are available in the `git` process. 25 | 26 | If you are relying on `GIT_*` (or any other) environment variables to make `git` function 27 | correctly, ensure you pass those through as well: 28 | 29 | ```typescript 30 | import { simpleGit } from 'simple-git'; 31 | 32 | const git = simpleGit().env({ 33 | ...process.env, 34 | LANG: 'C', 35 | LC_ALL: 'C', 36 | }); 37 | const branches = await git.branchLocal(); 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /docs/PLUGIN-ABORT-CONTROLLER.md: -------------------------------------------------------------------------------- 1 | ## Using an AbortController to terminate tasks 2 | 3 | The easiest way to send a `SIGKILL` to the `git` child processes created by `simple-git` is to use an `AbortController` 4 | in the constructor options for `simpleGit`: 5 | 6 | ```typescript 7 | import { simpleGit, GitPluginError, SimpleGit } from 'simple-git'; 8 | 9 | const controller = new AbortController(); 10 | 11 | const git: SimpleGit = simpleGit({ 12 | baseDir: '/some/path', 13 | abort: controller.signal, 14 | }); 15 | 16 | try { 17 | await git.pull(); 18 | } 19 | catch (err) { 20 | if (err instanceof GitPluginError && err.plugin === 'abort') { 21 | // task failed because `controller.abort` was called while waiting for the `git.pull` 22 | } 23 | } 24 | ``` 25 | 26 | ### Examples: 27 | 28 | #### Share AbortController across many instances 29 | 30 | Run the same operation against multiple repositories, cancel any pending operations when the first has been completed. 31 | 32 | ```typescript 33 | const repos = [ 34 | '/path/to/repo-a', 35 | '/path/to/repo-b', 36 | '/path/to/repo-c', 37 | ]; 38 | 39 | const controller = new AbortController(); 40 | const result = await Promise.race( 41 | repos.map(baseDir => simpleGit({ baseDir, abort: controller.signal }).fetch()) 42 | ); 43 | controller.abort(); 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/PLUGIN-COMPLETION-DETECTION.md: -------------------------------------------------------------------------------- 1 | ## Configure how `simple-git` determines the `git` tasks to be complete. 2 | 3 | Up until version `2.46.0`, `simple-git` used both `close` and `exit` events from the `git` child process to determine 4 | whether the task was complete as follows: 5 | 6 | - the close / exit event fires 7 | - if there is data on either `stderr` or `stdout`, or if the child process as a whole has thrown an exception (for 8 | example when `git` is not installed) then the task is complete. 9 | - otherwise wait `50ms` and then treat the task as complete. 10 | 11 | From version `2.46.0` onwards, you can configure this behaviour by using the 12 | `completion` plugin: 13 | 14 | ```typescript 15 | import { simpleGit, SimpleGit } from 'simple-git'; 16 | 17 | const git: SimpleGit = simpleGit({ 18 | completion: { 19 | onExit: 50, 20 | onClose: true, 21 | }, 22 | }); 23 | ``` 24 | 25 | The `completion` plugin accepts two properties `onClose` and `onExit` that can be either: 26 | 27 | - `false` to ignore this event from the child process 28 | - `true` to treat the task as complete as soon as the event has fired 29 | - `number` to wait an arbitrary number of `ms` after the event has fired before treating the task as complete. 30 | 31 | To ensure backward compatibility, version 2.x of `simple-git` uses a default of 32 | `onClose = true, onExit = 50`. 33 | 34 | From version 3.x of `simple-git` the default will change to `onClose = true, onExit = false`, 35 | it should only be necessary to handle the `exit` event when the child processes are 36 | configured to not close (ie: with keep-alive). 37 | -------------------------------------------------------------------------------- /docs/PLUGIN-ERRORS.md: -------------------------------------------------------------------------------- 1 | ## Custom Error Detection 2 | 3 | By default, `simple-git` will determine that a `git` task has resulted in an error when the process exit 4 | code is anything other than `0` and there has been some data sent to the `stdErr` stream. Error handlers 5 | will be passed the content of both `stdOut` and `stdErr` concatenated together. 6 | 7 | To change any of this behaviour, configure the `simple-git` with the `errors` plugin with a function to be 8 | called after every task has been run and should return either `undefined` when the task is treated as 9 | a success, or a `Buffer` or `Error` when the task should be treated as a failure. 10 | 11 | When the default error handler (or any other plugin) has thrown an error, the first argument to the error 12 | detection plugin is the original error. Either return that error directly to allow it to bubble up to the 13 | task's error handlers, or implement your own error detection as below: 14 | 15 | ```typescript 16 | import { simpleGit } from 'simple-git'; 17 | 18 | const git = simpleGit({ 19 | errors(error, result) { 20 | // optionally pass through any errors reported before this plugin runs 21 | if (error) return error; 22 | 23 | // customise the `errorCode` values to treat as success 24 | if (result.exitCode === 0) { 25 | return; 26 | } 27 | 28 | // the default error messages include both stdOut and stdErr, but that 29 | // can be changed here, or completely replaced with some other content 30 | return Buffer.concat([...result.stdOut, ...result.stdErr]); 31 | } 32 | }) 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/PLUGIN-PROGRESS-EVENTS.md: -------------------------------------------------------------------------------- 1 | ## Progress Events 2 | 3 | To receive progress updates, pass a `progress` configuration option to the `simpleGit` instance: 4 | 5 | ```typescript 6 | import { simpleGit, SimpleGit, SimpleGitProgressEvent } from 'simple-git'; 7 | 8 | const progress = ({method, stage, progress}: SimpleGitProgressEvent) => { 9 | console.log(`git.${method} ${stage} stage ${progress}% complete`); 10 | } 11 | const git: SimpleGit = simpleGit({baseDir: '/some/path', progress}); 12 | 13 | // pull automatically triggers progress events when the progress plugin is configured 14 | await git.pull(); 15 | 16 | // supply the `--progress` option to any other command that supports it to receive 17 | // progress events into your handler 18 | await git.raw('pull', '--progress'); 19 | ``` 20 | 21 | The `checkout`, `clone`, 'fetch, `pull`, `push` methods will automatically enable progress events 22 | when a progress handler has been set. For any other method that _can_ support progress events, 23 | set `--progress` in the task's `TaskOptions` for example to receive progress events when running 24 | submodule tasks: 25 | 26 | ```typescript 27 | await git.submoduleUpdate('submodule-name', { '--progress': null }); 28 | await git.submoduleUpdate('submodule-name', ['--progress']); 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/PLUGIN-SPAWN-OPTIONS.md: -------------------------------------------------------------------------------- 1 | ## Process Owner User / Group 2 | 3 | To set the user identity or group identity of the spawned git commands to something other than the owner of 4 | the current Node process, supply a `spawnOptions` option with a `uid`, a `gid`, or both: 5 | 6 | ```typescript 7 | const git: SimpleGit = simpleGit('/some/path', { spawnOptions: { gid: 20 } }); 8 | 9 | // any command executed will belong to system group 20 10 | await git.pull(); 11 | ``` 12 | 13 | ```typescript 14 | const git: SimpleGit = simpleGit('/some/path', { spawnOptions: { uid: 1000 } }); 15 | 16 | // any command executed will belong to system user 1000 17 | await git.pull(); 18 | ``` 19 | -------------------------------------------------------------------------------- /examples/git-change-working-directory.md: -------------------------------------------------------------------------------- 1 | ## Changing Working Directory 2 | 3 | To change the directory the `git` commands are run in you can either configure the `simple-git` instance 4 | when it is created by using the `baseDir` property: 5 | 6 | ```typescript 7 | import { join } from 'path'; 8 | import { simpleGit } from 'simple-git'; 9 | 10 | const git = simpleGit({ baseDir: join(__dirname, 'repos') }); 11 | ``` 12 | 13 | Or explicitly set the working directory at some later time, for example after cloning a repo: 14 | 15 | ```typescript 16 | import { join } from 'path'; 17 | import { simpleGit, SimpleGit } from 'simple-git'; 18 | 19 | const remote = `https://github.com/steveukx/git-js.git`; 20 | const target = join(__dirname, 'repos', 'git-js'); 21 | 22 | // repo is now a `SimpleGit` instance operating on the `target` directory 23 | // having cloned the remote repo then switched into the cloned directory 24 | const repo: SimpleGit = await simpleGit().clone(remote, target).cwd({ path: target }); 25 | ``` 26 | 27 | In the example above we're using the command chaining feature of `simple-git` where many commands 28 | are treated as an atomic operation. To rewrite this using separate `async/await` steps would be: 29 | 30 | ```typescript 31 | import { join } from 'path'; 32 | import { simpleGit, SimpleGit } from 'simple-git'; 33 | 34 | const remote = `https://github.com/steveukx/git-js.git`; 35 | const target = join(__dirname, 'repos', 'git-js'); 36 | 37 | // create a `SimpleGit` instance 38 | const git: SimpleGit = simpleGit(); 39 | 40 | // use that instance to do the clone 41 | await git.clone(remote, target); 42 | 43 | // then set the working directory of the root instance - you want all future 44 | // tasks run through `git` to be from the new directory, rather than just tasks 45 | // chained off this task 46 | await git.cwd({ path: target, root: true }); 47 | ``` 48 | -------------------------------------------------------------------------------- /examples/git-grep.md: -------------------------------------------------------------------------------- 1 | ## Git Grep 2 | 3 | The official documentation for [git grep](https://git-scm.com/docs/git-grep) gives the full set of options that can be passed to the `simple-git` `git.grep` method as [options](https://github.com/steveukx/git-js/blob/main/readme.md#how-to-specify-options) (note that `-h` to hide the file name is disallowed). 4 | 5 | The simplest version is to search with a single search token: 6 | 7 | ```typescript 8 | import simpleGit from 'simple-git'; 9 | 10 | console.log(await simpleGit().grep('search-term')); 11 | ``` 12 | 13 | To search with multiple terms, use the `grepQueryBuilder` helper to construct the remaining arguments: 14 | 15 | ```typescript 16 | import simpleGit, { grepQueryBuilder } from 'simple-git'; 17 | 18 | // logs all files that contain `aaa` AND either `bbb` or `ccc` 19 | console.log( 20 | await simpleGit().grep(grepQueryBuilder('aaa').and('bbb', 'ccc')) 21 | ); 22 | ``` 23 | 24 | The builder interface is purely there to simplify the many `-e` flags needed to instruct `git` to treat an argument as a search term - the code above translates to: 25 | 26 | ```typescript 27 | console.log(Array.from(grepQueryBuilder('aaa').and('bbb', 'ccc'))) 28 | // [ '-e', 'aaa', '--and', '(', '-e', 'bbb', '-e', 'ccc', ')' ] 29 | ``` 30 | 31 | To build your own query instead of using the `grepQueryBuilder`, use the array form of [options](https://github.com/steveukx/git-js/blob/main/readme.md#how-to-specify-options): 32 | 33 | ```typescript 34 | import simpleGit from 'simple-git'; 35 | 36 | console.log(await simpleGit().grep('search-term', ['-e', 'another search term'])); 37 | ``` 38 | 39 | `git.grep` will include previews around the matched term in the resulting data, to disable this use options such as `-l` to only show the file name or `-c` to show the number of instances of a match in the file rather than the text that was matched. 40 | 41 | -------------------------------------------------------------------------------- /examples/git-output-handler.md: -------------------------------------------------------------------------------- 1 | ## Output Handler 2 | 3 | As `simple-git` receives data on either `stdout` or `stderr` streams from the `git` 4 | child processes it spawns, the data is buffered for parsing when the process has 5 | completed. 6 | 7 | Add an `outputHandler` to the instance to pipe these streams to another target, for 8 | example piping to the main process `stdout` / `stderr`: 9 | 10 | ```typescript 11 | import { InitResult, SimpleGit, simpleGit } from "simple-git"; 12 | 13 | const git: SimpleGit = simpleGit() 14 | .outputHandler((_command, stdout, stderr) => { 15 | stdout.pipe(process.stdout); 16 | stderr.pipe(process.stderr); 17 | }); 18 | 19 | const init: InitResult = await git.init(); 20 | ``` 21 | 22 | Note: there is a single `outputHandler` per `simple-git` instance, calling the method again 23 | will overwrite the existing `outputHandler`. 24 | 25 | Other uses for the `outputHandler` can include tracking the processes for metrics purposes, 26 | such as checking how many commands are currently being executed: 27 | 28 | ```typescript 29 | let processes = new Set(); 30 | const currentlyRunning = () => processes.size; 31 | const git = context.git.outputHandler((_command, stdout, stderr) => { 32 | const start = new Date(); 33 | const onClose = () => processes.delete(start); 34 | 35 | stdout.on('close', onClose); 36 | stderr.on('close', onClose); 37 | 38 | processes.add(start); 39 | }); 40 | 41 | expect(currentlyRunning()).toBe(0); 42 | const queue = [git.init(), git.add('*.txt')]; 43 | 44 | await wait(0); 45 | expect(currentlyRunning()).toBe(2); 46 | 47 | await Promise.all(queue); 48 | expect(currentlyRunning()).toBe(0); 49 | ``` 50 | -------------------------------------------------------------------------------- /examples/git-version.md: -------------------------------------------------------------------------------- 1 | ## Check if `git` is installed 2 | 3 | To check if `git` (or the `customBinary` of your choosing) is accessible, use the 4 | `git.version()` API: 5 | 6 | ```typescript 7 | import { simpleGit } from 'simple-git'; 8 | 9 | const {installed} = await simpleGit().version(); 10 | if (!installed) { 11 | throw new Error(`Exit: "git" not available.`); 12 | } 13 | 14 | // ... continue using Git commands here 15 | ``` 16 | 17 | ## Check for a specific version of `git` 18 | 19 | Using the `git.version()` interface, you can query for the current `git` version 20 | information split by `major`, `minor` and `patch`: 21 | 22 | ```typescript 23 | import { simpleGit } from 'simple-git'; 24 | import { lt } from 'semver'; 25 | 26 | const versionResult = await simpleGit().version(); 27 | if (lt(String(versionResult), '2.1.0')) { 28 | throw new Error(`Exit: "git" must be at least version 2.1.0.`); 29 | } 30 | 31 | // ... continue using Git commands here that are compatible with 2.1.0 or higher 32 | ``` 33 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "yarn", 3 | "command": { 4 | "create": { 5 | "homepage": "https://github.com/steveukx/git-js", 6 | "license": "MIT" 7 | } 8 | }, 9 | "ignoreChanges": ["**/test/**", "**/*.md"], 10 | "version": "2.48.0" 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simple-git/repo", 3 | "version": "3.1.1", 4 | "private": true, 5 | "workspaces": [ 6 | "packages/*", 7 | "simple-git" 8 | ], 9 | "resolutions": { 10 | "typescript": "4.7.4", 11 | "jest": "29.7.0" 12 | }, 13 | "scripts": { 14 | "build": "lerna run build", 15 | "clean": "git clean -fxd -e .idea -e node_modules -e .yarn", 16 | "clean:cache": "git clean -fxd .yarn node_modules packages simple-git", 17 | "format": "prettier --write .", 18 | "test": "lerna run test" 19 | }, 20 | "dependencies": { 21 | "@changesets/changelog-github": "^0.5.0", 22 | "@changesets/cli": "^2.28.1", 23 | "lerna": "^8.1.9", 24 | "prettier": "^3.5.2" 25 | }, 26 | "packageManager": "yarn@4.7.0" 27 | } 28 | -------------------------------------------------------------------------------- /packages/babel-config/babel.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const { existsSync } = require('fs'); 3 | 4 | function resolver(resolveToDist) { 5 | const root = resolve(__dirname, '../..', 'simple-git'); 6 | const dist = resolveToDist ? resolve(root, 'dist', 'cjs') : root; 7 | 8 | const pkg = existsSync(dist) ? dist : root; 9 | 10 | return [ 11 | 'module-resolver', 12 | { 13 | root: [pkg], 14 | alias: { 15 | 'simple-git/promise': resolve(root, 'promise'), 16 | 'simple-git': pkg, 17 | }, 18 | }, 19 | ]; 20 | } 21 | 22 | module.exports = function (resolveToDist = false) { 23 | return { 24 | presets: [ 25 | [ 26 | '@babel/preset-env', 27 | { 28 | targets: { 29 | node: 'current', 30 | }, 31 | }, 32 | ], 33 | '@babel/preset-typescript', 34 | ], 35 | plugins: [resolver(resolveToDist)], 36 | }; 37 | }; 38 | -------------------------------------------------------------------------------- /packages/babel-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simple-git/babel-config", 3 | "version": "1.0.0", 4 | "private": true, 5 | "main": "babel.config.js", 6 | "devDependencies": { 7 | "@babel/core": "^7.12.9", 8 | "@babel/preset-env": "^7.12.7", 9 | "@babel/preset-typescript": "^7.12.7", 10 | "babel-jest": "^27.4.5", 11 | "babel-plugin-module-resolver": "^4.0.0" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/steveukx/git-js.git", 16 | "directory": "packages/babel-config" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/test-es-module-consumer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @simple-git/test-es-module-consumer 2 | 3 | ## 1.0.1 4 | ### Patch Changes 5 | 6 | - 2f021e7: Support for importing as an ES module with TypeScript moduleResolution `node16` or newer by adding 7 | `simpleGit` as a named export. 8 | - Updated dependencies [2f021e7] 9 | - simple-git@3.10.0 10 | -------------------------------------------------------------------------------- /packages/test-es-module-consumer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simple-git/test-es-module-consumer", 3 | "private": true, 4 | "version": "1.0.1", 5 | "type": "module", 6 | "scripts": { 7 | "test": "node test.mjs" 8 | }, 9 | "dependencies": { 10 | "simple-git": "*" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/steveukx/git-js.git", 15 | "directory": "packages/test-es-module-consumer" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/test-es-module-consumer/suite.mjs: -------------------------------------------------------------------------------- 1 | import { strictEqual } from 'assert'; 2 | 3 | export async function suite(name, simpleGit, ResetMode) { 4 | exec(`${name}: imports default`, async () => { 5 | strictEqual( 6 | await simpleGit().checkIsRepo(), 7 | true, 8 | 'expected the current directory to be a valid git root' 9 | ); 10 | }); 11 | 12 | exec(`${name}: imports named exports`, async () => { 13 | strictEqual(/hard/.test(ResetMode.HARD), true, 'expected valid ResetMode enum'); 14 | }); 15 | } 16 | 17 | function exec(name, runner) { 18 | runner() 19 | .then(() => console.log(`${name}: OK`)) 20 | .catch((e) => { 21 | console.error(`${name}: ${e.message}`); 22 | throw e; 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /packages/test-es-module-consumer/test-default-as.mjs: -------------------------------------------------------------------------------- 1 | import { simpleGit, ResetMode } from 'simple-git'; 2 | import { suite } from './suite.mjs'; 3 | 4 | await suite('import named', simpleGit, ResetMode); 5 | -------------------------------------------------------------------------------- /packages/test-es-module-consumer/test-default.mjs: -------------------------------------------------------------------------------- 1 | import simpleGit, { ResetMode } from 'simple-git'; 2 | import { suite } from './suite.mjs'; 3 | 4 | await suite('import default', simpleGit, ResetMode); 5 | -------------------------------------------------------------------------------- /packages/test-es-module-consumer/test-named.mjs: -------------------------------------------------------------------------------- 1 | import { default as simpleGit, ResetMode } from 'simple-git'; 2 | import { suite } from './suite.mjs'; 3 | 4 | await suite('import default-as', simpleGit, ResetMode); 5 | -------------------------------------------------------------------------------- /packages/test-es-module-consumer/test.mjs: -------------------------------------------------------------------------------- 1 | import './test-default.mjs'; 2 | import './test-default-as.mjs'; 3 | import './test-named.mjs'; 4 | -------------------------------------------------------------------------------- /packages/test-javascript-consumer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @simple-git/teset-javascript-consumer 2 | 3 | ## 1.0.1 4 | ### Patch Changes 5 | 6 | - 2f021e7: Support for importing as an ES module with TypeScript moduleResolution `node16` or newer by adding 7 | `simpleGit` as a named export. 8 | - Updated dependencies [2f021e7] 9 | - simple-git@3.10.0 10 | -------------------------------------------------------------------------------- /packages/test-javascript-consumer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simple-git/teset-javascript-consumer", 3 | "private": true, 4 | "version": "1.0.1", 5 | "scripts": { 6 | "test": "node test.js && node test-default.js" 7 | }, 8 | "dependencies": { 9 | "simple-git": "*" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/steveukx/git-js.git", 14 | "directory": "packages/test-javascript-consumer" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/test-javascript-consumer/suite.js: -------------------------------------------------------------------------------- 1 | const { strictEqual } = require('assert'); 2 | 3 | module.exports = { 4 | async suite(name, simpleGit, ResetMode) { 5 | exec(`${name}: imports default`, async () => { 6 | strictEqual( 7 | await simpleGit().checkIsRepo(), 8 | true, 9 | 'expected the current directory to be a valid git root' 10 | ); 11 | }); 12 | 13 | exec(`${name}: imports named exports`, async () => { 14 | strictEqual(/hard/.test(ResetMode.HARD), true, 'expected valid ResetMode enum'); 15 | }); 16 | }, 17 | }; 18 | 19 | function exec(name, runner) { 20 | runner() 21 | .then(() => console.log(`${name}: OK`)) 22 | .catch((e) => { 23 | console.error(`${name}: ${e.message}`); 24 | throw e; 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /packages/test-javascript-consumer/test-default-as.js: -------------------------------------------------------------------------------- 1 | const { default: simpleGit, ResetMode } = require('simple-git'); 2 | const { suite } = require('./suite'); 3 | 4 | (async () => { 5 | await suite('require default-as', simpleGit, ResetMode); 6 | })(); 7 | -------------------------------------------------------------------------------- /packages/test-javascript-consumer/test-default.js: -------------------------------------------------------------------------------- 1 | const simpleGit = require('simple-git'); 2 | const { suite } = require('./suite'); 3 | 4 | (async () => { 5 | await suite('require default', simpleGit, simpleGit.ResetMode); 6 | })(); 7 | -------------------------------------------------------------------------------- /packages/test-javascript-consumer/test-named.js: -------------------------------------------------------------------------------- 1 | const { simpleGit, ResetMode } = require('simple-git'); 2 | const { suite } = require('./suite'); 3 | 4 | (async () => { 5 | await suite('require named', simpleGit, ResetMode); 6 | })(); 7 | -------------------------------------------------------------------------------- /packages/test-javascript-consumer/test.js: -------------------------------------------------------------------------------- 1 | require('./test-default'); 2 | require('./test-default-as'); 3 | require('./test-named'); 4 | -------------------------------------------------------------------------------- /packages/test-typescript-consumer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @simple-git/test-typescript-consumer 2 | 3 | ## 1.0.1 4 | ### Patch Changes 5 | 6 | - 2f021e7: Support for importing as an ES module with TypeScript moduleResolution `node16` or newer by adding 7 | `simpleGit` as a named export. 8 | - Updated dependencies [2f021e7] 9 | - simple-git@3.10.0 10 | -------------------------------------------------------------------------------- /packages/test-typescript-consumer/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@simple-git/babel-config')(true); 2 | -------------------------------------------------------------------------------- /packages/test-typescript-consumer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simple-git/test-typescript-consumer", 3 | "private": true, 4 | "version": "1.0.1", 5 | "jest": { 6 | "roots": [ 7 | "/test/" 8 | ], 9 | "testMatch": [ 10 | "**/test/*.spec.ts" 11 | ] 12 | }, 13 | "scripts": { 14 | "test": "yarn test:types && yarn test:jest", 15 | "test:types": "tsc", 16 | "test:jest": "jest" 17 | }, 18 | "dependencies": { 19 | "@simple-git/babel-config": "*", 20 | "simple-git": "*" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/steveukx/git-js.git", 25 | "directory": "packages/test-typescript-consumer" 26 | }, 27 | "devDependencies": { 28 | "jest": "^29.7.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/test-typescript-consumer/test/ts-default-from-root.spec.ts: -------------------------------------------------------------------------------- 1 | import { simpleGit, CleanOptions, SimpleGit, TaskConfigurationError } from 'simple-git'; 2 | 3 | describe('simple-git', () => { 4 | describe('named export', () => { 5 | it('is the simple-git factory', async () => { 6 | expect(await simpleGit().checkIsRepo()).toBe(true); 7 | }); 8 | 9 | it('builds exported types', async () => { 10 | const git: SimpleGit = simpleGit(); 11 | 12 | expect(git).not.toBeUndefined(); 13 | }); 14 | }); 15 | 16 | it('named class constructors', async () => { 17 | expect(new TaskConfigurationError('foo')).toBeInstanceOf(TaskConfigurationError); 18 | }); 19 | 20 | it('named enums', async () => { 21 | expect(CleanOptions.DRY_RUN).toBe('n'); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/test-typescript-consumer/test/ts-named-import.spec.ts: -------------------------------------------------------------------------------- 1 | import simpleGit, { gitP, CleanOptions, SimpleGit, TaskConfigurationError } from 'simple-git'; 2 | 3 | describe('simple-git', () => { 4 | describe('default export', () => { 5 | it('is the simple-git factory', async () => { 6 | expect(await simpleGit().checkIsRepo()).toBe(true); 7 | }); 8 | 9 | it('builds exported types', async () => { 10 | const git: SimpleGit = simpleGit(); 11 | 12 | expect(git).not.toBeUndefined(); 13 | }); 14 | }); 15 | 16 | describe('gitP export', () => { 17 | it('is the simple-git factory', async () => { 18 | expect(await gitP().checkIsRepo()).toBe(true); 19 | }); 20 | 21 | it('builds exported types', async () => { 22 | const git: SimpleGit = gitP(); 23 | 24 | expect(git).not.toBeUndefined(); 25 | }); 26 | }); 27 | 28 | it('default export is the simple-git factory', async () => { 29 | expect(await simpleGit().checkIsRepo()).toBe(true); 30 | }); 31 | 32 | it('named type exports', async () => { 33 | const git: SimpleGit = simpleGit(); 34 | 35 | expect(git).not.toBeUndefined(); 36 | }); 37 | 38 | it('named class constructors', async () => { 39 | expect(new TaskConfigurationError('foo')).toBeInstanceOf(TaskConfigurationError); 40 | }); 41 | 42 | it('named enums', async () => { 43 | expect(CleanOptions.DRY_RUN).toBe('n'); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /packages/test-typescript-consumer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "esnext"], 4 | "target": "es2015", 5 | "moduleResolution": "Node", 6 | "noEmit": true, 7 | "composite": false, 8 | "baseUrl": ".", 9 | "paths": {}, 10 | "esModuleInterop": false, 11 | "allowSyntheticDefaultImports": false 12 | }, 13 | "files": ["test/ts-default-from-root.spec.ts"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/test-typescript-esm-consumer/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @simple-git/test-typescript-esm-consumer 2 | 3 | ## 1.0.1 4 | ### Patch Changes 5 | 6 | - 2f021e7: Support for importing as an ES module with TypeScript moduleResolution `node16` or newer by adding 7 | `simpleGit` as a named export. 8 | - Updated dependencies [2f021e7] 9 | - simple-git@3.10.0 10 | -------------------------------------------------------------------------------- /packages/test-typescript-esm-consumer/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@simple-git/babel-config')(true); 2 | -------------------------------------------------------------------------------- /packages/test-typescript-esm-consumer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simple-git/test-typescript-esm-consumer", 3 | "private": true, 4 | "version": "1.0.1", 5 | "jest": { 6 | "roots": [ 7 | "/test/" 8 | ], 9 | "testMatch": [ 10 | "**/test/*.spec.ts" 11 | ] 12 | }, 13 | "scripts": { 14 | "test": "jest" 15 | }, 16 | "dependencies": { 17 | "@simple-git/babel-config": "*", 18 | "simple-git": "*" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/steveukx/git-js.git", 23 | "directory": "packages/test-typescript-consumer" 24 | }, 25 | "devDependencies": { 26 | "jest": "^29.7.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/test-typescript-esm-consumer/test/ts-default-renamed-import.spec.ts: -------------------------------------------------------------------------------- 1 | import { default as simpleGit, CleanOptions, SimpleGit, TaskConfigurationError } from 'simple-git'; 2 | 3 | describe('simple-git', () => { 4 | describe('renamed default export', () => { 5 | it('is the simple-git factory', async () => { 6 | expect(await simpleGit().checkIsRepo()).toBe(true); 7 | }); 8 | 9 | it('builds exported types', async () => { 10 | const git: SimpleGit = simpleGit(); 11 | 12 | expect(git).not.toBeUndefined(); 13 | }); 14 | }); 15 | 16 | it('named type exports', async () => { 17 | const git: SimpleGit = simpleGit(); 18 | 19 | expect(git).not.toBeUndefined(); 20 | }); 21 | 22 | it('named class constructors', async () => { 23 | expect(new TaskConfigurationError('foo')).toBeInstanceOf(TaskConfigurationError); 24 | }); 25 | 26 | it('named enums', async () => { 27 | expect(CleanOptions.DRY_RUN).toBe('n'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/test-typescript-esm-consumer/test/ts-named-import.spec.ts: -------------------------------------------------------------------------------- 1 | import { simpleGit, CleanOptions, SimpleGit, TaskConfigurationError } from 'simple-git'; 2 | 3 | describe('simple-git', () => { 4 | describe('named export', () => { 5 | it('is the simple-git factory', async () => { 6 | expect(await simpleGit().checkIsRepo()).toBe(true); 7 | }); 8 | 9 | it('builds exported types', async () => { 10 | const git: SimpleGit = simpleGit(); 11 | 12 | expect(git).not.toBeUndefined(); 13 | }); 14 | }); 15 | 16 | it('named type exports', async () => { 17 | const git: SimpleGit = simpleGit(); 18 | 19 | expect(git).not.toBeUndefined(); 20 | }); 21 | 22 | it('named class constructors', async () => { 23 | expect(new TaskConfigurationError('foo')).toBeInstanceOf(TaskConfigurationError); 24 | }); 25 | 26 | it('named enums', async () => { 27 | expect(CleanOptions.DRY_RUN).toBe('n'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/test-typescript-esm-consumer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "node16", 5 | "moduleResolution": "node16", 6 | "noEmit": true, 7 | "allowSyntheticDefaultImports": true, 8 | "baseUrl": "." 9 | // "emitDecoratorMetadata": true, 10 | // "experimentalDecorators": true, 11 | // "forceConsistentCasingInFileNames": true, 12 | // "incremental": true, 13 | // "noFallthroughCasesInSwitch": false, 14 | // "noImplicitAny": false, 15 | // "outDir": "./dist", 16 | // "removeComments": true, 17 | // "skipLibCheck": true, 18 | // "sourceMap": true, 19 | // "strictBindCallApply": true, 20 | // "strictNullChecks": false, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/test-utils/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @simple-git/test-utils 2 | 3 | ## 4.0.0 4 | 5 | ### Patch Changes 6 | 7 | - Updated dependencies [ec97a39] 8 | - Updated dependencies [97fde2c] 9 | - Updated dependencies [0a623e5] 10 | - simple-git@3.16.0 11 | 12 | ## 3.0.0 13 | 14 | ### Patch Changes 15 | 16 | - Updated dependencies [7746480] 17 | - Updated dependencies [7746480] 18 | - simple-git@3.15.0 19 | 20 | ## 2.0.0 21 | 22 | ### Patch Changes 23 | 24 | - Updated dependencies [19029fc] 25 | - Updated dependencies [4259b26] 26 | - simple-git@3.14.0 27 | 28 | ## 1.0.0 29 | 30 | ### Major Changes 31 | 32 | - 6b3e05c: Use shared test utilities bundle in simple-git tests, to enable consistent testing across packages in the future 33 | 34 | ### Patch Changes 35 | 36 | - Updated dependencies [87b0d75] 37 | - Updated dependencies [6b3e05c] 38 | - Updated dependencies [d0dceda] 39 | - simple-git@3.13.0 40 | -------------------------------------------------------------------------------- /packages/test-utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './src/create-abort-controller'; 2 | export * from './src/create-test-context'; 3 | export * from './src/expectations'; 4 | export * from './src/instance'; 5 | export * from './src/like'; 6 | export * from './src/wait'; 7 | 8 | export * from './src/setup/setup-conflicted'; 9 | export * from './src/setup/setup-files'; 10 | export * from './src/setup/setup-init'; 11 | -------------------------------------------------------------------------------- /packages/test-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@simple-git/test-utils", 3 | "version": "4.0.0", 4 | "private": true, 5 | "peerDependencies": { 6 | "simple-git": "*" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/test-utils/src/create-abort-controller.ts: -------------------------------------------------------------------------------- 1 | import { setMaxListeners } from 'events'; 2 | 3 | export function createAbortController() { 4 | if (typeof AbortController === 'undefined') { 5 | return createMockAbortController() as { controller: AbortController; abort: AbortSignal }; 6 | } 7 | 8 | const controller = new AbortController(); 9 | setMaxListeners(1000, controller.signal); 10 | return { 11 | controller, 12 | abort: controller.signal, 13 | mocked: false, 14 | }; 15 | } 16 | 17 | function createMockAbortController(): unknown { 18 | let aborted = false; 19 | const handlers: Set<() => void> = new Set(); 20 | const abort = { 21 | addEventListener(type: 'abort', handler: () => void) { 22 | if (type !== 'abort') throw new Error('Unsupported event name'); 23 | handlers.add(handler); 24 | }, 25 | removeEventListener(type: 'abort', handler: () => void) { 26 | if (type !== 'abort') throw new Error('Unsupported event name'); 27 | handlers.delete(handler); 28 | }, 29 | get aborted() { 30 | return aborted; 31 | }, 32 | }; 33 | 34 | return { 35 | controller: { 36 | abort() { 37 | if (aborted) throw new Error('abort called when already aborted'); 38 | aborted = true; 39 | handlers.forEach((h) => h()); 40 | }, 41 | }, 42 | abort, 43 | mocked: true, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /packages/test-utils/src/expectations.ts: -------------------------------------------------------------------------------- 1 | import { GitError, GitResponseError } from 'simple-git'; 2 | 3 | /** 4 | * Convenience for asserting the type and message of a `GitError` 5 | * 6 | * ```javascript 7 | const promise = doSomethingAsyncThatRejects(); 8 | const {threw, error} = await promiseError(git.init()); 9 | 10 | expect(threw).toBe(true); 11 | assertGitError(error, 'some message'); 12 | ``` 13 | */ 14 | export function assertGitError( 15 | errorInstance: Error | unknown, 16 | message: string | RegExp, 17 | errorConstructor: any = GitError 18 | ) { 19 | expect(errorInstance).toBeInstanceOf(errorConstructor); 20 | expect(errorInstance).toHaveProperty( 21 | 'message', 22 | typeof message === 'string' 23 | ? expect.stringContaining(message) 24 | : expect.stringMatching(message) 25 | ); 26 | } 27 | 28 | export function assertGitResponseError(errorInstance: Error | unknown, git: any, equality?: any) { 29 | expect(errorInstance).toBeInstanceOf(GitResponseError); 30 | git && expect((errorInstance as any).git).toBeInstanceOf(git); 31 | equality && expect((errorInstance as any).git).toEqual(equality); 32 | } 33 | -------------------------------------------------------------------------------- /packages/test-utils/src/instance.ts: -------------------------------------------------------------------------------- 1 | import { simpleGit as newSimpleGit } from 'simple-git'; 2 | 3 | export { newSimpleGit }; 4 | -------------------------------------------------------------------------------- /packages/test-utils/src/like.ts: -------------------------------------------------------------------------------- 1 | export function like(what: Partial) { 2 | return expect.objectContaining(what); 3 | } 4 | -------------------------------------------------------------------------------- /packages/test-utils/src/setup/setup-conflicted.ts: -------------------------------------------------------------------------------- 1 | import { SimpleGitTestContext } from '../create-test-context'; 2 | 3 | export const FIRST_BRANCH = 'first'; 4 | export const SECOND_BRANCH = 'second'; 5 | 6 | export async function setUpConflicted({ git, file }: SimpleGitTestContext) { 7 | await git.raw('checkout', '-b', FIRST_BRANCH); 8 | 9 | await file('aaa.txt', 'Some\nFile content\nhere'); 10 | await file('bbb.txt', Array(20).join('bbb\n')); 11 | 12 | await git.add(`*.txt`); 13 | await git.commit('first commit'); 14 | await git.raw('checkout', '-b', SECOND_BRANCH, FIRST_BRANCH); 15 | 16 | await file('aaa.txt', 'Different\nFile content\nhere'); 17 | await file('ccc.txt', 'Another file'); 18 | 19 | await git.add(`*.txt`); 20 | await git.commit('second commit'); 21 | } 22 | 23 | export async function createSingleConflict({ git, file }: SimpleGitTestContext) { 24 | await git.checkout(FIRST_BRANCH); 25 | await file('aaa.txt', 'Conflicting\nFile content\nhere'); 26 | 27 | await git.add(`aaa.txt`); 28 | await git.commit('move first ahead of second'); 29 | 30 | return SECOND_BRANCH; 31 | } 32 | -------------------------------------------------------------------------------- /packages/test-utils/src/setup/setup-files.ts: -------------------------------------------------------------------------------- 1 | import { SimpleGitTestContext } from '../create-test-context'; 2 | 3 | export async function setUpFilesAdded( 4 | { git, files }: SimpleGitTestContext, 5 | fileNames: string[], 6 | addSelector: string | string[] = '.', 7 | message = 'Create files' 8 | ) { 9 | await files(...fileNames); 10 | await git.add(addSelector); 11 | await git.commit(message); 12 | } 13 | -------------------------------------------------------------------------------- /packages/test-utils/src/setup/setup-init.ts: -------------------------------------------------------------------------------- 1 | import { SimpleGit } from 'simple-git'; 2 | import { SimpleGitTestContext } from '../create-test-context'; 3 | 4 | export const GIT_USER_NAME = 'Simple Git Tests'; 5 | export const GIT_USER_EMAIL = 'tests@simple-git.dev'; 6 | 7 | export async function setUpInit({ git }: Pick) { 8 | await git.raw('-c', 'init.defaultbranch=master', 'init'); 9 | await configureGitCommitter(git); 10 | } 11 | 12 | async function configureGitCommitter(git: SimpleGit, name = GIT_USER_NAME, email = GIT_USER_EMAIL) { 13 | await git.addConfig('user.name', name); 14 | await git.addConfig('user.email', email); 15 | } 16 | -------------------------------------------------------------------------------- /packages/test-utils/src/wait.ts: -------------------------------------------------------------------------------- 1 | export function wait(timeoutOrPromise: number | Promise = 10): Promise { 2 | if ( 3 | timeoutOrPromise && 4 | typeof timeoutOrPromise === 'object' && 5 | typeof timeoutOrPromise.then === 'function' 6 | ) { 7 | return timeoutOrPromise.then(() => wait()); 8 | } 9 | 10 | return new Promise((ok) => 11 | setTimeout(ok, typeof timeoutOrPromise === 'number' ? timeoutOrPromise : 10) 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ./simple-git/readme.md -------------------------------------------------------------------------------- /simple-git/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@simple-git/babel-config')(); 2 | -------------------------------------------------------------------------------- /simple-git/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/src/', '/test/', '/typings/'], 3 | coverageThreshold: { 4 | global: { 5 | branches: 80, 6 | functions: 80, 7 | lines: 80, 8 | statements: 80, 9 | }, 10 | }, 11 | coveragePathIgnorePatterns: ['/test/'], 12 | coverageReporters: ['json', 'lcov', 'text', 'clover'], 13 | testMatch: ['**/test/**/test-*.js', '**/test/**/*.spec.*'], 14 | }; 15 | -------------------------------------------------------------------------------- /simple-git/promise.js: -------------------------------------------------------------------------------- 1 | console.error(`============================================= 2 | simple-git has supported promises / async await since version 2.6.0. 3 | Importing from 'simple-git/promise' has been deprecated and will 4 | report this error until the next major release of version 4. 5 | 6 | To upgrade, change all 'simple-git/promise' imports to just 'simple-git' 7 | =============================================`); 8 | 9 | const simpleGit = require('.'); 10 | 11 | module.exports = Object.assign( 12 | function () { 13 | return simpleGit.gitP.apply(null, arguments); 14 | }, 15 | simpleGit, 16 | { default: simpleGit.gitP } 17 | ); 18 | -------------------------------------------------------------------------------- /simple-git/scripts/build.js: -------------------------------------------------------------------------------- 1 | const { writeFile } = require('fs'); 2 | const { resolve } = require('path'); 3 | const esbuild = require('esbuild'); 4 | const { nodeExternalsPlugin } = require('esbuild-node-externals'); 5 | const { logger } = require('./log'); 6 | 7 | const log = logger('ESM'); 8 | const outDir = resolve(__dirname, '..', 'dist'); 9 | 10 | Promise.resolve() 11 | .then(() => log('generating esm source')) 12 | .then(() => esm()) 13 | .then(() => log('generating cjs source')) 14 | .then(() => cjs()) 15 | .then(() => log('done')); 16 | 17 | async function esm() { 18 | const outfile = resolve(outDir, 'esm', 'index.js'); 19 | 20 | await esbuild.build({ 21 | entryPoints: ['src/esm.mjs'], 22 | bundle: true, 23 | platform: 'node', 24 | format: 'esm', 25 | sourcemap: true, 26 | outfile, 27 | plugins: [nodeExternalsPlugin()], 28 | }); 29 | 30 | await new Promise((done, fail) => 31 | writeFile( 32 | resolve(outfile, '..', 'package.json'), 33 | JSON.stringify({ type: 'module' }, null, 2), 34 | { encoding: 'utf8' }, 35 | (err) => (err ? fail(err) : done()) 36 | ) 37 | ); 38 | } 39 | 40 | async function cjs() { 41 | const outfile = resolve(outDir, 'cjs', 'index.js'); 42 | 43 | await esbuild.build({ 44 | entryPoints: ['src/index.js'], 45 | bundle: true, 46 | platform: 'node', 47 | format: 'cjs', 48 | sourcemap: true, 49 | outfile, 50 | plugins: [nodeExternalsPlugin()], 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /simple-git/scripts/log.js: -------------------------------------------------------------------------------- 1 | module.exports.logger = (name) => { 2 | return (...args) => { 3 | console.log(`${name}:`, ...args); 4 | }; 5 | }; 6 | -------------------------------------------------------------------------------- /simple-git/scripts/package-json.js: -------------------------------------------------------------------------------- 1 | const { writeFile } = require('fs'); 2 | const { resolve } = require('path'); 3 | const { logger } = require('./log'); 4 | 5 | const log = logger('package.json'); 6 | const src = resolve(__dirname, '..', 'package.json'); 7 | 8 | (async function () { 9 | log('Generating content'); 10 | const pkg = createPackageJson(); 11 | log('Writing content'); 12 | await save(pkg); 13 | log('Done'); 14 | })(); 15 | 16 | function save(content) { 17 | return new Promise((done, fail) => 18 | writeFile(src, JSON.stringify(content, null, 2), 'utf8', (err) => { 19 | err ? fail(err) : done(); 20 | }) 21 | ); 22 | } 23 | 24 | function createPackageJson() { 25 | const { publish, scripts, ...pkg } = require(src); 26 | 27 | return { 28 | ...pkg, 29 | ...publish, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /simple-git/src/esm.mjs: -------------------------------------------------------------------------------- 1 | import { gitInstanceFactory } from './lib/git-factory'; 2 | 3 | export { gitP } from './lib/runners/promise-wrapped'; 4 | export * from './lib/api'; 5 | 6 | export const simpleGit = gitInstanceFactory; 7 | 8 | export default gitInstanceFactory; 9 | -------------------------------------------------------------------------------- /simple-git/src/index.js: -------------------------------------------------------------------------------- 1 | const { gitP } = require('./lib/runners/promise-wrapped'); 2 | const { esModuleFactory, gitInstanceFactory, gitExportFactory } = require('./lib/git-factory'); 3 | 4 | const simpleGit = esModuleFactory(gitExportFactory(gitInstanceFactory)); 5 | 6 | module.exports = Object.assign(simpleGit, { gitP, simpleGit }); 7 | -------------------------------------------------------------------------------- /simple-git/src/lib/.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.map 3 | *.d.ts 4 | -------------------------------------------------------------------------------- /simple-git/src/lib/api.ts: -------------------------------------------------------------------------------- 1 | import { pathspec } from './args/pathspec'; 2 | import { GitConstructError } from './errors/git-construct-error'; 3 | import { GitError } from './errors/git-error'; 4 | import { GitPluginError } from './errors/git-plugin-error'; 5 | import { GitResponseError } from './errors/git-response-error'; 6 | import { TaskConfigurationError } from './errors/task-configuration-error'; 7 | import { CheckRepoActions } from './tasks/check-is-repo'; 8 | import { CleanOptions } from './tasks/clean'; 9 | import { GitConfigScope } from './tasks/config'; 10 | import { DiffNameStatus } from './tasks/diff-name-status'; 11 | import { grepQueryBuilder } from './tasks/grep'; 12 | import { ResetMode } from './tasks/reset'; 13 | 14 | export { 15 | CheckRepoActions, 16 | CleanOptions, 17 | DiffNameStatus, 18 | GitConfigScope, 19 | GitConstructError, 20 | GitError, 21 | GitPluginError, 22 | GitResponseError, 23 | ResetMode, 24 | TaskConfigurationError, 25 | grepQueryBuilder, 26 | pathspec, 27 | }; 28 | -------------------------------------------------------------------------------- /simple-git/src/lib/args/log-format.ts: -------------------------------------------------------------------------------- 1 | export enum LogFormat { 2 | NONE = '', 3 | STAT = '--stat', 4 | NUM_STAT = '--numstat', 5 | NAME_ONLY = '--name-only', 6 | NAME_STATUS = '--name-status', 7 | } 8 | 9 | const logFormatRegex = /^--(stat|numstat|name-only|name-status)(=|$)/; 10 | 11 | export function logFormatFromCommand(customArgs: string[]) { 12 | for (let i = 0; i < customArgs.length; i++) { 13 | const format = logFormatRegex.exec(customArgs[i]); 14 | if (format) { 15 | return `--${format[1]}` as LogFormat; 16 | } 17 | } 18 | 19 | return LogFormat.NONE; 20 | } 21 | 22 | export function isLogFormat(customArg: string | unknown) { 23 | return logFormatRegex.test(customArg as string); 24 | } 25 | -------------------------------------------------------------------------------- /simple-git/src/lib/args/pathspec.ts: -------------------------------------------------------------------------------- 1 | const cache = new WeakMap(); 2 | 3 | export function pathspec(...paths: string[]) { 4 | const key = new String(paths); 5 | cache.set(key, paths); 6 | 7 | return key as string; 8 | } 9 | 10 | export function isPathSpec(path: string | unknown): path is string { 11 | return path instanceof String && cache.has(path); 12 | } 13 | 14 | export function toPaths(pathSpec: string): string[] { 15 | return cache.get(pathSpec) || []; 16 | } 17 | -------------------------------------------------------------------------------- /simple-git/src/lib/errors/git-construct-error.ts: -------------------------------------------------------------------------------- 1 | import { GitError } from './git-error'; 2 | import { SimpleGitOptions } from '../types'; 3 | 4 | /** 5 | * The `GitConstructError` is thrown when an error occurs in the constructor 6 | * of the `simple-git` instance itself. Most commonly as a result of using 7 | * a `baseDir` option that points to a folder that either does not exist, 8 | * or cannot be read by the user the node script is running as. 9 | * 10 | * Check the `.message` property for more detail including the properties 11 | * passed to the constructor. 12 | */ 13 | export class GitConstructError extends GitError { 14 | constructor( 15 | public readonly config: SimpleGitOptions, 16 | message: string 17 | ) { 18 | super(undefined, message); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /simple-git/src/lib/errors/git-error.ts: -------------------------------------------------------------------------------- 1 | import type { SimpleGitTask } from '../types'; 2 | 3 | /** 4 | * The `GitError` is thrown when the underlying `git` process throws a 5 | * fatal exception (eg an `ENOENT` exception when attempting to use a 6 | * non-writable directory as the root for your repo), and acts as the 7 | * base class for more specific errors thrown by the parsing of the 8 | * git response or errors in the configuration of the task about to 9 | * be run. 10 | * 11 | * When an exception is thrown, pending tasks in the same instance will 12 | * not be executed. The recommended way to run a series of tasks that 13 | * can independently fail without needing to prevent future tasks from 14 | * running is to catch them individually: 15 | * 16 | * ```typescript 17 | import { gitP, SimpleGit, GitError, PullResult } from 'simple-git'; 18 | 19 | function catchTask (e: GitError) { 20 | return e. 21 | } 22 | 23 | const git = gitP(repoWorkingDir); 24 | const pulled: PullResult | GitError = await git.pull().catch(catchTask); 25 | const pushed: string | GitError = await git.pushTags().catch(catchTask); 26 | ``` 27 | */ 28 | export class GitError extends Error { 29 | constructor( 30 | public task?: SimpleGitTask, 31 | message?: string 32 | ) { 33 | super(message); 34 | Object.setPrototypeOf(this, new.target.prototype); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /simple-git/src/lib/errors/git-plugin-error.ts: -------------------------------------------------------------------------------- 1 | import { SimpleGitOptions, SimpleGitTask } from '../types'; 2 | import { GitError } from './git-error'; 3 | 4 | export class GitPluginError extends GitError { 5 | constructor( 6 | public task?: SimpleGitTask, 7 | public readonly plugin?: keyof SimpleGitOptions, 8 | message?: string 9 | ) { 10 | super(task, message); 11 | Object.setPrototypeOf(this, new.target.prototype); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /simple-git/src/lib/errors/git-response-error.ts: -------------------------------------------------------------------------------- 1 | import { GitError } from './git-error'; 2 | 3 | /** 4 | * The `GitResponseError` is the wrapper for a parsed response that is treated as 5 | * a fatal error, for example attempting a `merge` can leave the repo in a corrupted 6 | * state when there are conflicts so the task will reject rather than resolve. 7 | * 8 | * For example, catching the merge conflict exception: 9 | * 10 | * ```typescript 11 | import { gitP, SimpleGit, GitResponseError, MergeSummary } from 'simple-git'; 12 | 13 | const git = gitP(repoRoot); 14 | const mergeOptions: string[] = ['--no-ff', 'other-branch']; 15 | const mergeSummary: MergeSummary = await git.merge(mergeOptions) 16 | .catch((e: GitResponseError) => e.git); 17 | 18 | if (mergeSummary.failed) { 19 | // deal with the error 20 | } 21 | ``` 22 | */ 23 | export class GitResponseError extends GitError { 24 | constructor( 25 | /** 26 | * `.git` access the parsed response that is treated as being an error 27 | */ 28 | public readonly git: T, 29 | message?: string 30 | ) { 31 | super(undefined, message || String(git)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /simple-git/src/lib/errors/task-configuration-error.ts: -------------------------------------------------------------------------------- 1 | import { GitError } from './git-error'; 2 | 3 | /** 4 | * The `TaskConfigurationError` is thrown when a command was incorrectly 5 | * configured. An error of this kind means that no attempt was made to 6 | * run your command through the underlying `git` binary. 7 | * 8 | * Check the `.message` property for more detail on why your configuration 9 | * resulted in an error. 10 | */ 11 | export class TaskConfigurationError extends GitError { 12 | constructor(message?: string) { 13 | super(undefined, message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /simple-git/src/lib/parsers/parse-branch-delete.ts: -------------------------------------------------------------------------------- 1 | import { BranchMultiDeleteResult } from '../../../typings'; 2 | import { 3 | BranchDeletionBatch, 4 | branchDeletionFailure, 5 | branchDeletionSuccess, 6 | } from '../responses/BranchDeleteSummary'; 7 | import { TaskParser } from '../types'; 8 | import { ExitCodes, LineParser, parseStringResponse } from '../utils'; 9 | 10 | const deleteSuccessRegex = /(\S+)\s+\(\S+\s([^)]+)\)/; 11 | const deleteErrorRegex = /^error[^']+'([^']+)'/m; 12 | 13 | const parsers: LineParser[] = [ 14 | new LineParser(deleteSuccessRegex, (result, [branch, hash]) => { 15 | const deletion = branchDeletionSuccess(branch, hash); 16 | 17 | result.all.push(deletion); 18 | result.branches[branch] = deletion; 19 | }), 20 | new LineParser(deleteErrorRegex, (result, [branch]) => { 21 | const deletion = branchDeletionFailure(branch); 22 | 23 | result.errors.push(deletion); 24 | result.all.push(deletion); 25 | result.branches[branch] = deletion; 26 | }), 27 | ]; 28 | 29 | export const parseBranchDeletions: TaskParser = ( 30 | stdOut, 31 | stdErr 32 | ) => { 33 | return parseStringResponse(new BranchDeletionBatch(), parsers, [stdOut, stdErr]); 34 | }; 35 | 36 | export function hasBranchDeletionError(data: string, processExitCode: ExitCodes): boolean { 37 | return processExitCode === ExitCodes.ERROR && deleteErrorRegex.test(data); 38 | } 39 | -------------------------------------------------------------------------------- /simple-git/src/lib/parsers/parse-branch.ts: -------------------------------------------------------------------------------- 1 | import type { BranchSummary } from '../../../typings'; 2 | import { BranchSummaryResult } from '../responses/BranchSummary'; 3 | import { LineParser, parseStringResponse } from '../utils'; 4 | 5 | const parsers: LineParser[] = [ 6 | new LineParser( 7 | /^([*+]\s)?\((?:HEAD )?detached (?:from|at) (\S+)\)\s+([a-z0-9]+)\s(.*)$/, 8 | (result, [current, name, commit, label]) => { 9 | result.push(branchStatus(current), true, name, commit, label); 10 | } 11 | ), 12 | new LineParser( 13 | /^([*+]\s)?(\S+)\s+([a-z0-9]+)\s?(.*)$/s, 14 | (result, [current, name, commit, label]) => { 15 | result.push(branchStatus(current), false, name, commit, label); 16 | } 17 | ), 18 | ]; 19 | 20 | function branchStatus(input?: string) { 21 | return input ? input.charAt(0) : ''; 22 | } 23 | 24 | export function parseBranchSummary(stdOut: string): BranchSummary { 25 | return parseStringResponse(new BranchSummaryResult(), parsers, stdOut); 26 | } 27 | -------------------------------------------------------------------------------- /simple-git/src/lib/parsers/parse-commit.ts: -------------------------------------------------------------------------------- 1 | import { CommitResult } from '../../../typings'; 2 | import { LineParser, parseStringResponse } from '../utils'; 3 | 4 | const parsers: LineParser[] = [ 5 | new LineParser(/^\[([^\s]+)( \([^)]+\))? ([^\]]+)/, (result, [branch, root, commit]) => { 6 | result.branch = branch; 7 | result.commit = commit; 8 | result.root = !!root; 9 | }), 10 | new LineParser(/\s*Author:\s(.+)/i, (result, [author]) => { 11 | const parts = author.split('<'); 12 | const email = parts.pop(); 13 | 14 | if (!email || !email.includes('@')) { 15 | return; 16 | } 17 | 18 | result.author = { 19 | email: email.substr(0, email.length - 1), 20 | name: parts.join('<').trim(), 21 | }; 22 | }), 23 | new LineParser( 24 | /(\d+)[^,]*(?:,\s*(\d+)[^,]*)(?:,\s*(\d+))/g, 25 | (result, [changes, insertions, deletions]) => { 26 | result.summary.changes = parseInt(changes, 10) || 0; 27 | result.summary.insertions = parseInt(insertions, 10) || 0; 28 | result.summary.deletions = parseInt(deletions, 10) || 0; 29 | } 30 | ), 31 | new LineParser( 32 | /^(\d+)[^,]*(?:,\s*(\d+)[^(]+\(([+-]))?/, 33 | (result, [changes, lines, direction]) => { 34 | result.summary.changes = parseInt(changes, 10) || 0; 35 | const count = parseInt(lines, 10) || 0; 36 | if (direction === '-') { 37 | result.summary.deletions = count; 38 | } else if (direction === '+') { 39 | result.summary.insertions = count; 40 | } 41 | } 42 | ), 43 | ]; 44 | 45 | export function parseCommitResult(stdOut: string): CommitResult { 46 | const result: CommitResult = { 47 | author: null, 48 | branch: '', 49 | commit: '', 50 | root: false, 51 | summary: { 52 | changes: 0, 53 | insertions: 0, 54 | deletions: 0, 55 | }, 56 | }; 57 | return parseStringResponse(result, parsers, stdOut); 58 | } 59 | -------------------------------------------------------------------------------- /simple-git/src/lib/parsers/parse-fetch.ts: -------------------------------------------------------------------------------- 1 | import { FetchResult } from '../../../typings'; 2 | import { LineParser, parseStringResponse } from '../utils'; 3 | 4 | const parsers: LineParser[] = [ 5 | new LineParser(/From (.+)$/, (result, [remote]) => { 6 | result.remote = remote; 7 | }), 8 | new LineParser(/\* \[new branch]\s+(\S+)\s*-> (.+)$/, (result, [name, tracking]) => { 9 | result.branches.push({ 10 | name, 11 | tracking, 12 | }); 13 | }), 14 | new LineParser(/\* \[new tag]\s+(\S+)\s*-> (.+)$/, (result, [name, tracking]) => { 15 | result.tags.push({ 16 | name, 17 | tracking, 18 | }); 19 | }), 20 | new LineParser(/- \[deleted]\s+\S+\s*-> (.+)$/, (result, [tracking]) => { 21 | result.deleted.push({ 22 | tracking, 23 | }); 24 | }), 25 | new LineParser( 26 | /\s*([^.]+)\.\.(\S+)\s+(\S+)\s*-> (.+)$/, 27 | (result, [from, to, name, tracking]) => { 28 | result.updated.push({ 29 | name, 30 | tracking, 31 | to, 32 | from, 33 | }); 34 | } 35 | ), 36 | ]; 37 | 38 | export function parseFetchResult(stdOut: string, stdErr: string): FetchResult { 39 | const result: FetchResult = { 40 | raw: stdOut, 41 | remote: null, 42 | branches: [], 43 | tags: [], 44 | updated: [], 45 | deleted: [], 46 | }; 47 | return parseStringResponse(result, parsers, [stdOut, stdErr]); 48 | } 49 | -------------------------------------------------------------------------------- /simple-git/src/lib/parsers/parse-list-log-summary.ts: -------------------------------------------------------------------------------- 1 | import { ListLogLine, LogResult } from '../../../typings'; 2 | import { toLinesWithContent } from '../utils'; 3 | import { getDiffParser } from './parse-diff-summary'; 4 | import { LogFormat } from '../args/log-format'; 5 | 6 | export const START_BOUNDARY = 'òòòòòò '; 7 | 8 | export const COMMIT_BOUNDARY = ' òò'; 9 | 10 | export const SPLITTER = ' ò '; 11 | 12 | const defaultFieldNames = ['hash', 'date', 'message', 'refs', 'author_name', 'author_email']; 13 | 14 | function lineBuilder(tokens: string[], fields: string[]): any { 15 | return fields.reduce( 16 | (line, field, index) => { 17 | line[field] = tokens[index] || ''; 18 | return line; 19 | }, 20 | Object.create({ diff: null }) as any 21 | ); 22 | } 23 | 24 | export function createListLogSummaryParser( 25 | splitter = SPLITTER, 26 | fields = defaultFieldNames, 27 | logFormat = LogFormat.NONE 28 | ) { 29 | const parseDiffResult = getDiffParser(logFormat); 30 | 31 | return function (stdOut: string): LogResult { 32 | const all: ReadonlyArray = toLinesWithContent( 33 | stdOut.trim(), 34 | false, 35 | START_BOUNDARY 36 | ).map(function (item) { 37 | const lineDetail = item.split(COMMIT_BOUNDARY); 38 | const listLogLine: T & ListLogLine = lineBuilder(lineDetail[0].split(splitter), fields); 39 | 40 | if (lineDetail.length > 1 && !!lineDetail[1].trim()) { 41 | listLogLine.diff = parseDiffResult(lineDetail[1]); 42 | } 43 | 44 | return listLogLine; 45 | }); 46 | 47 | return { 48 | all, 49 | latest: (all.length && all[0]) || null, 50 | total: all.length, 51 | }; 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /simple-git/src/lib/parsers/parse-merge.ts: -------------------------------------------------------------------------------- 1 | import { MergeDetail, MergeResult } from '../../../typings'; 2 | import { MergeSummaryConflict, MergeSummaryDetail } from '../responses/MergeSummary'; 3 | import { TaskParser } from '../types'; 4 | import { LineParser, parseStringResponse } from '../utils'; 5 | import { parsePullResult } from './parse-pull'; 6 | 7 | const parsers: LineParser[] = [ 8 | new LineParser(/^Auto-merging\s+(.+)$/, (summary, [autoMerge]) => { 9 | summary.merges.push(autoMerge); 10 | }), 11 | new LineParser(/^CONFLICT\s+\((.+)\): Merge conflict in (.+)$/, (summary, [reason, file]) => { 12 | summary.conflicts.push(new MergeSummaryConflict(reason, file)); 13 | }), 14 | new LineParser( 15 | /^CONFLICT\s+\((.+\/delete)\): (.+) deleted in (.+) and/, 16 | (summary, [reason, file, deleteRef]) => { 17 | summary.conflicts.push(new MergeSummaryConflict(reason, file, { deleteRef })); 18 | } 19 | ), 20 | new LineParser(/^CONFLICT\s+\((.+)\):/, (summary, [reason]) => { 21 | summary.conflicts.push(new MergeSummaryConflict(reason, null)); 22 | }), 23 | new LineParser(/^Automatic merge failed;\s+(.+)$/, (summary, [result]) => { 24 | summary.result = result; 25 | }), 26 | ]; 27 | 28 | /** 29 | * Parse the complete response from `git.merge` 30 | */ 31 | export const parseMergeResult: TaskParser = (stdOut, stdErr) => { 32 | return Object.assign(parseMergeDetail(stdOut, stdErr), parsePullResult(stdOut, stdErr)); 33 | }; 34 | 35 | /** 36 | * Parse the merge specific detail (ie: not the content also available in the pull detail) from `git.mnerge` 37 | * @param stdOut 38 | */ 39 | export const parseMergeDetail: TaskParser = (stdOut) => { 40 | return parseStringResponse(new MergeSummaryDetail(), parsers, stdOut); 41 | }; 42 | -------------------------------------------------------------------------------- /simple-git/src/lib/parsers/parse-move.ts: -------------------------------------------------------------------------------- 1 | import { MoveResult } from '../../../typings'; 2 | import { LineParser, parseStringResponse } from '../utils'; 3 | 4 | const parsers: LineParser[] = [ 5 | new LineParser(/^Renaming (.+) to (.+)$/, (result, [from, to]) => { 6 | result.moves.push({ from, to }); 7 | }), 8 | ]; 9 | 10 | export function parseMoveResult(stdOut: string): MoveResult { 11 | return parseStringResponse({ moves: [] }, parsers, stdOut); 12 | } 13 | -------------------------------------------------------------------------------- /simple-git/src/lib/parsers/parse-remote-messages.ts: -------------------------------------------------------------------------------- 1 | import { PushResultRemoteMessages, RemoteMessageResult, RemoteMessages } from '../../../typings'; 2 | import { asNumber, parseStringResponse, RemoteLineParser } from '../utils'; 3 | import { remoteMessagesObjectParsers } from './parse-remote-objects'; 4 | 5 | const parsers: RemoteLineParser>[] = 6 | [ 7 | new RemoteLineParser(/^remote:\s*(.+)$/, (result, [text]) => { 8 | result.remoteMessages.all.push(text.trim()); 9 | return false; 10 | }), 11 | ...remoteMessagesObjectParsers, 12 | new RemoteLineParser( 13 | [/create a (?:pull|merge) request/i, /\s(https?:\/\/\S+)$/], 14 | (result, [pullRequestUrl]) => { 15 | (result.remoteMessages as PushResultRemoteMessages).pullRequestUrl = pullRequestUrl; 16 | } 17 | ), 18 | new RemoteLineParser( 19 | [/found (\d+) vulnerabilities.+\(([^)]+)\)/i, /\s(https?:\/\/\S+)$/], 20 | (result, [count, summary, url]) => { 21 | (result.remoteMessages as PushResultRemoteMessages).vulnerabilities = { 22 | count: asNumber(count), 23 | summary, 24 | url, 25 | }; 26 | } 27 | ), 28 | ]; 29 | 30 | export function parseRemoteMessages( 31 | _stdOut: string, 32 | stdErr: string 33 | ): RemoteMessageResult { 34 | return parseStringResponse({ remoteMessages: new RemoteMessageSummary() as T }, parsers, stdErr); 35 | } 36 | 37 | export class RemoteMessageSummary implements RemoteMessages { 38 | public readonly all: string[] = []; 39 | } 40 | -------------------------------------------------------------------------------- /simple-git/src/lib/plugins/abort-plugin.ts: -------------------------------------------------------------------------------- 1 | import { SimpleGitOptions } from '../types'; 2 | import { SimpleGitPlugin } from './simple-git-plugin'; 3 | import { GitPluginError } from '../errors/git-plugin-error'; 4 | 5 | export function abortPlugin(signal: SimpleGitOptions['abort']) { 6 | if (!signal) { 7 | return; 8 | } 9 | 10 | const onSpawnAfter: SimpleGitPlugin<'spawn.after'> = { 11 | type: 'spawn.after', 12 | action(_data, context) { 13 | function kill() { 14 | context.kill(new GitPluginError(undefined, 'abort', 'Abort signal received')); 15 | } 16 | 17 | signal.addEventListener('abort', kill); 18 | 19 | context.spawned.on('close', () => signal.removeEventListener('abort', kill)); 20 | }, 21 | }; 22 | 23 | const onSpawnBefore: SimpleGitPlugin<'spawn.before'> = { 24 | type: 'spawn.before', 25 | action(_data, context) { 26 | if (signal.aborted) { 27 | context.kill(new GitPluginError(undefined, 'abort', 'Abort already signaled')); 28 | } 29 | }, 30 | }; 31 | 32 | return [onSpawnBefore, onSpawnAfter]; 33 | } 34 | -------------------------------------------------------------------------------- /simple-git/src/lib/plugins/command-config-prefixing-plugin.ts: -------------------------------------------------------------------------------- 1 | import { prefixedArray } from '../utils'; 2 | import { SimpleGitPlugin } from './simple-git-plugin'; 3 | 4 | export function commandConfigPrefixingPlugin( 5 | configuration: string[] 6 | ): SimpleGitPlugin<'spawn.args'> { 7 | const prefix = prefixedArray(configuration, '-c'); 8 | 9 | return { 10 | type: 'spawn.args', 11 | action(data) { 12 | return [...prefix, ...data]; 13 | }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /simple-git/src/lib/plugins/custom-binary.plugin.ts: -------------------------------------------------------------------------------- 1 | import type { SimpleGitOptions } from '../types'; 2 | 3 | import { GitPluginError } from '../errors/git-plugin-error'; 4 | import { asArray } from '../utils'; 5 | import { PluginStore } from './plugin-store'; 6 | 7 | const WRONG_NUMBER_ERR = `Invalid value supplied for custom binary, requires a single string or an array containing either one or two strings`; 8 | const WRONG_CHARS_ERR = `Invalid value supplied for custom binary, restricted characters must be removed or supply the unsafe.allowUnsafeCustomBinary option`; 9 | 10 | function isBadArgument(arg: string) { 11 | return !arg || !/^([a-z]:)?([a-z0-9/.\\_-]+)$/i.test(arg); 12 | } 13 | 14 | function toBinaryConfig( 15 | input: string[], 16 | allowUnsafe: boolean 17 | ): { binary: string; prefix?: string } { 18 | if (input.length < 1 || input.length > 2) { 19 | throw new GitPluginError(undefined, 'binary', WRONG_NUMBER_ERR); 20 | } 21 | 22 | const isBad = input.some(isBadArgument); 23 | if (isBad) { 24 | if (allowUnsafe) { 25 | console.warn(WRONG_CHARS_ERR); 26 | } else { 27 | throw new GitPluginError(undefined, 'binary', WRONG_CHARS_ERR); 28 | } 29 | } 30 | 31 | const [binary, prefix] = input; 32 | return { 33 | binary, 34 | prefix, 35 | }; 36 | } 37 | 38 | export function customBinaryPlugin( 39 | plugins: PluginStore, 40 | input: SimpleGitOptions['binary'] = ['git'], 41 | allowUnsafe = false 42 | ) { 43 | let config = toBinaryConfig(asArray(input), allowUnsafe); 44 | 45 | plugins.on('binary', (input) => { 46 | config = toBinaryConfig(asArray(input), allowUnsafe); 47 | }); 48 | 49 | plugins.append('spawn.binary', () => { 50 | return config.binary; 51 | }); 52 | 53 | plugins.append('spawn.args', (data) => { 54 | return config.prefix ? [config.prefix, ...data] : data; 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /simple-git/src/lib/plugins/error-detection.plugin.ts: -------------------------------------------------------------------------------- 1 | import { GitError } from '../errors/git-error'; 2 | import { GitExecutorResult, SimpleGitPluginConfig } from '../types'; 3 | import { SimpleGitPlugin } from './simple-git-plugin'; 4 | 5 | type TaskResult = Omit; 6 | 7 | function isTaskError(result: TaskResult) { 8 | return !!(result.exitCode && result.stdErr.length); 9 | } 10 | 11 | function getErrorMessage(result: TaskResult) { 12 | return Buffer.concat([...result.stdOut, ...result.stdErr]); 13 | } 14 | 15 | export function errorDetectionHandler( 16 | overwrite = false, 17 | isError = isTaskError, 18 | errorMessage: (result: TaskResult) => Buffer | Error = getErrorMessage 19 | ) { 20 | return (error: Buffer | Error | undefined, result: TaskResult) => { 21 | if ((!overwrite && error) || !isError(result)) { 22 | return error; 23 | } 24 | 25 | return errorMessage(result); 26 | }; 27 | } 28 | 29 | export function errorDetectionPlugin( 30 | config: SimpleGitPluginConfig['errors'] 31 | ): SimpleGitPlugin<'task.error'> { 32 | return { 33 | type: 'task.error', 34 | action(data, context) { 35 | const error = config(data.error, { 36 | stdErr: context.stdErr, 37 | stdOut: context.stdOut, 38 | exitCode: context.exitCode, 39 | }); 40 | 41 | if (Buffer.isBuffer(error)) { 42 | return { error: new GitError(undefined, error.toString('utf-8')) }; 43 | } 44 | 45 | return { 46 | error, 47 | }; 48 | }, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /simple-git/src/lib/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export * from './abort-plugin'; 2 | export * from './block-unsafe-operations-plugin'; 3 | export * from './command-config-prefixing-plugin'; 4 | export * from './completion-detection.plugin'; 5 | export * from './custom-binary.plugin'; 6 | export * from './error-detection.plugin'; 7 | export * from './plugin-store'; 8 | export * from './progress-monitor-plugin'; 9 | export * from './simple-git-plugin'; 10 | export * from './spawn-options-plugin'; 11 | export * from './timout-plugin'; 12 | -------------------------------------------------------------------------------- /simple-git/src/lib/plugins/plugin-store.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'node:events'; 2 | 3 | import type { 4 | SimpleGitPlugin, 5 | SimpleGitPluginType, 6 | SimpleGitPluginTypes, 7 | } from './simple-git-plugin'; 8 | import { append, asArray } from '../utils'; 9 | import type { SimpleGitPluginConfig } from '../types'; 10 | 11 | export class PluginStore { 12 | private plugins: Set> = new Set(); 13 | private events = new EventEmitter(); 14 | 15 | on( 16 | type: K, 17 | listener: (data: SimpleGitPluginConfig[K]) => void 18 | ) { 19 | this.events.on(type, listener); 20 | } 21 | 22 | reconfigure(type: K, data: SimpleGitPluginConfig[K]) { 23 | this.events.emit(type, data); 24 | } 25 | 26 | public append(type: T, action: SimpleGitPlugin['action']) { 27 | const plugin = append(this.plugins, { type, action }); 28 | 29 | return () => this.plugins.delete(plugin); 30 | } 31 | 32 | public add( 33 | plugin: void | SimpleGitPlugin | SimpleGitPlugin[] 34 | ) { 35 | const plugins: SimpleGitPlugin[] = []; 36 | 37 | asArray(plugin).forEach((plugin) => plugin && this.plugins.add(append(plugins, plugin))); 38 | 39 | return () => { 40 | plugins.forEach((plugin) => this.plugins.delete(plugin)); 41 | }; 42 | } 43 | 44 | public exec( 45 | type: T, 46 | data: SimpleGitPluginTypes[T]['data'], 47 | context: SimpleGitPluginTypes[T]['context'] 48 | ): typeof data { 49 | let output = data; 50 | const contextual = Object.freeze(Object.create(context)); 51 | 52 | for (const plugin of this.plugins) { 53 | if (plugin.type === type) { 54 | output = plugin.action(output, contextual); 55 | } 56 | } 57 | 58 | return output; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /simple-git/src/lib/plugins/progress-monitor-plugin.ts: -------------------------------------------------------------------------------- 1 | import { SimpleGitOptions } from '../types'; 2 | import { asNumber, including } from '../utils'; 3 | 4 | import { SimpleGitPlugin } from './simple-git-plugin'; 5 | 6 | export function progressMonitorPlugin(progress: Exclude) { 7 | const progressCommand = '--progress'; 8 | const progressMethods = ['checkout', 'clone', 'fetch', 'pull', 'push']; 9 | 10 | const onProgress: SimpleGitPlugin<'spawn.after'> = { 11 | type: 'spawn.after', 12 | action(_data, context) { 13 | if (!context.commands.includes(progressCommand)) { 14 | return; 15 | } 16 | 17 | context.spawned.stderr?.on('data', (chunk: Buffer) => { 18 | const message = /^([\s\S]+?):\s*(\d+)% \((\d+)\/(\d+)\)/.exec(chunk.toString('utf8')); 19 | if (!message) { 20 | return; 21 | } 22 | 23 | progress({ 24 | method: context.method, 25 | stage: progressEventStage(message[1]), 26 | progress: asNumber(message[2]), 27 | processed: asNumber(message[3]), 28 | total: asNumber(message[4]), 29 | }); 30 | }); 31 | }, 32 | }; 33 | 34 | const onArgs: SimpleGitPlugin<'spawn.args'> = { 35 | type: 'spawn.args', 36 | action(args, context) { 37 | if (!progressMethods.includes(context.method)) { 38 | return args; 39 | } 40 | 41 | return including(args, progressCommand); 42 | }, 43 | }; 44 | 45 | return [onArgs, onProgress]; 46 | } 47 | 48 | function progressEventStage(input: string) { 49 | return String(input.toLowerCase().split(' ', 1)) || 'unknown'; 50 | } 51 | -------------------------------------------------------------------------------- /simple-git/src/lib/plugins/simple-git-plugin.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcess, SpawnOptions } from 'child_process'; 2 | import { GitExecutorResult } from '../types'; 3 | 4 | type SimpleGitTaskPluginContext = { 5 | readonly method: string; 6 | readonly commands: string[]; 7 | }; 8 | 9 | export interface SimpleGitPluginTypes { 10 | 'spawn.args': { 11 | data: string[]; 12 | context: SimpleGitTaskPluginContext & {}; 13 | }; 14 | 'spawn.binary': { 15 | data: string; 16 | context: SimpleGitTaskPluginContext & {}; 17 | }; 18 | 'spawn.options': { 19 | data: Partial; 20 | context: SimpleGitTaskPluginContext & {}; 21 | }; 22 | 'spawn.before': { 23 | data: void; 24 | context: SimpleGitTaskPluginContext & { 25 | kill(reason: Error): void; 26 | }; 27 | }; 28 | 'spawn.after': { 29 | data: void; 30 | context: SimpleGitTaskPluginContext & { 31 | spawned: ChildProcess; 32 | close(exitCode: number, reason?: Error): void; 33 | kill(reason: Error): void; 34 | }; 35 | }; 36 | 'task.error': { 37 | data: { error?: Error }; 38 | context: SimpleGitTaskPluginContext & GitExecutorResult; 39 | }; 40 | } 41 | 42 | export type SimpleGitPluginType = keyof SimpleGitPluginTypes; 43 | 44 | export interface SimpleGitPlugin { 45 | action( 46 | data: SimpleGitPluginTypes[T]['data'], 47 | context: SimpleGitPluginTypes[T]['context'] 48 | ): typeof data; 49 | 50 | type: T; 51 | } 52 | -------------------------------------------------------------------------------- /simple-git/src/lib/plugins/spawn-options-plugin.ts: -------------------------------------------------------------------------------- 1 | import { SpawnOptions } from 'child_process'; 2 | import { pick } from '../utils'; 3 | import { SimpleGitPlugin } from './simple-git-plugin'; 4 | 5 | export function spawnOptionsPlugin( 6 | spawnOptions: Partial 7 | ): SimpleGitPlugin<'spawn.options'> { 8 | const options = pick(spawnOptions, ['uid', 'gid']); 9 | 10 | return { 11 | type: 'spawn.options', 12 | action(data) { 13 | return { ...options, ...data }; 14 | }, 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /simple-git/src/lib/plugins/suffix-paths.plugin.ts: -------------------------------------------------------------------------------- 1 | import { SimpleGitPlugin } from './simple-git-plugin'; 2 | import { isPathSpec, toPaths } from '../args/pathspec'; 3 | 4 | export function suffixPathsPlugin(): SimpleGitPlugin<'spawn.args'> { 5 | return { 6 | type: 'spawn.args', 7 | action(data) { 8 | const prefix: string[] = []; 9 | let suffix: undefined | string[]; 10 | function append(args: string[]) { 11 | (suffix = suffix || []).push(...args); 12 | } 13 | 14 | for (let i = 0; i < data.length; i++) { 15 | const param = data[i]; 16 | 17 | if (isPathSpec(param)) { 18 | append(toPaths(param)); 19 | continue; 20 | } 21 | 22 | if (param === '--') { 23 | append( 24 | data.slice(i + 1).flatMap((item) => (isPathSpec(item) && toPaths(item)) || item) 25 | ); 26 | break; 27 | } 28 | 29 | prefix.push(param); 30 | } 31 | 32 | return !suffix ? prefix : [...prefix, '--', ...suffix.map(String)]; 33 | }, 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /simple-git/src/lib/plugins/timout-plugin.ts: -------------------------------------------------------------------------------- 1 | import type { SimpleGitPlugin } from './simple-git-plugin'; 2 | 3 | import type { SimpleGitOptions } from '../types'; 4 | import { GitPluginError } from '../errors/git-plugin-error'; 5 | 6 | export function timeoutPlugin({ 7 | block, 8 | stdErr = true, 9 | stdOut = true, 10 | }: Exclude): SimpleGitPlugin<'spawn.after'> | void { 11 | if (block > 0) { 12 | return { 13 | type: 'spawn.after', 14 | action(_data, context) { 15 | let timeout: NodeJS.Timeout; 16 | 17 | function wait() { 18 | timeout && clearTimeout(timeout); 19 | timeout = setTimeout(kill, block); 20 | } 21 | 22 | function stop() { 23 | context.spawned.stdout?.off('data', wait); 24 | context.spawned.stderr?.off('data', wait); 25 | context.spawned.off('exit', stop); 26 | context.spawned.off('close', stop); 27 | timeout && clearTimeout(timeout); 28 | } 29 | 30 | function kill() { 31 | stop(); 32 | context.kill(new GitPluginError(undefined, 'timeout', `block timeout reached`)); 33 | } 34 | 35 | stdOut && context.spawned.stdout?.on('data', wait); 36 | stdErr && context.spawned.stderr?.on('data', wait); 37 | context.spawned.on('exit', stop); 38 | context.spawned.on('close', stop); 39 | 40 | wait(); 41 | }, 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /simple-git/src/lib/responses/BranchDeleteSummary.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BranchMultiDeleteResult, 3 | BranchSingleDeleteFailure, 4 | BranchSingleDeleteResult, 5 | BranchSingleDeleteSuccess, 6 | } from '../../../typings'; 7 | 8 | export class BranchDeletionBatch implements BranchMultiDeleteResult { 9 | all: BranchSingleDeleteResult[] = []; 10 | branches: { [branchName: string]: BranchSingleDeleteResult } = {}; 11 | errors: BranchSingleDeleteResult[] = []; 12 | 13 | get success(): boolean { 14 | return !this.errors.length; 15 | } 16 | } 17 | 18 | export function branchDeletionSuccess(branch: string, hash: string): BranchSingleDeleteSuccess { 19 | return { 20 | branch, 21 | hash, 22 | success: true, 23 | }; 24 | } 25 | 26 | export function branchDeletionFailure(branch: string): BranchSingleDeleteFailure { 27 | return { 28 | branch, 29 | hash: null, 30 | success: false, 31 | }; 32 | } 33 | 34 | export function isSingleBranchDeleteFailure( 35 | test: BranchSingleDeleteResult 36 | ): test is BranchSingleDeleteSuccess { 37 | return test.success; 38 | } 39 | -------------------------------------------------------------------------------- /simple-git/src/lib/responses/BranchSummary.ts: -------------------------------------------------------------------------------- 1 | import type { BranchSummary, BranchSummaryBranch } from '../../../typings'; 2 | 3 | export enum BranchStatusIdentifier { 4 | CURRENT = '*', 5 | LINKED = '+', 6 | } 7 | 8 | export class BranchSummaryResult implements BranchSummary { 9 | public all: string[] = []; 10 | public branches: { [p: string]: BranchSummaryBranch } = {}; 11 | public current: string = ''; 12 | public detached: boolean = false; 13 | 14 | push( 15 | status: BranchStatusIdentifier | unknown, 16 | detached: boolean, 17 | name: string, 18 | commit: string, 19 | label: string 20 | ) { 21 | if (status === BranchStatusIdentifier.CURRENT) { 22 | this.detached = detached; 23 | this.current = name; 24 | } 25 | 26 | this.all.push(name); 27 | this.branches[name] = { 28 | current: status === BranchStatusIdentifier.CURRENT, 29 | linkedWorkTree: status === BranchStatusIdentifier.LINKED, 30 | name, 31 | commit, 32 | label, 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /simple-git/src/lib/responses/CheckIgnore.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Parser for the `check-ignore` command - returns each file as a string array 3 | */ 4 | export const parseCheckIgnore = (text: string): string[] => { 5 | return text 6 | .split(/\n/g) 7 | .map((line) => line.trim()) 8 | .filter((file) => !!file); 9 | }; 10 | -------------------------------------------------------------------------------- /simple-git/src/lib/responses/CleanSummary.ts: -------------------------------------------------------------------------------- 1 | import { CleanSummary } from '../../../typings'; 2 | import { toLinesWithContent } from '../utils'; 3 | 4 | export class CleanResponse implements CleanSummary { 5 | public paths: string[] = []; 6 | public files: string[] = []; 7 | public folders: string[] = []; 8 | 9 | constructor(public readonly dryRun: boolean) {} 10 | } 11 | 12 | const removalRegexp = /^[a-z]+\s*/i; 13 | const dryRunRemovalRegexp = /^[a-z]+\s+[a-z]+\s*/i; 14 | const isFolderRegexp = /\/$/; 15 | 16 | export function cleanSummaryParser(dryRun: boolean, text: string): CleanSummary { 17 | const summary = new CleanResponse(dryRun); 18 | const regexp = dryRun ? dryRunRemovalRegexp : removalRegexp; 19 | 20 | toLinesWithContent(text).forEach((line) => { 21 | const removed = line.replace(regexp, ''); 22 | 23 | summary.paths.push(removed); 24 | (isFolderRegexp.test(removed) ? summary.folders : summary.files).push(removed); 25 | }); 26 | 27 | return summary; 28 | } 29 | -------------------------------------------------------------------------------- /simple-git/src/lib/responses/DiffSummary.ts: -------------------------------------------------------------------------------- 1 | import { DiffResult, DiffResultBinaryFile, DiffResultTextFile } from '../../../typings'; 2 | 3 | /*** 4 | * The DiffSummary is returned as a response to getting `git().status()` 5 | */ 6 | export class DiffSummary implements DiffResult { 7 | changed = 0; 8 | deletions = 0; 9 | insertions = 0; 10 | 11 | files: Array = []; 12 | } 13 | -------------------------------------------------------------------------------- /simple-git/src/lib/responses/FileStatusSummary.ts: -------------------------------------------------------------------------------- 1 | import { FileStatusResult } from '../../../typings'; 2 | 3 | export const fromPathRegex = /^(.+)\0(.+)$/; 4 | 5 | export class FileStatusSummary implements FileStatusResult { 6 | public readonly from: string | undefined; 7 | 8 | constructor( 9 | public path: string, 10 | public index: string, 11 | public working_dir: string 12 | ) { 13 | if (index === 'R' || working_dir === 'R') { 14 | const detail = fromPathRegex.exec(path) || [null, path, path]; 15 | this.from = detail[2] || ''; 16 | this.path = detail[1] || ''; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /simple-git/src/lib/responses/GetRemoteSummary.ts: -------------------------------------------------------------------------------- 1 | import { forEachLineWithContent } from '../utils'; 2 | 3 | export interface RemoteWithoutRefs { 4 | name: string; 5 | } 6 | 7 | export interface RemoteWithRefs extends RemoteWithoutRefs { 8 | refs: { 9 | fetch: string; 10 | push: string; 11 | }; 12 | } 13 | 14 | export function parseGetRemotes(text: string): RemoteWithoutRefs[] { 15 | const remotes: { [name: string]: RemoteWithoutRefs } = {}; 16 | 17 | forEach(text, ([name]) => (remotes[name] = { name })); 18 | 19 | return Object.values(remotes); 20 | } 21 | 22 | export function parseGetRemotesVerbose(text: string): RemoteWithRefs[] { 23 | const remotes: { [name: string]: RemoteWithRefs } = {}; 24 | 25 | forEach(text, ([name, url, purpose]) => { 26 | if (!remotes.hasOwnProperty(name)) { 27 | remotes[name] = { 28 | name: name, 29 | refs: { fetch: '', push: '' }, 30 | }; 31 | } 32 | 33 | if (purpose && url) { 34 | remotes[name].refs[purpose.replace(/[^a-z]/g, '') as keyof RemoteWithRefs['refs']] = url; 35 | } 36 | }); 37 | 38 | return Object.values(remotes); 39 | } 40 | 41 | function forEach(text: string, handler: (line: string[]) => void) { 42 | forEachLineWithContent(text, (line) => handler(line.split(/\s+/))); 43 | } 44 | -------------------------------------------------------------------------------- /simple-git/src/lib/responses/InitSummary.ts: -------------------------------------------------------------------------------- 1 | import { InitResult } from '../../../typings'; 2 | 3 | export class InitSummary implements InitResult { 4 | constructor( 5 | public readonly bare: boolean, 6 | public readonly path: string, 7 | public readonly existing: boolean, 8 | public readonly gitDir: string 9 | ) {} 10 | } 11 | 12 | const initResponseRegex = /^Init.+ repository in (.+)$/; 13 | const reInitResponseRegex = /^Rein.+ in (.+)$/; 14 | 15 | export function parseInit(bare: boolean, path: string, text: string) { 16 | const response = String(text).trim(); 17 | let result; 18 | 19 | if ((result = initResponseRegex.exec(response))) { 20 | return new InitSummary(bare, path, false, result[1]); 21 | } 22 | 23 | if ((result = reInitResponseRegex.exec(response))) { 24 | return new InitSummary(bare, path, true, result[1]); 25 | } 26 | 27 | let gitDir = ''; 28 | const tokens = response.split(' '); 29 | while (tokens.length) { 30 | const token = tokens.shift(); 31 | if (token === 'in') { 32 | gitDir = tokens.join(' '); 33 | break; 34 | } 35 | } 36 | 37 | return new InitSummary(bare, path, /^re/i.test(response), gitDir); 38 | } 39 | -------------------------------------------------------------------------------- /simple-git/src/lib/responses/MergeSummary.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MergeConflict, 3 | MergeConflictDeletion, 4 | MergeDetail, 5 | MergeResultStatus, 6 | } from '../../../typings'; 7 | 8 | export class MergeSummaryConflict implements MergeConflict { 9 | constructor( 10 | public readonly reason: string, 11 | public readonly file: string | null = null, 12 | public readonly meta?: MergeConflictDeletion 13 | ) {} 14 | 15 | toString() { 16 | return `${this.file}:${this.reason}`; 17 | } 18 | } 19 | 20 | export class MergeSummaryDetail implements MergeDetail { 21 | public conflicts: MergeConflict[] = []; 22 | public merges: string[] = []; 23 | public result: MergeResultStatus = 'success'; 24 | 25 | get failed() { 26 | return this.conflicts.length > 0; 27 | } 28 | 29 | get reason() { 30 | return this.result; 31 | } 32 | 33 | toString() { 34 | if (this.conflicts.length) { 35 | return `CONFLICTS: ${this.conflicts.join(', ')}`; 36 | } 37 | 38 | return 'OK'; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /simple-git/src/lib/responses/PullSummary.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PullDetailFileChanges, 3 | PullDetailSummary, 4 | PullFailedResult, 5 | PullResult, 6 | } from '../../../typings'; 7 | 8 | export class PullSummary implements PullResult { 9 | public remoteMessages = { 10 | all: [], 11 | }; 12 | public created = []; 13 | public deleted: string[] = []; 14 | public files: string[] = []; 15 | public deletions: PullDetailFileChanges = {}; 16 | public insertions: PullDetailFileChanges = {}; 17 | public summary: PullDetailSummary = { 18 | changes: 0, 19 | deletions: 0, 20 | insertions: 0, 21 | }; 22 | } 23 | 24 | export class PullFailedSummary implements PullFailedResult { 25 | remote = ''; 26 | hash = { 27 | local: '', 28 | remote: '', 29 | }; 30 | branch = { 31 | local: '', 32 | remote: '', 33 | }; 34 | message = ''; 35 | 36 | toString() { 37 | return this.message; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /simple-git/src/lib/responses/TagList.ts: -------------------------------------------------------------------------------- 1 | import { TagResult } from '../../../typings'; 2 | 3 | export class TagList implements TagResult { 4 | constructor( 5 | public readonly all: string[], 6 | public readonly latest: string | undefined 7 | ) {} 8 | } 9 | 10 | export const parseTagList = function (data: string, customSort = false) { 11 | const tags = data.split('\n').map(trimmed).filter(Boolean); 12 | 13 | if (!customSort) { 14 | tags.sort(function (tagA, tagB) { 15 | const partsA = tagA.split('.'); 16 | const partsB = tagB.split('.'); 17 | 18 | if (partsA.length === 1 || partsB.length === 1) { 19 | return singleSorted(toNumber(partsA[0]), toNumber(partsB[0])); 20 | } 21 | 22 | for (let i = 0, l = Math.max(partsA.length, partsB.length); i < l; i++) { 23 | const diff = sorted(toNumber(partsA[i]), toNumber(partsB[i])); 24 | 25 | if (diff) { 26 | return diff; 27 | } 28 | } 29 | 30 | return 0; 31 | }); 32 | } 33 | 34 | const latest = customSort ? tags[0] : [...tags].reverse().find((tag) => tag.indexOf('.') >= 0); 35 | 36 | return new TagList(tags, latest); 37 | }; 38 | 39 | function singleSorted(a: number, b: number): number { 40 | const aIsNum = isNaN(a); 41 | const bIsNum = isNaN(b); 42 | 43 | if (aIsNum !== bIsNum) { 44 | return aIsNum ? 1 : -1; 45 | } 46 | 47 | return aIsNum ? sorted(a, b) : 0; 48 | } 49 | 50 | function sorted(a: number, b: number) { 51 | return a === b ? 0 : a > b ? 1 : -1; 52 | } 53 | 54 | function trimmed(input: string) { 55 | return input.trim(); 56 | } 57 | 58 | function toNumber(input: string | undefined) { 59 | if (typeof input === 'string') { 60 | return parseInt(input.replace(/^\D+/g, ''), 10) || 0; 61 | } 62 | 63 | return 0; 64 | } 65 | -------------------------------------------------------------------------------- /simple-git/src/lib/runners/git-executor.ts: -------------------------------------------------------------------------------- 1 | import type { PluginStore } from '../plugins'; 2 | import type { GitExecutorEnv, outputHandler, SimpleGitExecutor, SimpleGitTask } from '../types'; 3 | 4 | import { GitExecutorChain } from './git-executor-chain'; 5 | import { Scheduler } from './scheduler'; 6 | 7 | export class GitExecutor implements SimpleGitExecutor { 8 | private _chain = new GitExecutorChain(this, this._scheduler, this._plugins); 9 | 10 | public env: GitExecutorEnv; 11 | public outputHandler?: outputHandler; 12 | 13 | constructor( 14 | public cwd: string, 15 | private _scheduler: Scheduler, 16 | private _plugins: PluginStore 17 | ) {} 18 | 19 | chain(): SimpleGitExecutor { 20 | return new GitExecutorChain(this, this._scheduler, this._plugins); 21 | } 22 | 23 | push(task: SimpleGitTask): Promise { 24 | return this._chain.push(task); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /simple-git/src/lib/runners/scheduler.ts: -------------------------------------------------------------------------------- 1 | import { append, remove } from '../utils'; 2 | import { createDeferred, DeferredPromise } from '@kwsites/promise-deferred'; 3 | import { createLogger } from '../git-logger'; 4 | 5 | type ScheduleCompleteCallback = () => void; 6 | type ScheduledTask = Pick, 'promise' | 'done'> & { 7 | id: number; 8 | }; 9 | 10 | const createScheduledTask: () => ScheduledTask = (() => { 11 | let id = 0; 12 | return () => { 13 | id++; 14 | const { promise, done } = createDeferred(); 15 | 16 | return { 17 | promise, 18 | done, 19 | id, 20 | }; 21 | }; 22 | })(); 23 | 24 | export class Scheduler { 25 | private logger = createLogger('', 'scheduler'); 26 | private pending: ScheduledTask[] = []; 27 | private running: ScheduledTask[] = []; 28 | 29 | constructor(private concurrency = 2) { 30 | this.logger(`Constructed, concurrency=%s`, concurrency); 31 | } 32 | 33 | private schedule() { 34 | if (!this.pending.length || this.running.length >= this.concurrency) { 35 | this.logger( 36 | `Schedule attempt ignored, pending=%s running=%s concurrency=%s`, 37 | this.pending.length, 38 | this.running.length, 39 | this.concurrency 40 | ); 41 | return; 42 | } 43 | 44 | const task = append(this.running, this.pending.shift()!); 45 | this.logger(`Attempting id=%s`, task.id); 46 | task.done(() => { 47 | this.logger(`Completing id=`, task.id); 48 | remove(this.running, task); 49 | this.schedule(); 50 | }); 51 | } 52 | 53 | next(): Promise { 54 | const { promise, id } = append(this.pending, createScheduledTask()); 55 | this.logger(`Scheduling id=%s`, id); 56 | 57 | this.schedule(); 58 | 59 | return promise; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /simple-git/src/lib/task-callback.ts: -------------------------------------------------------------------------------- 1 | import { GitError } from './errors/git-error'; 2 | import { GitResponseError } from './errors/git-response-error'; 3 | import { SimpleGitTask, SimpleGitTaskCallback } from './types'; 4 | import { NOOP } from './utils'; 5 | 6 | export function taskCallback( 7 | task: SimpleGitTask, 8 | response: Promise, 9 | callback: SimpleGitTaskCallback = NOOP 10 | ) { 11 | const onSuccess = (data: R) => { 12 | callback(null, data); 13 | }; 14 | 15 | const onError = (err: GitError | GitResponseError) => { 16 | if (err?.task === task) { 17 | callback( 18 | err instanceof GitResponseError ? addDeprecationNoticeToError(err) : err, 19 | undefined as any 20 | ); 21 | } 22 | }; 23 | 24 | response.then(onSuccess, onError); 25 | } 26 | 27 | function addDeprecationNoticeToError(err: GitResponseError) { 28 | let log = (name: string) => { 29 | console.warn( 30 | `simple-git deprecation notice: accessing GitResponseError.${name} should be GitResponseError.git.${name}, this will no longer be available in version 3` 31 | ); 32 | log = NOOP; 33 | }; 34 | 35 | return Object.create(err, Object.getOwnPropertyNames(err.git).reduce(descriptorReducer, {})); 36 | 37 | function descriptorReducer(all: PropertyDescriptorMap, name: string): typeof all { 38 | if (name in err) { 39 | return all; 40 | } 41 | 42 | all[name] = { 43 | enumerable: false, 44 | configurable: false, 45 | get() { 46 | log(name); 47 | return err.git[name]; 48 | }, 49 | }; 50 | 51 | return all; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/apply-patch.ts: -------------------------------------------------------------------------------- 1 | import { straightThroughStringTask } from './task'; 2 | import { OptionFlags, Options, StringTask } from '../types'; 3 | 4 | export type ApplyOptions = Options & 5 | OptionFlags< 6 | | '--stat' 7 | | '--numstat' 8 | | '--summary' 9 | | '--check' 10 | | '--index' 11 | | '--intent-to-add' 12 | | '--3way' 13 | | '--apply' 14 | | '--no-add' 15 | | '-R' 16 | | '--reverse' 17 | | '--allow-binary-replacement' 18 | | '--binary' 19 | | '--reject' 20 | | '-z' 21 | | '--inaccurate-eof' 22 | | '--recount' 23 | | '--cached' 24 | | '--ignore-space-change' 25 | | '--ignore-whitespace' 26 | | '--verbose' 27 | | '--unsafe-paths' 28 | > & 29 | OptionFlags<'--whitespace', 'nowarn' | 'warn' | 'fix' | 'error' | 'error-all'> & 30 | OptionFlags<'--build-fake-ancestor' | '--exclude' | '--include' | '--directory', string> & 31 | OptionFlags<'-p' | '-C', number>; 32 | 33 | export function applyPatchTask(patches: string[], customArgs: string[]): StringTask { 34 | return straightThroughStringTask(['apply', ...customArgs, ...patches]); 35 | } 36 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/change-working-directory.ts: -------------------------------------------------------------------------------- 1 | import { folderExists } from '../utils'; 2 | import { SimpleGitExecutor } from '../types'; 3 | import { adhocExecTask } from './task'; 4 | 5 | export function changeWorkingDirectoryTask(directory: string, root?: SimpleGitExecutor) { 6 | return adhocExecTask((instance: SimpleGitExecutor) => { 7 | if (!folderExists(directory)) { 8 | throw new Error(`Git.cwd: cannot change to non-directory "${directory}"`); 9 | } 10 | 11 | return ((root || instance).cwd = directory); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/check-ignore.ts: -------------------------------------------------------------------------------- 1 | import { StringTask } from '../types'; 2 | import { parseCheckIgnore } from '../responses/CheckIgnore'; 3 | 4 | export function checkIgnoreTask(paths: string[]): StringTask { 5 | return { 6 | commands: ['check-ignore', ...paths], 7 | format: 'utf-8', 8 | parser: parseCheckIgnore, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/check-is-repo.ts: -------------------------------------------------------------------------------- 1 | import { ExitCodes } from '../utils'; 2 | import { Maybe, StringTask } from '../types'; 3 | 4 | export enum CheckRepoActions { 5 | BARE = 'bare', 6 | IN_TREE = 'tree', 7 | IS_REPO_ROOT = 'root', 8 | } 9 | 10 | const onError: StringTask['onError'] = ({ exitCode }, error, done, fail) => { 11 | if (exitCode === ExitCodes.UNCLEAN && isNotRepoMessage(error)) { 12 | return done(Buffer.from('false')); 13 | } 14 | 15 | fail(error); 16 | }; 17 | 18 | const parser: StringTask['parser'] = (text) => { 19 | return text.trim() === 'true'; 20 | }; 21 | 22 | export function checkIsRepoTask(action: Maybe): StringTask { 23 | switch (action) { 24 | case CheckRepoActions.BARE: 25 | return checkIsBareRepoTask(); 26 | case CheckRepoActions.IS_REPO_ROOT: 27 | return checkIsRepoRootTask(); 28 | } 29 | 30 | const commands = ['rev-parse', '--is-inside-work-tree']; 31 | 32 | return { 33 | commands, 34 | format: 'utf-8', 35 | onError, 36 | parser, 37 | }; 38 | } 39 | 40 | export function checkIsRepoRootTask(): StringTask { 41 | const commands = ['rev-parse', '--git-dir']; 42 | 43 | return { 44 | commands, 45 | format: 'utf-8', 46 | onError, 47 | parser(path) { 48 | return /^\.(git)?$/.test(path.trim()); 49 | }, 50 | }; 51 | } 52 | 53 | export function checkIsBareRepoTask(): StringTask { 54 | const commands = ['rev-parse', '--is-bare-repository']; 55 | 56 | return { 57 | commands, 58 | format: 'utf-8', 59 | onError, 60 | parser, 61 | }; 62 | } 63 | 64 | function isNotRepoMessage(error: Error): boolean { 65 | return /(Not a git repository|Kein Git-Repository)/i.test(String(error)); 66 | } 67 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/checkout.ts: -------------------------------------------------------------------------------- 1 | import type { SimpleGit } from '../../../typings'; 2 | import type { SimpleGitApi } from '../simple-git-api'; 3 | import { getTrailingOptions, remove, trailingFunctionArgument } from '../utils'; 4 | import { straightThroughStringTask } from './task'; 5 | 6 | function checkoutTask(args: string[]) { 7 | const commands = ['checkout', ...args]; 8 | if (commands[1] === '-b' && commands.includes('-B')) { 9 | commands[1] = remove(commands, '-B'); 10 | } 11 | 12 | return straightThroughStringTask(commands); 13 | } 14 | 15 | export default function (): Pick { 16 | return { 17 | checkout(this: SimpleGitApi) { 18 | return this._runTask( 19 | checkoutTask(getTrailingOptions(arguments, 1)), 20 | trailingFunctionArgument(arguments) 21 | ); 22 | }, 23 | 24 | checkoutBranch(this: SimpleGitApi, branchName, startPoint) { 25 | return this._runTask( 26 | checkoutTask(['-b', branchName, startPoint, ...getTrailingOptions(arguments)]), 27 | trailingFunctionArgument(arguments) 28 | ); 29 | }, 30 | 31 | checkoutLocalBranch(this: SimpleGitApi, branchName) { 32 | return this._runTask( 33 | checkoutTask(['-b', branchName, ...getTrailingOptions(arguments)]), 34 | trailingFunctionArgument(arguments) 35 | ); 36 | }, 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/clone.ts: -------------------------------------------------------------------------------- 1 | import { configurationErrorTask, EmptyTask, straightThroughStringTask } from './task'; 2 | import { OptionFlags, Options, StringTask } from '../types'; 3 | import { append, filterString } from '../utils'; 4 | 5 | export type CloneOptions = Options & 6 | OptionFlags< 7 | | '--bare' 8 | | '--dissociate' 9 | | '--mirror' 10 | | '--no-checkout' 11 | | '--no-remote-submodules' 12 | | '--no-shallow-submodules' 13 | | '--no-single-branch' 14 | | '--no-tags' 15 | | '--remote-submodules' 16 | | '--single-branch' 17 | | '--shallow-submodules' 18 | | '--verbose' 19 | > & 20 | OptionFlags<'--depth' | '-j' | '--jobs', number> & 21 | OptionFlags< 22 | | '--branch' 23 | | '--origin' 24 | | '--recurse-submodules' 25 | | '--separate-git-dir' 26 | | '--shallow-exclude' 27 | | '--shallow-since' 28 | | '--template', 29 | string 30 | >; 31 | 32 | function disallowedCommand(command: string) { 33 | return /^--upload-pack(=|$)/.test(command); 34 | } 35 | 36 | export function cloneTask( 37 | repo: string | undefined, 38 | directory: string | undefined, 39 | customArgs: string[] 40 | ): StringTask | EmptyTask { 41 | const commands = ['clone', ...customArgs]; 42 | 43 | filterString(repo) && commands.push(repo); 44 | filterString(directory) && commands.push(directory); 45 | 46 | const banned = commands.find(disallowedCommand); 47 | if (banned) { 48 | return configurationErrorTask(`git.fetch: potential exploit argument blocked.`); 49 | } 50 | 51 | return straightThroughStringTask(commands); 52 | } 53 | 54 | export function cloneMirrorTask( 55 | repo: string | undefined, 56 | directory: string | undefined, 57 | customArgs: string[] 58 | ) { 59 | append(customArgs, '--mirror'); 60 | 61 | return cloneTask(repo, directory, customArgs); 62 | } 63 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/commit.ts: -------------------------------------------------------------------------------- 1 | import type { CommitResult, SimpleGit } from '../../../typings'; 2 | import type { SimpleGitApi } from '../simple-git-api'; 3 | import type { StringTask } from '../types'; 4 | import { parseCommitResult } from '../parsers/parse-commit'; 5 | import { 6 | asArray, 7 | filterArray, 8 | filterStringOrStringArray, 9 | filterType, 10 | getTrailingOptions, 11 | prefixedArray, 12 | trailingFunctionArgument, 13 | } from '../utils'; 14 | import { configurationErrorTask } from './task'; 15 | 16 | export function commitTask( 17 | message: string[], 18 | files: string[], 19 | customArgs: string[] 20 | ): StringTask { 21 | const commands: string[] = [ 22 | '-c', 23 | 'core.abbrev=40', 24 | 'commit', 25 | ...prefixedArray(message, '-m'), 26 | ...files, 27 | ...customArgs, 28 | ]; 29 | 30 | return { 31 | commands, 32 | format: 'utf-8', 33 | parser: parseCommitResult, 34 | }; 35 | } 36 | 37 | export default function (): Pick { 38 | return { 39 | commit(this: SimpleGitApi, message: string | string[], ...rest: unknown[]) { 40 | const next = trailingFunctionArgument(arguments); 41 | const task = 42 | rejectDeprecatedSignatures(message) || 43 | commitTask( 44 | asArray(message), 45 | asArray(filterType(rest[0], filterStringOrStringArray, [])), 46 | [...filterType(rest[1], filterArray, []), ...getTrailingOptions(arguments, 0, true)] 47 | ); 48 | 49 | return this._runTask(task, next); 50 | }, 51 | }; 52 | 53 | function rejectDeprecatedSignatures(message?: unknown) { 54 | return ( 55 | !filterStringOrStringArray(message) && 56 | configurationErrorTask( 57 | `git.commit: requires the commit message to be supplied as a string/string[]` 58 | ) 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/count-objects.ts: -------------------------------------------------------------------------------- 1 | import type { SimpleGitApi } from '../simple-git-api'; 2 | import type { SimpleGit } from '../../../typings'; 3 | import { asCamelCase, asNumber, LineParser, parseStringResponse } from '../utils'; 4 | 5 | export interface CountObjectsResult { 6 | count: number; 7 | size: number; 8 | inPack: number; 9 | packs: number; 10 | sizePack: number; 11 | prunePackable: number; 12 | garbage: number; 13 | sizeGarbage: number; 14 | } 15 | 16 | function countObjectsResponse(): CountObjectsResult { 17 | return { 18 | count: 0, 19 | garbage: 0, 20 | inPack: 0, 21 | packs: 0, 22 | prunePackable: 0, 23 | size: 0, 24 | sizeGarbage: 0, 25 | sizePack: 0, 26 | }; 27 | } 28 | 29 | const parser: LineParser = new LineParser( 30 | /([a-z-]+): (\d+)$/, 31 | (result, [key, value]) => { 32 | const property = asCamelCase(key); 33 | if (result.hasOwnProperty(property)) { 34 | result[property as keyof typeof result] = asNumber(value); 35 | } 36 | } 37 | ); 38 | 39 | export default function (): Pick { 40 | return { 41 | countObjects(this: SimpleGitApi) { 42 | return this._runTask({ 43 | commands: ['count-objects', '--verbose'], 44 | format: 'utf-8', 45 | parser(stdOut: string) { 46 | return parseStringResponse(countObjectsResponse(), [parser], stdOut); 47 | }, 48 | }); 49 | }, 50 | }; 51 | } 52 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/diff-name-status.ts: -------------------------------------------------------------------------------- 1 | export enum DiffNameStatus { 2 | ADDED = 'A', 3 | COPIED = 'C', 4 | DELETED = 'D', 5 | MODIFIED = 'M', 6 | RENAMED = 'R', 7 | CHANGED = 'T', 8 | UNMERGED = 'U', 9 | UNKNOWN = 'X', 10 | BROKEN = 'B', 11 | } 12 | 13 | const diffNameStatus = new Set(Object.values(DiffNameStatus)); 14 | 15 | export function isDiffNameStatus(input: string): input is DiffNameStatus { 16 | return diffNameStatus.has(input as DiffNameStatus); 17 | } 18 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/diff.ts: -------------------------------------------------------------------------------- 1 | import { StringTask } from '../types'; 2 | import { DiffResult } from '../../../typings'; 3 | import { isLogFormat, LogFormat, logFormatFromCommand } from '../args/log-format'; 4 | import { getDiffParser } from '../parsers/parse-diff-summary'; 5 | import { configurationErrorTask, EmptyTask } from './task'; 6 | 7 | export function diffSummaryTask(customArgs: string[]): StringTask | EmptyTask { 8 | let logFormat = logFormatFromCommand(customArgs); 9 | 10 | const commands = ['diff']; 11 | 12 | if (logFormat === LogFormat.NONE) { 13 | logFormat = LogFormat.STAT; 14 | commands.push('--stat=4096'); 15 | } 16 | 17 | commands.push(...customArgs); 18 | 19 | return ( 20 | validateLogFormatConfig(commands) || { 21 | commands, 22 | format: 'utf-8', 23 | parser: getDiffParser(logFormat), 24 | } 25 | ); 26 | } 27 | 28 | export function validateLogFormatConfig(customArgs: unknown[]): EmptyTask | void { 29 | const flags = customArgs.filter(isLogFormat); 30 | 31 | if (flags.length > 1) { 32 | return configurationErrorTask( 33 | `Summary flags are mutually exclusive - pick one of ${flags.join(',')}` 34 | ); 35 | } 36 | 37 | if (flags.length && customArgs.includes('-z')) { 38 | return configurationErrorTask( 39 | `Summary flag ${flags} parsing is not compatible with null termination option '-z'` 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/fetch.ts: -------------------------------------------------------------------------------- 1 | import { FetchResult } from '../../../typings'; 2 | import { parseFetchResult } from '../parsers/parse-fetch'; 3 | import { StringTask } from '../types'; 4 | 5 | import { configurationErrorTask, EmptyTask } from './task'; 6 | 7 | function disallowedCommand(command: string) { 8 | return /^--upload-pack(=|$)/.test(command); 9 | } 10 | 11 | export function fetchTask( 12 | remote: string, 13 | branch: string, 14 | customArgs: string[] 15 | ): StringTask | EmptyTask { 16 | const commands = ['fetch', ...customArgs]; 17 | if (remote && branch) { 18 | commands.push(remote, branch); 19 | } 20 | 21 | const banned = commands.find(disallowedCommand); 22 | if (banned) { 23 | return configurationErrorTask(`git.fetch: potential exploit argument blocked.`); 24 | } 25 | 26 | return { 27 | commands, 28 | format: 'utf-8', 29 | parser: parseFetchResult, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/first-commit.ts: -------------------------------------------------------------------------------- 1 | import { Response, SimpleGit } from '../../../typings'; 2 | import { SimpleGitApi } from '../simple-git-api'; 3 | import { trailingFunctionArgument } from '../utils'; 4 | import { straightThroughStringTask } from './task'; 5 | 6 | export default function (): Pick { 7 | return { 8 | firstCommit(this: SimpleGitApi): Response { 9 | return this._runTask( 10 | straightThroughStringTask(['rev-list', '--max-parents=0', 'HEAD'], true), 11 | trailingFunctionArgument(arguments) 12 | ); 13 | }, 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/hash-object.ts: -------------------------------------------------------------------------------- 1 | import { straightThroughStringTask } from './task'; 2 | import { StringTask } from '../types'; 3 | 4 | /** 5 | * Task used by `git.hashObject` 6 | */ 7 | export function hashObjectTask(filePath: string, write: boolean): StringTask { 8 | const commands = ['hash-object', filePath]; 9 | if (write) { 10 | commands.push('-w'); 11 | } 12 | 13 | return straightThroughStringTask(commands, true); 14 | } 15 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/init.ts: -------------------------------------------------------------------------------- 1 | import { InitResult } from '../../../typings'; 2 | import { parseInit } from '../responses/InitSummary'; 3 | import { StringTask } from '../types'; 4 | 5 | const bareCommand = '--bare'; 6 | 7 | function hasBareCommand(command: string[]) { 8 | return command.includes(bareCommand); 9 | } 10 | 11 | export function initTask(bare = false, path: string, customArgs: string[]): StringTask { 12 | const commands = ['init', ...customArgs]; 13 | if (bare && !hasBareCommand(commands)) { 14 | commands.splice(1, 0, bareCommand); 15 | } 16 | 17 | return { 18 | commands, 19 | format: 'utf-8', 20 | parser(text: string): InitResult { 21 | return parseInit(commands.includes('--bare'), path, text); 22 | }, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/merge.ts: -------------------------------------------------------------------------------- 1 | import { MergeResult } from '../../../typings'; 2 | import { GitResponseError } from '../errors/git-response-error'; 3 | import { parseMergeResult } from '../parsers/parse-merge'; 4 | import { StringTask } from '../types'; 5 | import { configurationErrorTask, EmptyTask } from './task'; 6 | 7 | export function mergeTask(customArgs: string[]): EmptyTask | StringTask { 8 | if (!customArgs.length) { 9 | return configurationErrorTask('Git.merge requires at least one option'); 10 | } 11 | 12 | return { 13 | commands: ['merge', ...customArgs], 14 | format: 'utf-8', 15 | parser(stdOut, stdErr): MergeResult { 16 | const merge = parseMergeResult(stdOut, stdErr); 17 | if (merge.failed) { 18 | throw new GitResponseError(merge); 19 | } 20 | 21 | return merge; 22 | }, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/move.ts: -------------------------------------------------------------------------------- 1 | import { MoveResult } from '../../../typings'; 2 | import { parseMoveResult } from '../parsers/parse-move'; 3 | import { StringTask } from '../types'; 4 | import { asArray } from '../utils'; 5 | 6 | export function moveTask(from: string | string[], to: string): StringTask { 7 | return { 8 | commands: ['mv', '-v', ...asArray(from), to], 9 | format: 'utf-8', 10 | parser: parseMoveResult, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/pull.ts: -------------------------------------------------------------------------------- 1 | import { PullResult } from '../../../typings'; 2 | import { GitResponseError } from '../errors/git-response-error'; 3 | import { parsePullErrorResult, parsePullResult } from '../parsers/parse-pull'; 4 | import { Maybe, StringTask } from '../types'; 5 | import { bufferToString } from '../utils'; 6 | 7 | export function pullTask( 8 | remote: Maybe, 9 | branch: Maybe, 10 | customArgs: string[] 11 | ): StringTask { 12 | const commands: string[] = ['pull', ...customArgs]; 13 | if (remote && branch) { 14 | commands.splice(1, 0, remote, branch); 15 | } 16 | 17 | return { 18 | commands, 19 | format: 'utf-8', 20 | parser(stdOut, stdErr): PullResult { 21 | return parsePullResult(stdOut, stdErr); 22 | }, 23 | onError(result, _error, _done, fail) { 24 | const pullError = parsePullErrorResult( 25 | bufferToString(result.stdOut), 26 | bufferToString(result.stdErr) 27 | ); 28 | if (pullError) { 29 | return fail(new GitResponseError(pullError)); 30 | } 31 | 32 | fail(_error); 33 | }, 34 | }; 35 | } 36 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/push.ts: -------------------------------------------------------------------------------- 1 | import { PushResult } from '../../../typings'; 2 | import { parsePushResult as parser } from '../parsers/parse-push'; 3 | import { StringTask } from '../types'; 4 | import { append, remove } from '../utils'; 5 | 6 | type PushRef = { remote?: string; branch?: string }; 7 | 8 | export function pushTagsTask(ref: PushRef = {}, customArgs: string[]): StringTask { 9 | append(customArgs, '--tags'); 10 | return pushTask(ref, customArgs); 11 | } 12 | 13 | export function pushTask(ref: PushRef = {}, customArgs: string[]): StringTask { 14 | const commands = ['push', ...customArgs]; 15 | if (ref.branch) { 16 | commands.splice(1, 0, ref.branch); 17 | } 18 | if (ref.remote) { 19 | commands.splice(1, 0, ref.remote); 20 | } 21 | 22 | remove(commands, '-v'); 23 | append(commands, '--verbose'); 24 | append(commands, '--porcelain'); 25 | 26 | return { 27 | commands, 28 | format: 'utf-8', 29 | parser, 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/remote.ts: -------------------------------------------------------------------------------- 1 | import { parseGetRemotes, parseGetRemotesVerbose } from '../responses/GetRemoteSummary'; 2 | import { StringTask } from '../types'; 3 | import { straightThroughStringTask } from './task'; 4 | 5 | export function addRemoteTask( 6 | remoteName: string, 7 | remoteRepo: string, 8 | customArgs: string[] 9 | ): StringTask { 10 | return straightThroughStringTask(['remote', 'add', ...customArgs, remoteName, remoteRepo]); 11 | } 12 | 13 | export function getRemotesTask(verbose: boolean): StringTask { 14 | const commands = ['remote']; 15 | if (verbose) { 16 | commands.push('-v'); 17 | } 18 | 19 | return { 20 | commands, 21 | format: 'utf-8', 22 | parser: verbose ? parseGetRemotesVerbose : parseGetRemotes, 23 | }; 24 | } 25 | 26 | export function listRemotesTask(customArgs: string[]): StringTask { 27 | const commands = [...customArgs]; 28 | if (commands[0] !== 'ls-remote') { 29 | commands.unshift('ls-remote'); 30 | } 31 | 32 | return straightThroughStringTask(commands); 33 | } 34 | 35 | export function remoteTask(customArgs: string[]): StringTask { 36 | const commands = [...customArgs]; 37 | if (commands[0] !== 'remote') { 38 | commands.unshift('remote'); 39 | } 40 | 41 | return straightThroughStringTask(commands); 42 | } 43 | 44 | export function removeRemoteTask(remoteName: string) { 45 | return straightThroughStringTask(['remote', 'remove', remoteName]); 46 | } 47 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/reset.ts: -------------------------------------------------------------------------------- 1 | import { straightThroughStringTask } from './task'; 2 | import { Maybe, OptionFlags, Options } from '../types'; 3 | 4 | export enum ResetMode { 5 | MIXED = 'mixed', 6 | SOFT = 'soft', 7 | HARD = 'hard', 8 | MERGE = 'merge', 9 | KEEP = 'keep', 10 | } 11 | 12 | const ResetModes = Array.from(Object.values(ResetMode)); 13 | 14 | export type ResetOptions = Options & 15 | OptionFlags<'-q' | '--quiet' | '--no-quiet' | '--pathspec-from-nul'> & 16 | OptionFlags<'--pathspec-from-file', string>; 17 | 18 | export function resetTask(mode: Maybe, customArgs: string[]) { 19 | const commands: string[] = ['reset']; 20 | if (isValidResetMode(mode)) { 21 | commands.push(`--${mode}`); 22 | } 23 | commands.push(...customArgs); 24 | 25 | return straightThroughStringTask(commands); 26 | } 27 | 28 | export function getResetMode(mode: ResetMode | any): Maybe { 29 | if (isValidResetMode(mode)) { 30 | return mode; 31 | } 32 | 33 | switch (typeof mode) { 34 | case 'string': 35 | case 'undefined': 36 | return ResetMode.SOFT; 37 | } 38 | 39 | return; 40 | } 41 | 42 | function isValidResetMode(mode: ResetMode | any): mode is ResetMode { 43 | return ResetModes.includes(mode); 44 | } 45 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/show.ts: -------------------------------------------------------------------------------- 1 | import { SimpleGit } from '../../../typings'; 2 | import { SimpleGitApi } from '../simple-git-api'; 3 | import { getTrailingOptions, trailingFunctionArgument } from '../utils'; 4 | import { straightThroughBufferTask, straightThroughStringTask } from './task'; 5 | 6 | export default function (): Pick { 7 | return { 8 | showBuffer(this: SimpleGitApi) { 9 | const commands = ['show', ...getTrailingOptions(arguments, 1)]; 10 | if (!commands.includes('--binary')) { 11 | commands.splice(1, 0, '--binary'); 12 | } 13 | 14 | return this._runTask( 15 | straightThroughBufferTask(commands), 16 | trailingFunctionArgument(arguments) 17 | ); 18 | }, 19 | 20 | show(this: SimpleGitApi) { 21 | const commands = ['show', ...getTrailingOptions(arguments, 1)]; 22 | return this._runTask( 23 | straightThroughStringTask(commands), 24 | trailingFunctionArgument(arguments) 25 | ); 26 | }, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/stash-list.ts: -------------------------------------------------------------------------------- 1 | import { LogOptions, LogResult } from '../../../typings'; 2 | import { logFormatFromCommand } from '../args/log-format'; 3 | import { createListLogSummaryParser } from '../parsers/parse-list-log-summary'; 4 | import type { StringTask } from '../types'; 5 | import { validateLogFormatConfig } from './diff'; 6 | import { parseLogOptions } from './log'; 7 | import type { EmptyTask } from './task'; 8 | 9 | export function stashListTask( 10 | opt: LogOptions = {}, 11 | customArgs: string[] 12 | ): EmptyTask | StringTask { 13 | const options = parseLogOptions(opt); 14 | const commands = ['stash', 'list', ...options.commands, ...customArgs]; 15 | const parser = createListLogSummaryParser( 16 | options.splitter, 17 | options.fields, 18 | logFormatFromCommand(commands) 19 | ); 20 | 21 | return ( 22 | validateLogFormatConfig(commands) || { 23 | commands, 24 | format: 'utf-8', 25 | parser, 26 | } 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/status.ts: -------------------------------------------------------------------------------- 1 | import { StatusResult } from '../../../typings'; 2 | import { parseStatusSummary } from '../responses/StatusSummary'; 3 | import { StringTask } from '../types'; 4 | 5 | const ignoredOptions = ['--null', '-z']; 6 | 7 | export function statusTask(customArgs: string[]): StringTask { 8 | const commands = [ 9 | 'status', 10 | '--porcelain', 11 | '-b', 12 | '-u', 13 | '--null', 14 | ...customArgs.filter((arg) => !ignoredOptions.includes(arg)), 15 | ]; 16 | 17 | return { 18 | format: 'utf-8', 19 | commands, 20 | parser(text: string) { 21 | return parseStatusSummary(text); 22 | }, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/sub-module.ts: -------------------------------------------------------------------------------- 1 | import { StringTask } from '../types'; 2 | import { straightThroughStringTask } from './task'; 3 | 4 | export function addSubModuleTask(repo: string, path: string): StringTask { 5 | return subModuleTask(['add', repo, path]); 6 | } 7 | 8 | export function initSubModuleTask(customArgs: string[]): StringTask { 9 | return subModuleTask(['init', ...customArgs]); 10 | } 11 | 12 | export function subModuleTask(customArgs: string[]): StringTask { 13 | const commands = [...customArgs]; 14 | if (commands[0] !== 'submodule') { 15 | commands.unshift('submodule'); 16 | } 17 | 18 | return straightThroughStringTask(commands); 19 | } 20 | 21 | export function updateSubModuleTask(customArgs: string[]): StringTask { 22 | return subModuleTask(['update', ...customArgs]); 23 | } 24 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/tag.ts: -------------------------------------------------------------------------------- 1 | import { TagResult } from '../../../typings'; 2 | import { parseTagList } from '../responses/TagList'; 3 | import { StringTask } from '../types'; 4 | 5 | /** 6 | * Task used by `git.tags` 7 | */ 8 | export function tagListTask(customArgs: string[] = []): StringTask { 9 | const hasCustomSort = customArgs.some((option) => /^--sort=/.test(option)); 10 | 11 | return { 12 | format: 'utf-8', 13 | commands: ['tag', '-l', ...customArgs], 14 | parser(text: string) { 15 | return parseTagList(text, hasCustomSort); 16 | }, 17 | }; 18 | } 19 | 20 | /** 21 | * Task used by `git.addTag` 22 | */ 23 | export function addTagTask(name: string): StringTask<{ name: string }> { 24 | return { 25 | format: 'utf-8', 26 | commands: ['tag', name], 27 | parser() { 28 | return { name }; 29 | }, 30 | }; 31 | } 32 | 33 | /** 34 | * Task used by `git.addTag` 35 | */ 36 | export function addAnnotatedTagTask( 37 | name: string, 38 | tagMessage: string 39 | ): StringTask<{ name: string }> { 40 | return { 41 | format: 'utf-8', 42 | commands: ['tag', '-a', '-m', tagMessage, name], 43 | parser() { 44 | return { name }; 45 | }, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /simple-git/src/lib/tasks/task.ts: -------------------------------------------------------------------------------- 1 | import { TaskConfigurationError } from '../errors/task-configuration-error'; 2 | import type { BufferTask, EmptyTaskParser, SimpleGitTask, StringTask } from '../types'; 3 | 4 | export const EMPTY_COMMANDS: [] = []; 5 | 6 | export type EmptyTask = { 7 | commands: typeof EMPTY_COMMANDS; 8 | format: 'empty'; 9 | parser: EmptyTaskParser; 10 | onError?: undefined; 11 | }; 12 | 13 | export function adhocExecTask(parser: EmptyTaskParser): EmptyTask { 14 | return { 15 | commands: EMPTY_COMMANDS, 16 | format: 'empty', 17 | parser, 18 | }; 19 | } 20 | 21 | export function configurationErrorTask(error: Error | string): EmptyTask { 22 | return { 23 | commands: EMPTY_COMMANDS, 24 | format: 'empty', 25 | parser() { 26 | throw typeof error === 'string' ? new TaskConfigurationError(error) : error; 27 | }, 28 | }; 29 | } 30 | 31 | export function straightThroughStringTask(commands: string[], trimmed = false): StringTask { 32 | return { 33 | commands, 34 | format: 'utf-8', 35 | parser(text) { 36 | return trimmed ? String(text).trim() : text; 37 | }, 38 | }; 39 | } 40 | 41 | export function straightThroughBufferTask(commands: string[]): BufferTask { 42 | return { 43 | commands, 44 | format: 'buffer', 45 | parser(buffer) { 46 | return buffer; 47 | }, 48 | }; 49 | } 50 | 51 | export function isBufferTask(task: SimpleGitTask): task is BufferTask { 52 | return task.format === 'buffer'; 53 | } 54 | 55 | export function isEmptyTask(task: SimpleGitTask): task is EmptyTask { 56 | return task.format === 'empty' || !task.commands.length; 57 | } 58 | -------------------------------------------------------------------------------- /simple-git/src/lib/types/handlers.ts: -------------------------------------------------------------------------------- 1 | import { GitError } from '../errors/git-error'; 2 | 3 | /** 4 | * The node-style callback to a task accepts either two arguments with the first as a null 5 | * and the second as the data, or just one argument which is an error. 6 | */ 7 | export type SimpleGitTaskCallback = ( 8 | err: E | null, 9 | data: T 10 | ) => void; 11 | 12 | /** 13 | * The event data emitted to the progress handler whenever progress detail is received. 14 | */ 15 | export interface SimpleGitProgressEvent { 16 | /** The underlying method called - push, pull etc */ 17 | method: string; 18 | /** The type of progress being reported, note that any one task may emit many stages - for example `git clone` emits both `receiving` and `resolving` */ 19 | stage: 'compressing' | 'counting' | 'receiving' | 'resolving' | 'unknown' | 'writing' | string; 20 | /** The percent progressed as a number 0 - 100 */ 21 | progress: number; 22 | /** The number of items processed so far */ 23 | processed: number; 24 | /** The total number of items to be processed */ 25 | total: number; 26 | } 27 | -------------------------------------------------------------------------------- /simple-git/src/lib/types/tasks.ts: -------------------------------------------------------------------------------- 1 | import { GitExecutorResult, SimpleGitExecutor } from './index'; 2 | import { EmptyTask } from '../tasks/task'; 3 | 4 | export type TaskResponseFormat = Buffer | string; 5 | 6 | export interface TaskParser { 7 | (stdOut: INPUT, stdErr: INPUT): RESPONSE; 8 | } 9 | 10 | export interface EmptyTaskParser { 11 | (executor: SimpleGitExecutor): void; 12 | } 13 | 14 | export interface SimpleGitTaskConfiguration { 15 | commands: string[]; 16 | format: FORMAT; 17 | parser: TaskParser; 18 | 19 | onError?: ( 20 | result: GitExecutorResult, 21 | error: Error, 22 | done: (result: Buffer | Buffer[]) => void, 23 | fail: (error: string | Error) => void 24 | ) => void; 25 | } 26 | 27 | export type StringTask = SimpleGitTaskConfiguration; 28 | 29 | export type BufferTask = SimpleGitTaskConfiguration; 30 | 31 | export type RunnableTask = StringTask | BufferTask; 32 | 33 | export type SimpleGitTask = RunnableTask | EmptyTask; 34 | -------------------------------------------------------------------------------- /simple-git/src/lib/utils/exit-codes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Known process exit codes used by the task parsers to determine whether an error 3 | * was one they can automatically handle 4 | */ 5 | export enum ExitCodes { 6 | SUCCESS, 7 | ERROR, 8 | NOT_FOUND = -2, 9 | UNCLEAN = 128, 10 | } 11 | -------------------------------------------------------------------------------- /simple-git/src/lib/utils/git-output-streams.ts: -------------------------------------------------------------------------------- 1 | import { TaskResponseFormat } from '../types'; 2 | 3 | export class GitOutputStreams { 4 | constructor( 5 | public readonly stdOut: T, 6 | public readonly stdErr: T 7 | ) {} 8 | 9 | asStrings(): GitOutputStreams { 10 | return new GitOutputStreams(this.stdOut.toString('utf8'), this.stdErr.toString('utf8')); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /simple-git/src/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './argument-filters'; 2 | export * from './exit-codes'; 3 | export * from './git-output-streams'; 4 | export * from './line-parser'; 5 | export * from './simple-git-options'; 6 | export * from './task-options'; 7 | export * from './task-parser'; 8 | export * from './util'; 9 | -------------------------------------------------------------------------------- /simple-git/src/lib/utils/line-parser.ts: -------------------------------------------------------------------------------- 1 | export class LineParser { 2 | protected matches: string[] = []; 3 | 4 | private _regExp: RegExp[]; 5 | 6 | constructor( 7 | regExp: RegExp | RegExp[], 8 | useMatches?: (target: T, match: string[]) => boolean | void 9 | ) { 10 | this._regExp = Array.isArray(regExp) ? regExp : [regExp]; 11 | if (useMatches) { 12 | this.useMatches = useMatches; 13 | } 14 | } 15 | 16 | parse = (line: (offset: number) => string | undefined, target: T): boolean => { 17 | this.resetMatches(); 18 | 19 | if (!this._regExp.every((reg, index) => this.addMatch(reg, index, line(index)))) { 20 | return false; 21 | } 22 | 23 | return this.useMatches(target, this.prepareMatches()) !== false; 24 | }; 25 | 26 | // @ts-ignore 27 | protected useMatches(target: T, match: string[]): boolean | void { 28 | throw new Error(`LineParser:useMatches not implemented`); 29 | } 30 | 31 | protected resetMatches() { 32 | this.matches.length = 0; 33 | } 34 | 35 | protected prepareMatches() { 36 | return this.matches; 37 | } 38 | 39 | protected addMatch(reg: RegExp, index: number, line?: string) { 40 | const matched = line && reg.exec(line); 41 | if (matched) { 42 | this.pushMatch(index, matched); 43 | } 44 | 45 | return !!matched; 46 | } 47 | 48 | protected pushMatch(_index: number, matched: string[]) { 49 | this.matches.push(...matched.slice(1)); 50 | } 51 | } 52 | 53 | export class RemoteLineParser extends LineParser { 54 | protected addMatch(reg: RegExp, index: number, line?: string): boolean { 55 | return /^remote:\s/.test(String(line)) && super.addMatch(reg, index, line); 56 | } 57 | 58 | protected pushMatch(index: number, matched: string[]) { 59 | if (index > 0 || matched.length > 1) { 60 | super.pushMatch(index, matched); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /simple-git/src/lib/utils/simple-git-options.ts: -------------------------------------------------------------------------------- 1 | import { SimpleGitOptions } from '../types'; 2 | 3 | const defaultOptions: Omit = { 4 | binary: 'git', 5 | maxConcurrentProcesses: 5, 6 | config: [], 7 | trimmed: false, 8 | }; 9 | 10 | export function createInstanceConfig( 11 | ...options: Array | undefined> 12 | ): SimpleGitOptions { 13 | const baseDir = process.cwd(); 14 | const config: SimpleGitOptions = Object.assign( 15 | { baseDir, ...defaultOptions }, 16 | ...options.filter((o) => typeof o === 'object' && o) 17 | ); 18 | 19 | config.baseDir = config.baseDir || baseDir; 20 | config.trimmed = config.trimmed === true; 21 | 22 | return config; 23 | } 24 | -------------------------------------------------------------------------------- /simple-git/src/lib/utils/task-parser.ts: -------------------------------------------------------------------------------- 1 | import type { MaybeArray, TaskParser, TaskResponseFormat } from '../types'; 2 | import { GitOutputStreams } from './git-output-streams'; 3 | import { LineParser } from './line-parser'; 4 | import { asArray, toLinesWithContent } from './util'; 5 | 6 | export function callTaskParser( 7 | parser: TaskParser, 8 | streams: GitOutputStreams 9 | ) { 10 | return parser(streams.stdOut, streams.stdErr); 11 | } 12 | 13 | export function parseStringResponse( 14 | result: T, 15 | parsers: LineParser[], 16 | texts: MaybeArray, 17 | trim = true 18 | ): T { 19 | asArray(texts).forEach((text) => { 20 | for (let lines = toLinesWithContent(text, trim), i = 0, max = lines.length; i < max; i++) { 21 | const line = (offset = 0) => { 22 | if (i + offset >= max) { 23 | return; 24 | } 25 | return lines[i + offset]; 26 | }; 27 | 28 | parsers.some(({ parse }) => parse(line, result)); 29 | } 30 | }); 31 | 32 | return result; 33 | } 34 | -------------------------------------------------------------------------------- /simple-git/test/integration/add.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createTestContext, 3 | like, 4 | newSimpleGit, 5 | setUpInit, 6 | SimpleGitTestContext, 7 | } from '@simple-git/test-utils'; 8 | 9 | describe('add', () => { 10 | let context: SimpleGitTestContext; 11 | 12 | beforeEach(async () => (context = await createTestContext())); 13 | beforeEach(async () => { 14 | await setUpInit(context); 15 | await context.files('aaa.txt', 'bbb.txt', 'ccc.other'); 16 | }); 17 | 18 | it('adds a single file', async () => { 19 | await context.git.add('aaa.txt'); 20 | expect(await newSimpleGit(context.root).status()).toEqual( 21 | like({ 22 | created: ['aaa.txt'], 23 | not_added: ['bbb.txt', 'ccc.other'], 24 | }) 25 | ); 26 | }); 27 | 28 | it('adds multiple files explicitly', async () => { 29 | await context.git.add(['aaa.txt', 'ccc.other']); 30 | 31 | expect(await newSimpleGit(context.root).status()).toEqual( 32 | like({ 33 | created: ['aaa.txt', 'ccc.other'], 34 | not_added: ['bbb.txt'], 35 | }) 36 | ); 37 | }); 38 | 39 | it('adds multiple files by wildcard', async () => { 40 | await context.git.add('*.txt'); 41 | 42 | expect(await newSimpleGit(context.root).status()).toEqual( 43 | like({ 44 | created: ['aaa.txt', 'bbb.txt'], 45 | not_added: ['ccc.other'], 46 | }) 47 | ); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /simple-git/test/integration/bad-initial-path.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | assertGitError, 3 | createTestContext, 4 | like, 5 | newSimpleGit, 6 | SimpleGitTestContext, 7 | } from '@simple-git/test-utils'; 8 | 9 | import { GitConstructError } from '../..'; 10 | 11 | describe('bad initial path', () => { 12 | let context: SimpleGitTestContext; 13 | 14 | beforeEach(async () => (context = await createTestContext())); 15 | 16 | it('simple-git', async () => { 17 | const baseDir = context.path('foo'); 18 | 19 | let errorInstance: Error | unknown; 20 | try { 21 | newSimpleGit(baseDir); 22 | } catch (e) { 23 | errorInstance = e; 24 | assertGitError(errorInstance, `does not exist`, GitConstructError); 25 | expect(errorInstance).toHaveProperty( 26 | 'config', 27 | like({ 28 | baseDir, 29 | }) 30 | ); 31 | } finally { 32 | expect(errorInstance).not.toBeUndefined(); 33 | } 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /simple-git/test/integration/checkout.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createTestContext, 3 | newSimpleGit, 4 | setUpInit, 5 | SimpleGitTestContext, 6 | } from '@simple-git/test-utils'; 7 | import { SimpleGit } from '../../typings'; 8 | import { promiseError } from '@kwsites/promise-result'; 9 | 10 | describe('checkout', () => { 11 | let context: SimpleGitTestContext; 12 | let git: SimpleGit; 13 | 14 | beforeEach(async () => (context = await createTestContext())); 15 | beforeEach(async () => { 16 | await setUpInit(context); 17 | await context.files('aaa.txt', 'bbb.txt', 'ccc.other'); 18 | git = newSimpleGit(context.root); 19 | }); 20 | 21 | it('checkoutLocalBranch', async () => { 22 | const { current: initialBranch } = await git.status(); 23 | 24 | expect(await promiseError(git.checkoutLocalBranch('my-new-branch'))).toBeUndefined(); 25 | 26 | const { current: finalBranch } = await git.status(); 27 | expect(finalBranch).toBe('my-new-branch'); 28 | expect(finalBranch).not.toBe(initialBranch); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /simple-git/test/integration/commit.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createTestContext, 3 | newSimpleGit, 4 | setUpInit, 5 | SimpleGitTestContext, 6 | } from '@simple-git/test-utils'; 7 | 8 | describe('commit', () => { 9 | let context: SimpleGitTestContext; 10 | 11 | beforeEach(async () => (context = await createTestContext())); 12 | beforeEach(async () => { 13 | await setUpInit(context); 14 | await context.files('hello', 'world'); 15 | await context.git.add('.'); 16 | }); 17 | 18 | it('details full commit hashes', async () => { 19 | const result = await newSimpleGit(context.root).commit('commit message'); 20 | 21 | expect(result.commit.length).toBeGreaterThan(10); 22 | expect(result.commit).toEqual(await context.git.revparse('HEAD')); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /simple-git/test/integration/concurrent-commands.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createTestContext, 3 | newSimpleGit, 4 | setUpFilesAdded, 5 | setUpInit, 6 | SimpleGitTestContext, 7 | } from '@simple-git/test-utils'; 8 | 9 | describe('concurrent commands', () => { 10 | let contexts: { first: SimpleGitTestContext; second: SimpleGitTestContext }; 11 | 12 | async function configure(context: SimpleGitTestContext, name: string) { 13 | await setUpInit(context); 14 | await setUpFilesAdded(context, [name]); 15 | await context.git.raw('checkout', '-b', name); 16 | return context; 17 | } 18 | 19 | beforeEach(async () => { 20 | contexts = { 21 | first: await configure(await createTestContext(), 'first'), 22 | second: await configure(await createTestContext(), 'second'), 23 | }; 24 | }); 25 | 26 | it('will queue tasks to ensure all tasks run eventually', async () => { 27 | const tests: Array = [ 28 | 'first', 29 | 'second', 30 | 'first', 31 | 'second', 32 | 'first', 33 | 'second', 34 | 'second', 35 | 'first', 36 | 'second', 37 | 'first', 38 | 'second', 39 | 'first', 40 | ]; 41 | const expected = [...tests]; 42 | const actual = await Promise.all(tests.map(currentBranchForDirectory)); 43 | 44 | expect(actual).toEqual(expected); 45 | }); 46 | 47 | function currentBranchForDirectory(dir: keyof typeof contexts) { 48 | const context = contexts[dir]; 49 | return newSimpleGit(context.root) 50 | .branchLocal() 51 | .then((result) => result.current); 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /simple-git/test/integration/exec.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTestContext, newSimpleGit, SimpleGitTestContext } from '@simple-git/test-utils'; 2 | 3 | describe('exec', () => { 4 | let context: SimpleGitTestContext; 5 | 6 | beforeEach(async () => { 7 | context = await createTestContext(); 8 | }); 9 | 10 | it('will exec a function between other chained methods', async () => { 11 | const calls: string[] = []; 12 | 13 | await newSimpleGit(context.root) 14 | .exec(() => calls.push('a')) 15 | .raw('init', () => calls.push('b')) 16 | .exec(() => calls.push('c')) 17 | .raw('init', () => calls.push('d')); 18 | 19 | expect(calls).toEqual(['a', 'b', 'c', 'd']); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /simple-git/test/integration/log-name-status.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createTestContext, 3 | like, 4 | newSimpleGit, 5 | setUpFilesAdded, 6 | setUpInit, 7 | SimpleGitTestContext, 8 | } from '@simple-git/test-utils'; 9 | 10 | import { DiffNameStatus, DiffResultTextFile } from '../..'; 11 | 12 | describe('log-name-status', function () { 13 | let context: SimpleGitTestContext; 14 | const steps = ['mv a b', 'commit -m two']; 15 | 16 | beforeEach(async () => { 17 | context = await createTestContext(); 18 | await setUpInit(context); 19 | await setUpFilesAdded(context, ['a'], '.', 'one'); 20 | for (const step of steps) { 21 | await context.git.raw(step.split(' ')); 22 | } 23 | }); 24 | 25 | it('detects files moved with --name-status', async () => { 26 | const actual = await newSimpleGit(context.root).log(['--name-status']); 27 | 28 | expect(actual.all).toEqual([ 29 | mockListLogLine('two', { b: [DiffNameStatus.RENAMED, 100, 'a'] }), 30 | mockListLogLine('one', { a: [DiffNameStatus.ADDED] }), 31 | ]); 32 | }); 33 | }); 34 | 35 | function mockListLogLine( 36 | message: string, 37 | changes: Record 38 | ) { 39 | const files: DiffResultTextFile[] = Object.entries(changes).map( 40 | ([file, [status, similarity = 0, from]]) => { 41 | return { 42 | binary: false, 43 | changes: 0, 44 | deletions: 0, 45 | file, 46 | insertions: 0, 47 | similarity, 48 | status, 49 | from, 50 | }; 51 | } 52 | ); 53 | return like({ 54 | message, 55 | diff: like({ changed: files.length, deletions: 0, insertions: 0, files }), 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /simple-git/test/integration/log-numstat.spec.ts: -------------------------------------------------------------------------------- 1 | import { promiseError } from '@kwsites/promise-result'; 2 | import { newSimpleGit } from '@simple-git/test-utils'; 3 | 4 | describe('log-numstat', function () { 5 | it('custom format and date range should not fail when also setting numstat', async () => { 6 | const ac = new AbortController(); 7 | const log = newSimpleGit(__dirname, { 8 | abort: ac.signal, 9 | }).log({ 10 | 'format': { 11 | H: '%H', 12 | h: '%h', 13 | P: '%P', 14 | p: '%p', 15 | aI: '%aI', 16 | s: '%s', 17 | D: '%D', 18 | b: '%b', 19 | an: '%an', 20 | ae: '%ae', 21 | }, 22 | '--all': null, 23 | '--since': '2024-02-04', 24 | '--numstat': null, 25 | }); 26 | 27 | setTimeout(() => ac.abort(), 500); 28 | 29 | expect(await promiseError(log)).toBeUndefined(); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /simple-git/test/integration/output-handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTestContext, setUpInit, SimpleGitTestContext, wait } from '@simple-git/test-utils'; 2 | 3 | describe('outputHandler', function () { 4 | let context: SimpleGitTestContext; 5 | 6 | beforeEach(async () => (context = await createTestContext())); 7 | beforeEach(async () => { 8 | await setUpInit(context); 9 | await context.files('aaa.txt', 'bbb.txt', 'ccc.other'); 10 | }); 11 | 12 | it('using the outputHandler to count currently running processes', async () => { 13 | let processes = new Set(); 14 | const currentlyRunning = () => processes.size; 15 | const git = context.git.outputHandler((_x, stdout, stderr) => { 16 | const start = new Date(); 17 | const onClose = () => processes.delete(start); 18 | 19 | stdout.on('close', onClose); 20 | stderr.on('close', onClose); 21 | 22 | processes.add(start); 23 | }); 24 | 25 | expect(currentlyRunning()).toBe(0); 26 | const queue = [git.init(), git.add('*.txt')]; 27 | 28 | await wait(0); 29 | expect(currentlyRunning()).toBe(2); 30 | 31 | await Promise.all(queue); 32 | expect(currentlyRunning()).toBe(0); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /simple-git/test/integration/plugin.abort.spec.ts: -------------------------------------------------------------------------------- 1 | import { promiseError } from '@kwsites/promise-result'; 2 | import { 3 | assertGitError, 4 | createAbortController, 5 | createTestContext, 6 | newSimpleGit, 7 | SimpleGitTestContext, 8 | wait, 9 | } from '@simple-git/test-utils'; 10 | 11 | import { GitPluginError } from '../..'; 12 | 13 | describe('timeout', () => { 14 | let context: SimpleGitTestContext; 15 | 16 | beforeEach(async () => (context = await createTestContext())); 17 | 18 | it('kills processes on abort signal', async () => { 19 | const { controller, abort } = createAbortController(); 20 | 21 | const threw = promiseError(newSimpleGit(context.root, { abort }).init()); 22 | 23 | await wait(0); 24 | controller.abort(); 25 | 26 | assertGitError(await threw, 'Abort signal received', GitPluginError); 27 | }); 28 | 29 | it('share AbortController across many instances', async () => { 30 | const { controller, abort } = createAbortController(); 31 | const upstream = await newSimpleGit(__dirname).revparse('--git-dir'); 32 | 33 | const repos = await Promise.all('abcdef'.split('').map((p) => context.dir(p))); 34 | 35 | repos.map((baseDir) => { 36 | const git = newSimpleGit({ baseDir, abort }); 37 | if (baseDir.endsWith('a')) { 38 | return promiseError(git.init()); 39 | } 40 | return promiseError(git.clone(upstream, baseDir)); 41 | }); 42 | 43 | await wait(0); 44 | controller.abort(); 45 | 46 | const results = await Promise.all( 47 | repos.map((baseDir) => newSimpleGit(baseDir).checkIsRepo()) 48 | ); 49 | 50 | expect(results).toContain(false); 51 | expect(results).toContain(true); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /simple-git/test/integration/plugin.completion.spec.ts: -------------------------------------------------------------------------------- 1 | import { promiseError } from '@kwsites/promise-result'; 2 | import { createTestContext, newSimpleGit, SimpleGitTestContext } from '@simple-git/test-utils'; 3 | 4 | describe('progress-monitor', () => { 5 | let context: SimpleGitTestContext; 6 | 7 | beforeEach(async () => (context = await createTestContext())); 8 | 9 | it('detects successful completion', async () => { 10 | const git = newSimpleGit(context.root); 11 | expect(await promiseError(git.init())).toBeUndefined(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /simple-git/test/integration/plugin.progress.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTestContext, newSimpleGit, SimpleGitTestContext } from '@simple-git/test-utils'; 2 | import { SimpleGitOptions } from '../../src/lib/types'; 3 | 4 | describe('progress-monitor', () => { 5 | const upstream = 'https://github.com/steveukx/git-js.git'; 6 | 7 | let context: SimpleGitTestContext; 8 | 9 | beforeEach(async () => (context = await createTestContext())); 10 | 11 | it('emits progress events', async () => { 12 | const progress = jest.fn(); 13 | const opt: Partial = { 14 | baseDir: context.root, 15 | progress, 16 | }; 17 | 18 | await newSimpleGit(opt).clone(upstream); 19 | 20 | const receivingUpdates = progressEventsAtStage(progress, 'receiving'); 21 | 22 | expect(receivingUpdates.length).toBeGreaterThan(0); 23 | 24 | receivingUpdates.reduce((previous, update) => { 25 | expect(update).toEqual({ 26 | method: 'clone', 27 | stage: 'receiving', 28 | progress: expect.any(Number), 29 | processed: expect.any(Number), 30 | total: expect.any(Number), 31 | }); 32 | 33 | expect(update.progress).toBeGreaterThanOrEqual(previous); 34 | return update.progress; 35 | }, 0); 36 | }); 37 | }); 38 | 39 | function progressEventsAtStage(mock: jest.Mock, stage: string) { 40 | return mock.mock.calls.filter((c) => c[0].stage === stage).map((c) => c[0]); 41 | } 42 | -------------------------------------------------------------------------------- /simple-git/test/integration/plugin.timeout.spec.ts: -------------------------------------------------------------------------------- 1 | import { promiseError } from '@kwsites/promise-result'; 2 | import { 3 | assertGitError, 4 | createTestContext, 5 | newSimpleGit, 6 | SimpleGitTestContext, 7 | } from '@simple-git/test-utils'; 8 | 9 | import { GitPluginError } from '../..'; 10 | 11 | describe('timeout', () => { 12 | let context: SimpleGitTestContext; 13 | 14 | beforeEach(async () => (context = await createTestContext())); 15 | 16 | it('kills processes after a timeout', async () => { 17 | const upstream = await newSimpleGit(__dirname).revparse('--git-dir'); 18 | 19 | const git = newSimpleGit({ 20 | baseDir: context.root, 21 | timeout: { 22 | block: 1, 23 | }, 24 | }); 25 | 26 | const threw = await promiseError(git.raw('clone', upstream, '.')); 27 | assertGitError(threw, 'block timeout reached', GitPluginError); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /simple-git/test/integration/promise-from-root.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTestContext, newSimpleGit, SimpleGitTestContext } from '@simple-git/test-utils'; 2 | 3 | describe('promises-from-root', () => { 4 | let context: SimpleGitTestContext; 5 | 6 | beforeEach(async () => (context = await createTestContext())); 7 | 8 | it('chains through the default export', async () => { 9 | const onInit = jest.fn(); 10 | const onShowTopLevel = jest.fn(); 11 | const onError = jest.fn(); 12 | 13 | const git = newSimpleGit(context.root); 14 | const queue = git 15 | .init() 16 | .then(onInit) 17 | .then(() => git.revparse(['--show-toplevel'])) 18 | .then(onShowTopLevel) 19 | .catch((err) => onError(err)); 20 | 21 | await queue; 22 | expect(onInit).toHaveBeenCalled(); 23 | expect(onShowTopLevel).toHaveBeenCalledWith(context.rootResolvedPath); 24 | expect(onError).not.toHaveBeenCalled(); 25 | }); 26 | 27 | it('calls provided callbacks when chained through root export', async () => { 28 | const onInit = jest.fn(); 29 | const onShowTopLevel = jest.fn(); 30 | 31 | const queue = newSimpleGit(context.root) 32 | .init(onInit) 33 | .revparse(['--show-toplevel'], onShowTopLevel); 34 | 35 | expect(await queue).toBe(context.rootResolvedPath); 36 | expect(onInit).toHaveBeenCalled(); 37 | expect(onShowTopLevel).toHaveBeenCalledWith(null, context.rootResolvedPath); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /simple-git/test/integration/remote.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createTestContext, 3 | newSimpleGit, 4 | setUpInit, 5 | SimpleGitTestContext, 6 | } from '@simple-git/test-utils'; 7 | 8 | describe('remote', () => { 9 | let context: SimpleGitTestContext; 10 | let REMOTE_URL_ROOT = 'https://github.com/steveukx'; 11 | let REMOTE_URL = `${REMOTE_URL_ROOT}/git-js.git`; 12 | 13 | beforeEach(async () => (context = await createTestContext())); 14 | beforeEach(async () => { 15 | await setUpInit(context); 16 | }); 17 | 18 | it('adds and removes named remotes', async () => { 19 | const git = newSimpleGit(context.root).addRemote('remote-name', REMOTE_URL); 20 | 21 | expect(await git.getRemotes(true)).toEqual([ 22 | { name: 'remote-name', refs: { fetch: REMOTE_URL, push: REMOTE_URL } }, 23 | ]); 24 | 25 | await git.removeRemote('remote-name'); 26 | expect(await git.getRemotes(true)).toEqual([]); 27 | }); 28 | 29 | it('allows setting the remote url', async () => { 30 | const git = newSimpleGit(context.root); 31 | 32 | let repoName = 'origin'; 33 | let initialRemoteRepo = `${REMOTE_URL_ROOT}/initial.git`; 34 | let updatedRemoteRepo = `${REMOTE_URL_ROOT}/updated.git`; 35 | 36 | await git.addRemote(repoName, initialRemoteRepo); 37 | expect(await git.getRemotes(true)).toEqual([ 38 | { name: repoName, refs: { fetch: initialRemoteRepo, push: initialRemoteRepo } }, 39 | ]); 40 | 41 | await git.remote(['set-url', repoName, updatedRemoteRepo]); 42 | expect(await git.getRemotes(true)).toEqual([ 43 | { name: repoName, refs: { fetch: updatedRemoteRepo, push: updatedRemoteRepo } }, 44 | ]); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /simple-git/test/integration/reset.spec.ts: -------------------------------------------------------------------------------- 1 | import { promiseError } from '@kwsites/promise-result'; 2 | import { 3 | assertGitError, 4 | createTestContext, 5 | newSimpleGit, 6 | setUpFilesAdded, 7 | setUpInit, 8 | SimpleGitTestContext, 9 | } from '@simple-git/test-utils'; 10 | 11 | import { ResetMode } from '../../src/lib/tasks/reset'; 12 | 13 | describe('reset', () => { 14 | let context: SimpleGitTestContext; 15 | 16 | beforeEach(async () => (context = await createTestContext())); 17 | beforeEach(async () => { 18 | await setUpInit(context); 19 | await setUpFilesAdded(context, ['alpha', 'beta', 'gamma'], 'alpha'); 20 | }); 21 | 22 | it('resets adding a single file', async () => { 23 | const git = newSimpleGit(context.root); 24 | expect((await git.status()).not_added).toEqual(['beta', 'gamma']); 25 | 26 | await git.add('.'); 27 | expect((await git.status()).not_added).toEqual([]); 28 | 29 | await git.reset(['--', 'beta']); 30 | expect((await git.status()).not_added).toEqual(['beta']); 31 | }); 32 | 33 | it('throws when hard resetting a path', async () => { 34 | const git = newSimpleGit(context.root); 35 | await git.add('.'); 36 | const error = await promiseError(git.reset(ResetMode.HARD, ['--', 'beta'])); 37 | 38 | assertGitError(error, /hard reset/); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /simple-git/test/integration/rev-parse.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createTestContext, 3 | newSimpleGit, 4 | setUpFilesAdded, 5 | setUpInit, 6 | SimpleGitTestContext, 7 | } from '@simple-git/test-utils'; 8 | 9 | describe('rev-parse', () => { 10 | let context: SimpleGitTestContext; 11 | 12 | beforeEach(async () => (context = await createTestContext())); 13 | beforeEach(async () => { 14 | await setUpInit(context); 15 | await setUpFilesAdded(context, ['file.txt']); 16 | }); 17 | 18 | it('gets the commit hash for HEAD, responds with a trimmed string', async () => { 19 | const actual = await newSimpleGit(context.root).revparse(['HEAD']); 20 | expect(actual).toBe(String(actual).trim()); 21 | }); 22 | 23 | it('gets the repo root', async () => { 24 | const actual = await newSimpleGit(context.root).revparse(['--show-toplevel']); 25 | expect(actual).toBe(context.rootResolvedPath); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /simple-git/test/integration/tag.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createTestContext, 3 | like, 4 | newSimpleGit, 5 | setUpFilesAdded, 6 | setUpInit, 7 | SimpleGitTestContext, 8 | } from '@simple-git/test-utils'; 9 | 10 | describe('tag', () => { 11 | let context: SimpleGitTestContext; 12 | 13 | beforeEach(async () => (context = await createTestContext())); 14 | beforeEach(async () => { 15 | await setUpInit(context); 16 | await setUpFilesAdded(context, ['foo', 'bar']); 17 | }); 18 | 19 | it('creates and gets the current named tag', async () => { 20 | const git = newSimpleGit(context.root); 21 | expect(await git.addTag('newTag')).toEqual({ name: 'newTag' }); 22 | expect(String(await git.tag()).trim()).toBe('newTag'); 23 | }); 24 | 25 | it('lists all tags', async () => { 26 | await context.git.raw('tag', 'v1.0'); 27 | await context.git.raw('tag', 'v1.5'); 28 | 29 | expect(await newSimpleGit(context.root).tags()).toEqual( 30 | like({ 31 | all: ['v1.0', 'v1.5'], 32 | latest: 'v1.5', 33 | }) 34 | ); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /simple-git/test/integration/version.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTestContext, newSimpleGit, SimpleGitTestContext } from '@simple-git/test-utils'; 2 | 3 | describe('version', () => { 4 | let context: SimpleGitTestContext; 5 | 6 | beforeEach(async () => (context = await createTestContext())); 7 | 8 | it('gets the current version', async () => { 9 | const git = newSimpleGit(context.root); 10 | expect(await git.version()).toEqual({ 11 | major: 2, 12 | minor: expect.any(Number), 13 | patch: expect.any(Number), 14 | agent: expect.any(String), 15 | installed: true, 16 | }); 17 | }); 18 | 19 | it('gets the current version when the binary is not installed', async () => { 20 | const git = newSimpleGit(context.root).customBinary('bad'); 21 | expect(await git.version()).toEqual({ 22 | major: 0, 23 | minor: 0, 24 | patch: 0, 25 | agent: '', 26 | installed: false, 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/create-fixture.ts: -------------------------------------------------------------------------------- 1 | type ResponseFixture = { 2 | stdOut: string; 3 | stdErr: string; 4 | parserArgs: [string, string]; 5 | }; 6 | 7 | export function createFixture(stdOut: string, stdErr: string): ResponseFixture { 8 | return { 9 | stdOut, 10 | stdErr, 11 | parserArgs: [stdOut, stdErr], 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/debug.ts: -------------------------------------------------------------------------------- 1 | jest.mock('debug', () => { 2 | function logger(name: string, logs: any) { 3 | logs[name] = logs[name] || []; 4 | 5 | return Object.assign( 6 | (_: string, ...messages: Array) => { 7 | logs[name].push( 8 | messages.filter((m) => typeof m === 'string' || Buffer.isBuffer(m)).join(' ') 9 | ); 10 | }, 11 | { 12 | extend(suffix: string) { 13 | return debug(`${name}:${suffix}`); 14 | }, 15 | get logs() { 16 | return logs; 17 | }, 18 | } 19 | ); 20 | } 21 | 22 | const debug: any = Object.assign( 23 | jest.fn((name) => { 24 | if (debug.mock.results[0].type === 'return') { 25 | return logger(name, debug.mock.results[0].value.logs); 26 | } 27 | 28 | return logger(name, {}); 29 | }), 30 | { 31 | formatters: { 32 | H: 'hello-world', 33 | }, 34 | } 35 | ); 36 | 37 | return debug; 38 | }); 39 | 40 | function logs(): Record { 41 | return (require('debug') as jest.Mock).mock.results[0].value.logs; 42 | } 43 | 44 | export function $logNames(...matching: RegExp[]) { 45 | return Object.keys(logs()).filter(matches); 46 | 47 | function matches(namespace: string) { 48 | return !matching.length || matching.some((regex) => regex.test(namespace)); 49 | } 50 | } 51 | 52 | export function $logMessagesFor(name: string) { 53 | const log = logs()[name]; 54 | 55 | expect(Array.isArray(log)).toBe(true); 56 | 57 | return log.join('\n'); 58 | } 59 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/expectations.ts: -------------------------------------------------------------------------------- 1 | import { mockChildProcessModule } from '../__mocks__/mock-child-process'; 2 | 3 | export function assertTheBuffer(actual: Buffer | unknown, content?: string) { 4 | expect(Buffer.isBuffer(actual)).toBe(true); 5 | if (typeof content === 'string') { 6 | expect((actual as Buffer).toString('utf8')).toBe(content); 7 | } 8 | } 9 | 10 | export function assertExecutedTasksCount(count: number) { 11 | expect(mockChildProcessModule.$allCommands()).toHaveLength(count); 12 | } 13 | 14 | export function assertNoExecutedTasks() { 15 | return assertExecutedTasksCount(0); 16 | } 17 | 18 | export function assertAllExecutedCommands(...commands: string[][]) { 19 | expect(mockChildProcessModule.$allCommands()).toEqual(commands); 20 | } 21 | 22 | export function assertExecutedCommands(...commands: string[]) { 23 | expect(mockChildProcessModule.$mostRecent().$args).toEqual(commands); 24 | } 25 | 26 | export function assertExecutedCommandsContains(command: string) { 27 | expect(mockChildProcessModule.$mostRecent().$args.indexOf(command)).not.toBe(-1); 28 | } 29 | 30 | export function assertExecutedCommandsContainsOnce(command: string) { 31 | expect(mockChildProcessModule.$mostRecent().$args.filter((c) => c === command)).toHaveLength(1); 32 | } 33 | 34 | export function assertChildProcessEnvironmentVariables(env: any) { 35 | expect(mockChildProcessModule.$mostRecent()).toHaveProperty('$env', env); 36 | } 37 | 38 | export function assertChildProcessSpawnOptions(options: any) { 39 | expect(mockChildProcessModule.$mostRecent().$options).toMatchObject(options); 40 | } 41 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/file-exists.ts: -------------------------------------------------------------------------------- 1 | import { exists } from '@kwsites/file-exists'; 2 | 3 | jest.mock('@kwsites/file-exists', () => ({ 4 | exists: jest.fn().mockReturnValue(true), 5 | })); 6 | 7 | export function isInvalidDirectory() { 8 | (exists as jest.Mock).mockReturnValue(false); 9 | } 10 | 11 | export function isValidDirectory() { 12 | (exists as jest.Mock).mockReturnValue(true); 13 | } 14 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/index.ts: -------------------------------------------------------------------------------- 1 | export * from './debug'; 2 | export * from './file-exists'; 3 | 4 | export * from './child-processes'; 5 | export * from './expectations'; 6 | 7 | export * from './responses/branch'; 8 | export * from './responses/commit'; 9 | export * from './responses/diff'; 10 | export * from './responses/merge'; 11 | export * from './responses/remote-messages'; 12 | export * from './responses/status'; 13 | 14 | export * from '@simple-git/test-utils'; 15 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/push/constants.ts: -------------------------------------------------------------------------------- 1 | export const gitHubAlertsUrl = 'https://github.com/kwsites/mock-repo/network/alerts'; 2 | export const gitHubPullRequest = 'https://github.com/kwsites/mock-repo/pull/new/new-branch-fff'; 3 | export const gitLabPullRequest = 4 | 'https://gitlab/kwsites/mock-repo/-/merge_requests/new?merge_request%5Bsource_branch%5D=new-branch-name-here'; 5 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/push/index.ts: -------------------------------------------------------------------------------- 1 | export * from './push-deleted-branch'; 2 | export * from './push-new-branch'; 3 | export * from './push-new-branch-remote-says-vulnerabilities'; 4 | export * from './push-new-branch-with-tags'; 5 | export * from './push-update-existing-branch'; 6 | export * from './constants'; 7 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/push/push-deleted-branch.ts: -------------------------------------------------------------------------------- 1 | import { createFixture } from '../create-fixture'; 2 | 3 | const stdOut = ` 4 | To github.com:kwsites/mock-repo.git 5 | - :refs/heads/feature/something [deleted] 6 | Done 7 | `; 8 | const stdErr = ` 9 | Pushing to git@github.com:kwsites/mock-repo.git 10 | updating local tracking ref 'refs/remotes/origin/feature/something' 11 | `; 12 | 13 | export const pushDeletedBranch = createFixture(stdOut, stdErr); 14 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/push/push-new-branch-remote-says-vulnerabilities.ts: -------------------------------------------------------------------------------- 1 | import { gitHubAlertsUrl, gitHubPullRequest } from './constants'; 2 | import { createFixture } from '../create-fixture'; 3 | 4 | const stdErr = `Pushing to git@github.com:kwsites/mock-repo.git 5 | remote: 6 | remote: Create a pull request for 'new-branch-fff' on GitHub by visiting: 7 | remote: ${gitHubPullRequest} 8 | remote: 9 | remote: GitHub found 12 vulnerabilities on kwsites/mock-repo's default branch (12 moderate). To find out more, visit: 10 | remote: ${gitHubAlertsUrl} 11 | remote: 12 | updating local tracking ref 'refs/remotes/origin/new-branch-fff'`; 13 | const stdOut = `To github.com:kwsites/mock-repo.git 14 | * refs/heads/features/some-branch:refs/heads/features/some-branch [new branch] 15 | Branch 'features/some-branch' set up to track remote branch 'features/some-branch' from 'origin'. 16 | Done`; 17 | 18 | export const pushNewBranchWithVulnerabilities = createFixture(stdOut, stdErr); 19 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/push/push-new-branch-with-tags.ts: -------------------------------------------------------------------------------- 1 | import { createFixture } from '../create-fixture'; 2 | 3 | const stdErr = `Pushing to git@github.com:kwsites/mock-repo.git 4 | updating local tracking ref 'refs/remotes/origin/new-branch-hhh'`; 5 | const stdOut = `To github.com:kwsites/mock-repo.git 6 | = refs/tags/tag-one:refs/tags/tag-one [up to date] 7 | * refs/heads/new-branch-hhh:refs/heads/new-branch-hhh [new branch] 8 | * refs/tags/tag-two:refs/tags/tag-two [new tag] 9 | Branch 'new-branch-hhh' set up to track remote branch 'new-branch-hhh' from 'origin'. 10 | Done`; 11 | 12 | export const pushNewBranchWithTags = createFixture(stdOut, stdErr); 13 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/push/push-new-branch.ts: -------------------------------------------------------------------------------- 1 | import { gitLabPullRequest } from './constants'; 2 | import { createFixture } from '../create-fixture'; 3 | 4 | const stdOut = `To github.com:kwsites/mock-repo.git 5 | * refs/heads/new-branch-name-here:refs/heads/new-branch-name-here [new branch] 6 | Branch 'new-branch-name-here' set up to track remote branch 'new-branch-name-here' from 'origin'. 7 | Done`; 8 | 9 | const stdErr = `Pushing to git@github.com:kwsites/mock-repo.git 10 | remote: 11 | remote: To create a merge request for new-branch-name-here, visit: 12 | remote: ${gitLabPullRequest} 13 | remote: 14 | updating local tracking ref 'refs/remotes/origin/new-branch-name-here'`; 15 | 16 | export const pushNewBranch = createFixture(stdOut, stdErr); 17 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/push/push-update-existing-branch.ts: -------------------------------------------------------------------------------- 1 | import { createFixture } from '../create-fixture'; 2 | 3 | const stdErr = `Pushing to git@github.com:kwsites/mock-repo.git 4 | updating local tracking ref 'refs/remotes/origin/master' 5 | To github.com:kwsites/mock-repo.git 6 | refs/heads/master:refs/heads/master 7259553..5a2ba71 7 | Done`; 8 | const stdOut = `To github.com:kwsites/mock-repo.git 9 | refs/heads/master:refs/heads/master 7259553..5a2ba71 10 | Done`; 11 | 12 | export const pushUpdateExistingBranch = createFixture(stdOut, stdErr); 13 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/responses/branch.ts: -------------------------------------------------------------------------------- 1 | export function branchSummary(...lines: string[]) { 2 | return lines.join('\n'); 3 | } 4 | 5 | export function branchSummaryLine(commit: string, hash = '', current = false) { 6 | const prefix = current ? '*' : ' '; 7 | const branch = hash || commit.replace(/[^a-z]/i, '').substr(0, 5); 8 | 9 | return `${prefix} branch-${branch} ${hash || branch.substr(0, 5)} ${commit}`; 10 | } 11 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/responses/commit.ts: -------------------------------------------------------------------------------- 1 | export const commitResultSingleFile = ` 2 | 3 | [foo 8f7d107] done 4 | Author: Some Author 5 | 1 files changed, 2 deletions(-) 6 | 7 | `; 8 | 9 | export const commitResultNoneStaged = ` 10 | On branch master 11 | Your branch is ahead of 'origin/master' by 1 commit. 12 | (use "git push" to publish your local commits) 13 | 14 | Changes not staged for commit: 15 | modified: src/some-file.js 16 | modified: src/another-file.js 17 | no changes added to commit 18 | `; 19 | 20 | export function commitToRepoRoot({ 21 | message = 'Commit Message', 22 | hash = 'b13bdd8', 23 | fileName = 'file-name', 24 | } = {}) { 25 | return ` 26 | [master (root-commit) ${hash}] ${message} 27 | 1 file changed, 1 insertion(+) 28 | create mode 100644 ${fileName} 29 | `; 30 | } 31 | 32 | export function commitToBranch({ 33 | message = 'Commit Message', 34 | hash = 'b13bdd8', 35 | fileName = 'file-name', 36 | branch = 'branch', 37 | } = {}) { 38 | return ` 39 | [${branch} ${hash}] ${message} 40 | 1 file changed, 1 insertion(+) 41 | create mode 100644 ${fileName} 42 | `; 43 | } 44 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/responses/diff.ts: -------------------------------------------------------------------------------- 1 | import { createFixture } from '../create-fixture'; 2 | 3 | type SmallNumber = 0 | 1 | 2; 4 | 5 | function change(count: number, sign: '-' | '+') { 6 | const label = sign === '-' ? 'deletion' : 'insertion'; 7 | 8 | switch (count) { 9 | case 0: 10 | return ''; 11 | case 1: 12 | return `, 1 ${label}(${sign})`; 13 | default: 14 | return `, ${count} ${label}s(${sign})`; 15 | } 16 | } 17 | 18 | function line(insertions: SmallNumber, deletions: SmallNumber, fileName: string) { 19 | return ` 20 | ${fileName} | ${insertions + deletions} ${''.padEnd(insertions, '+')}${''.padEnd( 21 | deletions, 22 | '-' 23 | )}`; 24 | } 25 | 26 | export function diffSummarySingleFile( 27 | insertions: SmallNumber = 1, 28 | deletions: SmallNumber = 2, 29 | fileName = 'package.json' 30 | ) { 31 | const stdOut = `${line(insertions, deletions, fileName)} 32 | 1 file changed${change(insertions, '+')}${change(deletions, '-')} 33 | `; 34 | return createFixture(stdOut, ''); 35 | } 36 | 37 | export function diffSummaryMultiFile( 38 | ...files: Array<{ fileName: string; insertions?: SmallNumber; deletions?: SmallNumber }> 39 | ) { 40 | let add = 0; 41 | let del = 0; 42 | let stdOut = ''; 43 | files.forEach(({ insertions = 0, deletions = 0, fileName }) => { 44 | stdOut += line(insertions, deletions, fileName); 45 | add += insertions; 46 | del += deletions; 47 | }); 48 | 49 | stdOut += ` 50 | ${files.length} file${files.length === 1 ? '' : 's'} changed ${change(add, '+')}${change( 51 | del, 52 | '-' 53 | )} 54 | `; 55 | return createFixture(stdOut, ''); 56 | } 57 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/responses/merge.ts: -------------------------------------------------------------------------------- 1 | type StringLike = string | (() => string); 2 | 3 | export function autoMergeResponse(...responses: StringLike[]): string { 4 | let response = responses.map((r) => (typeof r === 'function' ? r() : String(r))).join(''); 5 | if (/^CONFLICT/.test(response)) { 6 | response += `\nAutomatic merge failed; fix conflicts and then commit the result.`; 7 | } 8 | 9 | return response; 10 | } 11 | 12 | export function autoMergeConflict(fileName = 'fail.txt', reason = 'content') { 13 | return `${autoMergeFile(fileName)} 14 | CONFLICT (${reason}): Merge conflict in ${fileName}`; 15 | } 16 | 17 | export function autoMergeFile(fileName = 'pass.txt') { 18 | return ` 19 | Auto-merging ${fileName}`; 20 | } 21 | 22 | export function mergeMadeByStrategy(strategy: 'recursive') { 23 | return ` 24 | Merge made by the '${strategy}' strategy. 25 | ccc | 6 +++--- 26 | dd | 2 ++ 27 | 2 files changed, 5 insertions(+), 3 deletions(-) 28 | create mode 100644 dd`; 29 | } 30 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/responses/remote-messages.ts: -------------------------------------------------------------------------------- 1 | import { createFixture } from '../create-fixture'; 2 | 3 | const stdErr = ` 4 | remote: Enumerating objects: 5, done. 5 | remote: Counting objects: 100% (5/5), done. 6 | remote: Compressing objects: 100% (3/3), done. 7 | remote: Total 5 (delta 2), reused 5 (delta 2), pack-reused 0 8 | `; 9 | 10 | export const remoteMessagesObjectEnumeration = createFixture('', stdErr); 11 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/responses/show.ts: -------------------------------------------------------------------------------- 1 | import { createFixture } from '../create-fixture'; 2 | 3 | export function showAbbrevCommitSingleFile() { 4 | const stdOut = ` 5 | commit 2d4d33a 6 | Author: Steve King 7 | Date: Sun Oct 11 00:06:10 2015 +0200 8 | 9 | Some commit message 10 | 11 | diff --git a/src/file.js b/src/file.js 12 | index ab02a9b..5000197 100644 13 | --- a/src/file.js 14 | +++ b/src/file.js 15 | @@ -468,8 +468,13 @@ 16 | existing unchanged content 17 | - removed content 18 | + added content 19 | remaining content 20 | `; 21 | return createFixture(stdOut, ''); 22 | } 23 | -------------------------------------------------------------------------------- /simple-git/test/unit/__fixtures__/responses/status.ts: -------------------------------------------------------------------------------- 1 | import { createFixture } from '../create-fixture'; 2 | import { NULL } from '../../../../src/lib/utils'; 3 | 4 | export function stagedRenamed(from = 'from.ext', to = 'to.ext', workingDir = ' ') { 5 | return `R${workingDir} ${to}${NULL}${from}`; 6 | } 7 | 8 | export function stagedRenamedWithModifications(from = 'from.ext', to = 'to.ext') { 9 | return stagedRenamed(from, to, 'M'); 10 | } 11 | 12 | export function stagedDeleted(file = 'staged-deleted.ext') { 13 | return `D ${file}`; 14 | } 15 | 16 | export function unStagedDeleted(file = 'un-staged-deleted.ext') { 17 | return ` D ${file}`; 18 | } 19 | 20 | export function stagedModified(file = 'staged-modified.ext') { 21 | return `M ${file}`; 22 | } 23 | 24 | export function stagedIgnored(file = 'ignored.ext') { 25 | return `!! ${file}`; 26 | } 27 | 28 | export function statusResponse(branch = 'main', ...files: Array string)>) { 29 | const stdOut: string[] = [ 30 | `## ${branch}`, 31 | ...files.map((file) => (typeof file === 'function' ? file() : file)), 32 | ]; 33 | 34 | return createFixture(stdOut.join(NULL), ''); 35 | } 36 | -------------------------------------------------------------------------------- /simple-git/test/unit/add.spec.ts: -------------------------------------------------------------------------------- 1 | import { SimpleGit } from '../../typings'; 2 | import { assertExecutedCommands, closeWithSuccess, newSimpleGit } from './__fixtures__'; 3 | 4 | describe('add', () => { 5 | let git: SimpleGit; 6 | 7 | beforeEach(() => (git = newSimpleGit())); 8 | 9 | it('adds a single file', async () => { 10 | const queue = git.add('file.ext'); 11 | await closeWithSuccess('raw response'); 12 | 13 | expect(await queue).toBe('raw response'); 14 | assertExecutedCommands('add', 'file.ext'); 15 | }); 16 | 17 | it('adds multiple files', async () => { 18 | const queue = git.add(['file.one', 'file.two']); 19 | await closeWithSuccess('raw response'); 20 | 21 | expect(await queue).toBe('raw response'); 22 | assertExecutedCommands('add', 'file.one', 'file.two'); 23 | }); 24 | 25 | it('adds files with trailing callback', async () => { 26 | const callback = jest.fn(); 27 | const queue = git.add(['file.one', 'file.two'], callback); 28 | await closeWithSuccess('raw response'); 29 | 30 | expect(await queue).toBe('raw response'); 31 | expect(callback).toHaveBeenCalledWith(null, 'raw response'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /simple-git/test/unit/args.log-format.spec.ts: -------------------------------------------------------------------------------- 1 | import { LogFormat, logFormatFromCommand } from '../../src/lib/args/log-format'; 2 | 3 | describe('log-format', function () { 4 | it.each<[LogFormat, string[]]>([ 5 | [LogFormat.NONE, []], 6 | [LogFormat.NONE, ['foo', 'bar', '--nothing']], 7 | [LogFormat.STAT, ['foo', '--stat', 'bar']], 8 | [LogFormat.STAT, ['foo', '--stat=4096', '--bar']], 9 | [LogFormat.NUM_STAT, ['foo', '--numstat', '--bar']], 10 | [LogFormat.NAME_ONLY, ['--name-only', 'foo', '--bar']], 11 | [LogFormat.NAME_STATUS, ['--name-status']], 12 | ])('Picks %s from %s', (format, args) => { 13 | expect(logFormatFromCommand(args)).toBe(format); 14 | }); 15 | 16 | it('picks the first format', () => { 17 | expect(logFormatFromCommand(['--stat', '--numstat'])).toBe(LogFormat.STAT); 18 | expect(logFormatFromCommand(['--numstat', '--stat'])).toBe(LogFormat.NUM_STAT); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /simple-git/test/unit/check-ignore.spec.ts: -------------------------------------------------------------------------------- 1 | import { assertExecutedCommands, closeWithSuccess, newSimpleGit } from './__fixtures__'; 2 | import { SimpleGit } from '../../typings'; 3 | 4 | describe('checkIgnore', () => { 5 | let git: SimpleGit; 6 | let callback: jest.Mock; 7 | 8 | beforeEach(() => { 9 | git = newSimpleGit(); 10 | callback = jest.fn(); 11 | }); 12 | 13 | it('with single excluded file specified', async () => { 14 | const queue = git.checkIgnore('foo.log', callback); 15 | closeWithSuccess('foo.log'); 16 | 17 | expect(callback).toHaveBeenCalledWith(null, await queue); 18 | assertExecutedCommands('check-ignore', 'foo.log'); 19 | }); 20 | 21 | it('with two excluded files specified', async () => { 22 | const queue = git.checkIgnore(['foo.log', 'bar.log']); 23 | closeWithSuccess(` 24 | foo.log 25 | bar.log 26 | `); 27 | 28 | expect(await queue).toEqual(['foo.log', 'bar.log']); 29 | assertExecutedCommands('check-ignore', 'foo.log', 'bar.log'); 30 | }); 31 | 32 | it('with no excluded files', async () => { 33 | const queue = git.checkIgnore(['foo.log', 'bar.log']); 34 | closeWithSuccess(); 35 | 36 | expect(await queue).toEqual([]); 37 | assertExecutedCommands('check-ignore', 'foo.log', 'bar.log'); 38 | }); 39 | 40 | it('with spaces in file names', async () => { 41 | const queue = git.checkIgnore('foo space .log'); 42 | closeWithSuccess(' foo space .log '); 43 | 44 | expect(await queue).toEqual(['foo space .log']); 45 | assertExecutedCommands('check-ignore', 'foo space .log'); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /simple-git/test/unit/child-process.spec.ts: -------------------------------------------------------------------------------- 1 | import { promiseError } from '@kwsites/promise-result'; 2 | import { 3 | assertChildProcessEnvironmentVariables, 4 | assertGitError, 5 | closeWithError, 6 | closeWithSuccess, 7 | newSimpleGit, 8 | } from './__fixtures__'; 9 | import { SimpleGit } from '../../typings'; 10 | 11 | describe('child-process', () => { 12 | let git: SimpleGit; 13 | let callback: jest.Mock; 14 | 15 | beforeEach(() => { 16 | git = newSimpleGit(); 17 | callback = jest.fn(); 18 | }); 19 | 20 | it('handles child process errors', async () => { 21 | const queue = git.init(callback); 22 | await closeWithError('SOME ERROR'); 23 | 24 | const error = await promiseError(queue); 25 | expect(callback).toHaveBeenCalledWith(error, undefined); 26 | assertGitError(error, 'SOME ERROR'); 27 | }); 28 | 29 | it('passes empty set of environment variables by default', async () => { 30 | git.init(callback); 31 | await closeWithSuccess(); 32 | assertChildProcessEnvironmentVariables(undefined); 33 | }); 34 | 35 | it('supports passing individual environment variables to the underlying child process', async () => { 36 | git.env('foo', 'bar').env('baz', 'bat').init(); 37 | await closeWithSuccess(); 38 | assertChildProcessEnvironmentVariables({ foo: 'bar', baz: 'bat' }); 39 | }); 40 | 41 | it('supports passing environment variables to the underlying child process', async () => { 42 | git.env({ foo: 'bar' }).init(); 43 | await closeWithSuccess(); 44 | assertChildProcessEnvironmentVariables({ foo: 'bar' }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /simple-git/test/unit/count-objects.spec.ts: -------------------------------------------------------------------------------- 1 | import { closeWithSuccess, like, newSimpleGit } from './__fixtures__'; 2 | import { CountObjectsResult } from '../../typings'; 3 | 4 | const COUNT_OBJ_RESPONSE = ` 5 | count: 323 6 | size: 7920 7 | in-pack: 8134 8 | packs: 1 9 | size-pack: 3916 10 | prune-packable: 0 11 | garbage: 0 12 | size-garbage: 0 13 | `; 14 | 15 | describe('count-objects', () => { 16 | it('gets the repo object counts', async () => { 17 | const task = newSimpleGit().countObjects(); 18 | await closeWithSuccess(COUNT_OBJ_RESPONSE); 19 | const objects = await task; 20 | 21 | expect(objects).toEqual( 22 | like({ 23 | count: 323, 24 | size: 7920, 25 | inPack: 8134, 26 | packs: 1, 27 | sizePack: 3916, 28 | }) 29 | ); 30 | }); 31 | 32 | it('ignores unknown properties', async () => { 33 | const task = newSimpleGit().countObjects(); 34 | await closeWithSuccess('foo: 123'); 35 | expect(await task).not.toHaveProperty('foo'); 36 | }); 37 | 38 | it('ignores invalid values', async () => { 39 | const task = newSimpleGit().countObjects(); 40 | await closeWithSuccess('packs: error'); 41 | expect(await task).toHaveProperty('packs', 0); 42 | }); 43 | 44 | it.each<[string, keyof CountObjectsResult, number]>([ 45 | ['prune-packable', 'prunePackable', 100], 46 | ['garbage', 'garbage', 101], 47 | ['size-garbage', 'sizeGarbage', 102], 48 | ])('parses %s property', async (key, asKey, value) => { 49 | const task = newSimpleGit().countObjects(); 50 | await closeWithSuccess(`${key}: ${value}`); 51 | 52 | expect(await task).toEqual(like({ [asKey]: value })); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /simple-git/test/unit/cwd.spec.ts: -------------------------------------------------------------------------------- 1 | import { SimpleGit } from 'typings'; 2 | import { 3 | assertNoExecutedTasks, 4 | isInvalidDirectory, 5 | isValidDirectory, 6 | newSimpleGit, 7 | wait, 8 | } from './__fixtures__'; 9 | 10 | describe('cwd', () => { 11 | let git: SimpleGit; 12 | 13 | beforeEach(() => { 14 | git = newSimpleGit(); 15 | }); 16 | 17 | it('to a known directory', async () => { 18 | isValidDirectory(); 19 | 20 | const callback = jest.fn(); 21 | git.cwd('./', callback); 22 | 23 | await wait(); 24 | expect(callback).toHaveBeenCalledWith(null, './'); 25 | assertNoExecutedTasks(); 26 | }); 27 | 28 | it('to an invalid directory', async () => { 29 | isInvalidDirectory(); 30 | 31 | const callback = jest.fn((err) => expect(err.message).toMatch('invalid_path')); 32 | git.cwd('./invalid_path', callback); 33 | 34 | await wait(); 35 | expect(callback).toHaveBeenCalledWith(expect.any(Error), undefined); 36 | assertNoExecutedTasks(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /simple-git/test/unit/first-commit.spec.ts: -------------------------------------------------------------------------------- 1 | import { assertExecutedCommands, closeWithSuccess, newSimpleGit } from './__fixtures__'; 2 | 3 | describe('firstCommit', () => { 4 | it('gets the first commit in a repo async', async () => { 5 | const task = newSimpleGit().firstCommit(); 6 | await closeWithSuccess('a-commit-hash\n'); 7 | 8 | expect(await task).toBe('a-commit-hash'); 9 | assertExecutedCommands('rev-list', '--max-parents=0', 'HEAD'); 10 | }); 11 | 12 | it('gets the first commit in a repo callback', async () => { 13 | const callback = jest.fn(); 14 | const task = newSimpleGit().firstCommit(callback); 15 | await closeWithSuccess('a-commit-hash\n'); 16 | 17 | expect(callback).toHaveBeenCalledWith(null, await task); 18 | assertExecutedCommands('rev-list', '--max-parents=0', 'HEAD'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /simple-git/test/unit/hash-object.spec.ts: -------------------------------------------------------------------------------- 1 | import { SimpleGit } from 'typings'; 2 | import { assertExecutedCommands, closeWithSuccess, newSimpleGit } from './__fixtures__'; 3 | 4 | describe('hash-object', () => { 5 | let git: SimpleGit; 6 | 7 | beforeEach(() => (git = newSimpleGit())); 8 | 9 | it('trims the output', async () => { 10 | const task = git.hashObject('index.js'); 11 | await closeWithSuccess(` 12 | 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 13 | `); 14 | 15 | assertExecutedCommands('hash-object', 'index.js'); 16 | expect(await task).toEqual('3b18e512dba79e4c8300dd08aeb37f8e728b8dad'); 17 | }); 18 | 19 | it('optionally writes the result', async () => { 20 | git.hashObject('index.js', true); 21 | await closeWithSuccess(); 22 | assertExecutedCommands('hash-object', 'index.js', '-w'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /simple-git/test/unit/output-handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { closeWithSuccess, newSimpleGit } from './__fixtures__'; 2 | import { SimpleGit } from '../../typings'; 3 | 4 | describe('outputHandler', () => { 5 | let git: SimpleGit; 6 | let callback: jest.Mock; 7 | 8 | beforeEach(() => { 9 | git = newSimpleGit(); 10 | callback = jest.fn(); 11 | }); 12 | 13 | it('passes name of command to callback', async () => { 14 | const queue = git.outputHandler(callback).init(); 15 | 16 | closeWithSuccess(); 17 | await queue; 18 | 19 | expect(callback).toHaveBeenCalledWith('git', expect.any(Object), expect.any(Object), [ 20 | 'init', 21 | ]); 22 | }); 23 | 24 | it('passes name of command to callback - custom binary', async () => { 25 | const queue = git.outputHandler(callback).customBinary('something').init(); 26 | 27 | closeWithSuccess(); 28 | await queue; 29 | 30 | expect(callback).toHaveBeenCalledWith('something', expect.any(Object), expect.any(Object), [ 31 | 'init', 32 | ]); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /simple-git/test/unit/plugins/plugin.completion-detection.spec.ts: -------------------------------------------------------------------------------- 1 | import { newSimpleGit, theChildProcessMatching, wait } from '../__fixtures__'; 2 | import { MockChildProcess } from '../__mocks__/mock-child-process'; 3 | 4 | describe('completionDetectionPlugin', () => { 5 | function process(proc: MockChildProcess, data: string, close = false, exit = false) { 6 | proc.stdout.$emit('data', Buffer.from(data)); 7 | close && proc.$emit('close', 1); 8 | exit && proc.$emit('exit', 1); 9 | } 10 | 11 | it('can respond to just close events', async () => { 12 | const git = newSimpleGit({ 13 | completion: { 14 | onClose: true, 15 | onExit: false, 16 | }, 17 | }); 18 | 19 | const output = Promise.race([git.raw('foo'), git.raw('bar')]); 20 | 21 | await wait(); 22 | 23 | process(theChildProcessMatching(['foo']), 'foo', false, true); 24 | process(theChildProcessMatching(['bar']), 'bar', true, false); 25 | 26 | expect(await output).toBe('bar'); 27 | }); 28 | 29 | it('can respond to just exit events', async () => { 30 | const git = newSimpleGit({ 31 | completion: { 32 | onClose: false, 33 | onExit: true, 34 | }, 35 | }); 36 | 37 | const output = Promise.race([git.raw('foo'), git.raw('bar')]); 38 | 39 | await wait(); 40 | 41 | process(theChildProcessMatching(['foo']), 'foo', false, true); 42 | process(theChildProcessMatching(['bar']), 'bar', true, false); 43 | 44 | expect(await output).toBe('foo'); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /simple-git/test/unit/plugins/plugin.error.spec.ts: -------------------------------------------------------------------------------- 1 | import { promiseError } from '@kwsites/promise-result'; 2 | import { assertGitError, closeWithError, closeWithSuccess, newSimpleGit } from '../__fixtures__'; 3 | 4 | import { GitError } from '../../..'; 5 | 6 | describe('errorDetectionPlugin', () => { 7 | it('can throw with custom content', async () => { 8 | const errors = jest.fn().mockReturnValue(Buffer.from('foo')); 9 | const git = newSimpleGit({ errors }).init(); 10 | await closeWithError('err'); 11 | 12 | assertGitError(await promiseError(git), 'foo'); 13 | }); 14 | 15 | it('can throw error when otherwise deemed ok', async () => { 16 | const errors = jest.fn().mockReturnValue(new Error('FAIL')); 17 | const git = newSimpleGit({ errors }).init(); 18 | await closeWithSuccess('OK'); 19 | 20 | expect(errors).toHaveBeenCalledWith(undefined, { 21 | exitCode: 0, 22 | stdErr: [], 23 | stdOut: [expect.any(Buffer)], 24 | }); 25 | assertGitError(await promiseError(git), 'FAIL'); 26 | }); 27 | 28 | it('can ignore errors that would otherwise throw', async () => { 29 | const errors = jest.fn(); 30 | 31 | const git = newSimpleGit({ errors }).raw('foo'); 32 | await closeWithError('OUT', 100); 33 | 34 | expect(errors).toHaveBeenCalledWith(expect.any(GitError), { 35 | exitCode: 100, 36 | stdOut: [], 37 | stdErr: [expect.any(Buffer)], 38 | }); 39 | expect(await promiseError(git)).toBeUndefined(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /simple-git/test/unit/rebase.spec.ts: -------------------------------------------------------------------------------- 1 | import { assertExecutedCommands, closeWithSuccess, newSimpleGit } from './__fixtures__'; 2 | import { SimpleGit } from '../../typings'; 3 | 4 | describe('rebase', () => { 5 | let git: SimpleGit; 6 | let callback: jest.Mock; 7 | 8 | beforeEach(() => { 9 | git = newSimpleGit(); 10 | callback = jest.fn(); 11 | }); 12 | 13 | it('rebases', async () => { 14 | const queue = git.rebase(callback); 15 | await closeWithSuccess('some data'); 16 | 17 | expect(await queue).toBe('some data'); 18 | expect(callback).toHaveBeenCalledWith(null, 'some data'); 19 | assertExecutedCommands('rebase'); 20 | }); 21 | 22 | it('rebases with array of options', async () => { 23 | git.rebase(['master', 'topic']); 24 | await closeWithSuccess('some data'); 25 | assertExecutedCommands('rebase', 'master', 'topic'); 26 | }); 27 | 28 | it('rebases with object of options', async () => { 29 | git.rebase({ '--foo': null }); 30 | await closeWithSuccess('some data'); 31 | assertExecutedCommands('rebase', '--foo'); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /simple-git/test/unit/rev-parse.spec.ts: -------------------------------------------------------------------------------- 1 | import { assertExecutedCommands, closeWithSuccess, newSimpleGit } from './__fixtures__'; 2 | import { SimpleGit } from '../../typings'; 3 | 4 | describe('revParse', () => { 5 | let git: SimpleGit; 6 | let callback: jest.Mock; 7 | 8 | beforeEach(() => { 9 | callback = jest.fn(); 10 | }); 11 | 12 | describe('simple-git', () => { 13 | beforeEach(() => (git = newSimpleGit())); 14 | 15 | it('called with a string', async () => { 16 | git.revparse('some string'); 17 | await closeWithSuccess(); 18 | assertExecutedCommands('rev-parse', 'some string'); 19 | }); 20 | 21 | it('called with an array of strings', async () => { 22 | git.revparse(['another', 'string']); 23 | await closeWithSuccess(); 24 | assertExecutedCommands('rev-parse', 'another', 'string'); 25 | }); 26 | 27 | it('called with all arguments', async () => { 28 | const queue = git.revparse('foo', { bar: null }, callback); 29 | await closeWithSuccess(' some data '); 30 | expect(await queue).toBe('some data'); 31 | expect(callback).toHaveBeenCalledWith(null, 'some data'); 32 | assertExecutedCommands('rev-parse', 'foo', 'bar'); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /simple-git/test/unit/revert.spec.ts: -------------------------------------------------------------------------------- 1 | import { promiseError } from '@kwsites/promise-result'; 2 | import { 3 | assertExecutedCommands, 4 | assertGitError, 5 | assertNoExecutedTasks, 6 | closeWithSuccess, 7 | newSimpleGit, 8 | } from './__fixtures__'; 9 | import { SimpleGit } from '../../typings'; 10 | 11 | import { TaskConfigurationError } from '../..'; 12 | 13 | describe('revert', () => { 14 | let git: SimpleGit; 15 | let callback: jest.Mock; 16 | 17 | beforeEach(() => { 18 | git = newSimpleGit(); 19 | callback = jest.fn(); 20 | }); 21 | 22 | it('reverts', async () => { 23 | git.revert('HEAD~3', callback); 24 | await closeWithSuccess(); 25 | assertExecutedCommands('revert', 'HEAD~3'); 26 | }); 27 | 28 | it('reverts a range', async () => { 29 | git.revert('master~5..master~2', { '-n': null }, callback); 30 | await closeWithSuccess(); 31 | assertExecutedCommands('revert', '-n', 'master~5..master~2'); 32 | }); 33 | 34 | it('requires a string', async () => { 35 | const err = await promiseError(git.revert(callback as any)); 36 | assertGitError(err, 'Commit must be a string', TaskConfigurationError); 37 | assertNoExecutedTasks(); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /simple-git/test/unit/rm.spec.ts: -------------------------------------------------------------------------------- 1 | import { assertExecutedCommands, closeWithSuccess, newSimpleGit } from './__fixtures__'; 2 | import { SimpleGit } from '../../typings'; 3 | 4 | describe('rm', () => { 5 | let git: SimpleGit; 6 | let callback: jest.Mock; 7 | 8 | beforeEach(() => { 9 | git = newSimpleGit(); 10 | callback = jest.fn(); 11 | }); 12 | 13 | it('remove single file', async () => { 14 | git.rm('string', callback); 15 | await closeWithSuccess(); 16 | assertExecutedCommands('rm', '-f', 'string'); 17 | }); 18 | 19 | it('remove multiple files', async () => { 20 | git.rm(['foo', 'bar'], callback); 21 | await closeWithSuccess(); 22 | assertExecutedCommands('rm', '-f', 'foo', 'bar'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /simple-git/test/unit/show.spec.ts: -------------------------------------------------------------------------------- 1 | import { assertExecutedCommands, closeWithSuccess, newSimpleGit } from './__fixtures__'; 2 | import { SimpleGit } from '../../typings'; 3 | import { showAbbrevCommitSingleFile } from './__fixtures__/responses/show'; 4 | 5 | describe('show', () => { 6 | let git: SimpleGit; 7 | let callback: jest.Mock; 8 | 9 | beforeEach(() => { 10 | git = newSimpleGit(); 11 | callback = jest.fn(); 12 | }); 13 | 14 | it('permits binary responses', async () => { 15 | const task = git.showBuffer('HEAD:img.jpg'); 16 | await closeWithSuccess('some response'); 17 | const result = await task; 18 | 19 | expect(result).toEqual(expect.any(Buffer)); 20 | expect(result.toString('utf8')).toEqual('some response'); 21 | }); 22 | 23 | it('passes the response through without editing', async () => { 24 | const { stdOut } = showAbbrevCommitSingleFile(); 25 | 26 | const queue = git.show(callback); 27 | await closeWithSuccess(stdOut); 28 | expect(await queue).toBe(stdOut); 29 | }); 30 | 31 | it('allows the use of an array of options', async () => { 32 | git.show(['--abbrev-commit', 'foo', 'bar'], callback); 33 | await closeWithSuccess(); 34 | assertExecutedCommands('show', '--abbrev-commit', 'foo', 'bar'); 35 | }); 36 | 37 | it('allows an options string', async () => { 38 | git.show('--abbrev-commit', callback); 39 | await closeWithSuccess(); 40 | assertExecutedCommands('show', '--abbrev-commit'); 41 | }); 42 | 43 | it('allows an options object', async () => { 44 | git.show({ '--foo': null }, callback); 45 | await closeWithSuccess(); 46 | assertExecutedCommands('show', '--foo'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /simple-git/test/unit/stash-list.spec.ts: -------------------------------------------------------------------------------- 1 | import { SimpleGit } from '../../typings'; 2 | import { assertExecutedCommands, closeWithSuccess, like, newSimpleGit } from './__fixtures__'; 3 | import { 4 | COMMIT_BOUNDARY, 5 | SPLITTER, 6 | START_BOUNDARY, 7 | } from '../../src/lib/parsers/parse-list-log-summary'; 8 | 9 | describe('stashList', () => { 10 | let git: SimpleGit; 11 | let callback: jest.Mock; 12 | 13 | beforeEach(() => { 14 | git = newSimpleGit(); 15 | callback = jest.fn(); 16 | }); 17 | 18 | it('with no stash', async () => { 19 | const expected = like({ 20 | total: 0, 21 | all: [], 22 | }); 23 | const queue = git.stashList(callback); 24 | closeWithSuccess(); 25 | 26 | expect(await queue).toEqual(expected); 27 | expect(callback).toHaveBeenCalledWith(null, expected); 28 | }); 29 | 30 | it('commands - default', async () => { 31 | git.stashList(); 32 | await closeWithSuccess(); 33 | 34 | assertExecutedCommands( 35 | 'stash', 36 | 'list', 37 | `--pretty=format:${START_BOUNDARY}%H${SPLITTER}%aI${SPLITTER}%s${SPLITTER}%D${SPLITTER}%b${SPLITTER}%aN${SPLITTER}%aE${COMMIT_BOUNDARY}` 38 | ); 39 | }); 40 | 41 | it('commands - custom splitter', async () => { 42 | const splitter = ';;'; 43 | 44 | git.stashList({ splitter }); 45 | await closeWithSuccess(); 46 | 47 | assertExecutedCommands( 48 | 'stash', 49 | 'list', 50 | `--pretty=format:${START_BOUNDARY}%H${splitter}%aI${splitter}%s${splitter}%D${splitter}%b${splitter}%aN${splitter}%aE${COMMIT_BOUNDARY}` 51 | ); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /simple-git/test/unit/stash.spec.ts: -------------------------------------------------------------------------------- 1 | import { assertExecutedCommands, closeWithSuccess, newSimpleGit } from './__fixtures__'; 2 | import { SimpleGit } from '../../typings'; 3 | 4 | describe('stash', () => { 5 | let git: SimpleGit; 6 | let callback: jest.Mock; 7 | 8 | beforeEach(() => { 9 | git = newSimpleGit(); 10 | callback = jest.fn(); 11 | }); 12 | 13 | it('supports selecting all files with a star', async () => { 14 | git.stash(['push', '--', '*']); 15 | await closeWithSuccess(); 16 | 17 | assertExecutedCommands('stash', 'push', '--', '*'); 18 | }); 19 | 20 | it('stash working directory', async () => { 21 | const queue = git.stash(callback); 22 | await closeWithSuccess(); 23 | 24 | assertExecutedCommands('stash'); 25 | expect(callback).toHaveBeenCalledWith(null, await queue); 26 | }); 27 | 28 | it('stash pop', async () => { 29 | const queue = git.stash(['pop'], callback); 30 | await closeWithSuccess(); 31 | 32 | assertExecutedCommands('stash', 'pop'); 33 | expect(callback).toHaveBeenCalledWith(null, await queue); 34 | }); 35 | 36 | it('stash with options no handler', async () => { 37 | git.stash(['branch', 'some-branch']); 38 | await closeWithSuccess(); 39 | 40 | assertExecutedCommands('stash', 'branch', 'some-branch'); 41 | }); 42 | 43 | it('stash with options object no handler', async () => { 44 | git.stash({ '--foo': null }); 45 | await closeWithSuccess(); 46 | 47 | assertExecutedCommands('stash', '--foo'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /simple-git/test/unit/update-server-info.spec.ts: -------------------------------------------------------------------------------- 1 | import { promiseError } from '@kwsites/promise-result'; 2 | import { assertExecutedCommands, closeWithSuccess, newSimpleGit } from './__fixtures__'; 3 | import { SimpleGit } from '../../typings'; 4 | 5 | describe('updateServerInfo', () => { 6 | let git: SimpleGit; 7 | 8 | beforeEach(() => (git = newSimpleGit())); 9 | 10 | it('update server info', async () => { 11 | const queue = git.updateServerInfo(); 12 | closeWithSuccess(); 13 | 14 | expect(await promiseError(queue)).toBeUndefined(); 15 | assertExecutedCommands('update-server-info'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /simple-git/test/unit/version.spec.ts: -------------------------------------------------------------------------------- 1 | import { closeWithError, closeWithSuccess, newSimpleGit } from './__fixtures__'; 2 | 3 | describe('version', () => { 4 | it('sringifies to version', async () => { 5 | const version = newSimpleGit().version(); 6 | await closeWithSuccess('git version 2.50.10 (Apple Git-133)'); 7 | 8 | expect(String(await version)).toBe('2.50.10'); 9 | }); 10 | 11 | it('detects missing', async () => { 12 | const version = newSimpleGit().version(); 13 | await closeWithError('FAIL', -2); 14 | 15 | expect(await version).toEqual({ 16 | installed: false, 17 | major: 0, 18 | minor: 0, 19 | patch: 0, 20 | agent: '', 21 | }); 22 | }); 23 | 24 | it('parses apple', async () => { 25 | const version = newSimpleGit().version(); 26 | await closeWithSuccess('git version 2.32.1 (Apple Git-133)'); 27 | 28 | expect(await version).toEqual({ 29 | installed: true, 30 | major: 2, 31 | minor: 32, 32 | patch: 1, 33 | agent: 'Apple Git-133', 34 | }); 35 | }); 36 | 37 | it('parses git from source', async () => { 38 | const version = newSimpleGit().version(); 39 | await closeWithSuccess('git version 2.37.GIT'); 40 | 41 | expect(await version).toEqual({ 42 | installed: true, 43 | major: 2, 44 | minor: 37, 45 | patch: 'GIT', 46 | agent: '', 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /simple-git/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "skipLibCheck": false, 5 | "target": "es2015", 6 | "module": "commonjs", 7 | "lib": ["dom", "esnext"], 8 | "rootDir": ".", 9 | // optionally pipe output an alternative directory 10 | // TODO once all content converted to TS 11 | // "outDir": "lib", 12 | "downlevelIteration": true, 13 | "strict": true, 14 | "removeComments": false, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "moduleResolution": "node", 20 | "allowSyntheticDefaultImports": false, 21 | "esModuleInterop": false, 22 | "forceConsistentCasingInFileNames": true, 23 | "baseUrl": ".", 24 | "composite": true, 25 | "declaration": true, 26 | "declarationMap": false, 27 | "sourceMap": true, 28 | "paths": {}, 29 | "noEmit": true 30 | }, 31 | "exclude": ["node_modules"], 32 | "include": [ 33 | "src/lib/**/*.ts", 34 | "test/__fixtures__/**/*.ts", 35 | "test/integration/**/*.ts", 36 | "test/unit/**/*.ts", 37 | "typings/index.d.ts" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /simple-git/tsconfig.release.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "composite": false, 5 | "noEmit": false, 6 | "emitDeclarationOnly": true, 7 | "skipLibCheck": true 8 | }, 9 | "exclude": ["node_modules", "test"], 10 | "include": ["src/lib/**/*.ts", "typings/index.d.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /simple-git/typings/errors.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../src/lib/errors/git-error'; 2 | export * from '../src/lib/errors/git-construct-error'; 3 | export * from '../src/lib/errors/git-plugin-error'; 4 | export * from '../src/lib/errors/git-response-error'; 5 | export * from '../src/lib/errors/task-configuration-error'; 6 | -------------------------------------------------------------------------------- /simple-git/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | import { SimpleGitFactory } from './simple-git'; 2 | 3 | export * from './simple-git'; 4 | export * from './errors'; 5 | export * from './response'; 6 | export * from './types'; 7 | 8 | export declare const gitP: SimpleGitFactory; 9 | 10 | export declare const simpleGit: SimpleGitFactory; 11 | 12 | export default simpleGit; 13 | -------------------------------------------------------------------------------- /simple-git/typings/types.d.ts: -------------------------------------------------------------------------------- 1 | export type { RemoteWithoutRefs, RemoteWithRefs } from '../src/lib/responses/GetRemoteSummary'; 2 | export type { LogOptions, DefaultLogFields } from '../src/lib/tasks/log'; 3 | 4 | export type { 5 | outputHandler, 6 | Options, 7 | TaskOptions, 8 | SimpleGitOptions, 9 | SimpleGitProgressEvent, 10 | SimpleGitTaskCallback, 11 | } from '../src/lib/types'; 12 | 13 | export { pathspec } from '../src/lib/args/pathspec'; 14 | export type { ApplyOptions } from '../src/lib/tasks/apply-patch'; 15 | export { CheckRepoActions } from '../src/lib/tasks/check-is-repo'; 16 | export { CleanOptions, CleanMode } from '../src/lib/tasks/clean'; 17 | export type { CloneOptions } from '../src/lib/tasks/clone'; 18 | export { GitConfigScope } from '../src/lib/tasks/config'; 19 | export type { CountObjectsResult } from '../src/lib/tasks/count-objects'; 20 | export { DiffNameStatus } from '../src/lib/tasks/diff-name-status'; 21 | export { GitGrepQuery, grepQueryBuilder } from '../src/lib/tasks/grep'; 22 | export { ResetOptions, ResetMode } from '../src/lib/tasks/reset'; 23 | export type { VersionResult } from '../src/lib/tasks/version'; 24 | --------------------------------------------------------------------------------