diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e469e58 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +# Ignore artifacts: +build +coverage +node_modules +dist diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} diff --git a/package-lock.json b/package-lock.json index 423a663..2b737aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "better-commits", - "version": "1.10.0", + "version": "1.15.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "better-commits", - "version": "1.10.0", + "version": "1.15.0", "license": "MIT", "dependencies": { "@clack/core": "^0.3.1", @@ -28,6 +28,7 @@ "@types/configstore": "^6.0.0", "@types/node": "^18.14.5", "jiti": "^1.17.0", + "prettier": "3.2.5", "semantic-release": "^21.0.1", "tsup": "^6.6.3", "tsx": "^3.12.3" @@ -6510,6 +6511,21 @@ } } }, + "node_modules/prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -12530,6 +12546,12 @@ "yaml": "^1.10.2" } }, + "prettier": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", + "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "dev": true + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/package.json b/package.json index 2633ffd..b23e0a7 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "@types/configstore": "^6.0.0", "@types/node": "^18.14.5", "jiti": "^1.17.0", + "prettier": "3.2.5", "semantic-release": "^21.0.1", "tsup": "^6.6.3", "tsx": "^3.12.3" diff --git a/readme.md b/readme.md index 6d08361..a7c2ab8 100644 --- a/readme.md +++ b/readme.md @@ -1,39 +1,39 @@

![bc-gradient](https://github.com/Everduin94/better-commits/assets/14320878/2f94e6ea-a40f-4f3e-b0b2-5cc7d83a9a7d) - [![better commits is enabled](https://img.shields.io/badge/better--commits-enabled?style=for-the-badge&logo=git&color=a6e3a1&logoColor=D9E0EE&labelColor=302D41)](https://github.com/Everduin94/better-commits) [![downloads](https://img.shields.io/npm/dt/better-commits.svg?style=for-the-badge&logo=npm&color=74c7ec&logoColor=D9E0EE&labelColor=302D41)](https://www.npmjs.com/package/better-commits) [![discord](https://img.shields.io/badge/discord-join--discord?style=for-the-badge&logo=discord&color=cba6f7&logoColor=D9E0EE&labelColor=302D41)](https://discord.gg/grHVnZwYup) +

A CLI for writing better commits, following the conventional commits specification.

- https://github.com/Everduin94/better-commits/assets/14320878/8fb15d46-17c4-4e5d-80d9-79abe0a2a00a - ## โœจ Features + - Generate conventional commits through a series of prompts - Highly configurable with sane defaults -- Infers ticket and commit-type from branch for consistent & fast commits +- Infers ticket and commit-type from branch for consistent & fast commits - Consistent branch creation with flexible workflow hooks via `better-branch` - Interactive git status/add on commit -- Preview commit messages in color +- Preview commit messages in color - Support for git emojis per commit-type - Configure globally or per repository - Config validation and error messaging As a side-effect of formatting messages + - Auto populate PR title / body - Automate semantic releases -- Automate changelogs +- Automate changelogs - Automatically link & close related tickets / issues ## ๐Ÿ“ฆ Installation - + ```sh npm install -g better-commits ``` @@ -54,15 +54,17 @@ Some of the values in these prompts will be infered by your branch name and auto To better understand these prompts and their intention, read [Conventional Commits Summary](https://www.conventionalcommits.org/en/v1.0.0-beta.4/#summary) ## โš™๏ธ Configuration - + ### Global Your first time running `better-commits`, a default config will be generated in your `$HOME` directory, named `.better-commits.json` + - This config will be used if a repository-specific config cannot be found. ### Repository To create a **repository-specific config**, navigate to the root of your project. + - Run `better-commits-init` - This will create a default config named `.better-commits.json` - Properties such as `confirm_with_editor` and `overrides` will prefer the global config @@ -73,6 +75,7 @@ Better-commits (& better-branch) are highly flexible with sane defaults. These o > [!NOTE]
> All properties are optional and can be removed from the config. It will be replaced by the default at run-time. +> > - See `.better-commits.json` in this repository as an example ### ๐Ÿ’ซ Default JSON Config @@ -80,184 +83,172 @@ Better-commits (& better-branch) are highly flexible with sane defaults. These o
Expand / Collapse - ```json +```json { - "check_status": true, - "commit_type": { - "enable": true, - "initial_value": "feat", - "infer_type_from_branch": true, - "append_emoji_to_label": false, - "append_emoji_to_commit": false, - "options": [ - { - "value": "feat", - "label": "feat", - "hint": "A new feature", - "emoji": "โœจ", - "trailer": "Changelog: feature" - }, - { - "value": "fix", - "label": "fix", - "hint": "A bug fix", - "emoji": "๐Ÿ›", - "trailer": "Changelog: fix" - }, - { - "value": "docs", - "label": "docs", - "hint": "Documentation only changes", - "emoji": "๐Ÿ“š", - "trailer": "Changelog: documentation" - }, - { - "value": "refactor", - "label": "refactor", - "hint": "A code change that neither fixes a bug nor adds a feature", - "emoji": "๐Ÿ”จ", - "trailer": "Changelog: refactor" - }, - { - "value": "perf", - "label": "perf", - "hint": "A code change that improves performance", - "emoji": "๐Ÿš€", - "trailer": "Changelog: performance" - }, - { - "value": "test", - "label": "test", - "hint": "Adding missing tests or correcting existing tests", - "emoji": "๐Ÿšจ", - "trailer": "Changelog: test" - }, - { - "value": "build", - "label": "build", - "hint": "Changes that affect the build system or external dependencies", - "emoji": "๐Ÿšง", - "trailer": "Changelog: build" - }, - { - "value": "ci", - "label": "ci", - "hint": "Changes to our CI configuration files and scripts", - "emoji": "๐Ÿค–", - "trailer": "Changelog: ci" - }, - { - "value": "chore", - "label": "chore", - "hint": "Other changes that do not modify src or test files", - "emoji": "๐Ÿงน", - "trailer": "Changelog: chore" - }, - { - "value": "", - "label": "none" - } - ] - }, - "commit_scope": { - "enable": true, - "custom_scope": false, - "initial_value": "app", - "options": [ - { - "value": "app", - "label": "app" - }, - { - "value": "shared", - "label": "shared" - }, - { - "value": "server", - "label": "server" - }, - { - "value": "tools", - "label": "tools" - }, - { - "value": "", - "label": "none" - } - ] - }, - "check_ticket": { - "infer_ticket": true, - "confirm_ticket": true, - "add_to_title": true, - "append_hashtag": false, - "prepend_hashtag": "Never", - "surround": "", - "title_position": "start" - }, - "commit_title": { - "max_size": 70 - }, - "commit_body": { - "enable": true, - "required": false - }, - "commit_footer": { - "enable": true, - "initial_value": [], - "options": [ - "closes", - "trailer", - "breaking-change", - "deprecated", - "custom" - ] - }, - "breaking_change": { - "add_exclamation_to_title": true - }, - "confirm_commit": true, - "confirm_with_editor": false, - "print_commit_output": true, - "branch_pre_commands": [], - "branch_post_commands": [], - "worktree_pre_commands": [], - "worktree_post_commands": [], - "branch_user": { - "enable": true, - "required": false, - "separator": "/" - }, - "branch_type": { - "enable": true, - "separator": "/" - }, - "branch_version": { - "enable": false, - "required": false, - "separator": "/" - }, - "branch_ticket": { - "enable": true, - "required": false, - "separator": "-" - }, - "branch_description": { - "max_length": 70, - "separator": "" - }, - "branch_action_default": "branch", - "branch_order": [ - "user", - "version", - "type", - "ticket", - "description" - ], - "enable_worktrees": true, - "overrides": { - "shell": "/bin/sh" - } + "check_status": true, + "commit_type": { + "enable": true, + "initial_value": "feat", + "infer_type_from_branch": true, + "append_emoji_to_label": false, + "append_emoji_to_commit": false, + "options": [ + { + "value": "feat", + "label": "feat", + "hint": "A new feature", + "emoji": "โœจ", + "trailer": "Changelog: feature" + }, + { + "value": "fix", + "label": "fix", + "hint": "A bug fix", + "emoji": "๐Ÿ›", + "trailer": "Changelog: fix" + }, + { + "value": "docs", + "label": "docs", + "hint": "Documentation only changes", + "emoji": "๐Ÿ“š", + "trailer": "Changelog: documentation" + }, + { + "value": "refactor", + "label": "refactor", + "hint": "A code change that neither fixes a bug nor adds a feature", + "emoji": "๐Ÿ”จ", + "trailer": "Changelog: refactor" + }, + { + "value": "perf", + "label": "perf", + "hint": "A code change that improves performance", + "emoji": "๐Ÿš€", + "trailer": "Changelog: performance" + }, + { + "value": "test", + "label": "test", + "hint": "Adding missing tests or correcting existing tests", + "emoji": "๐Ÿšจ", + "trailer": "Changelog: test" + }, + { + "value": "build", + "label": "build", + "hint": "Changes that affect the build system or external dependencies", + "emoji": "๐Ÿšง", + "trailer": "Changelog: build" + }, + { + "value": "ci", + "label": "ci", + "hint": "Changes to our CI configuration files and scripts", + "emoji": "๐Ÿค–", + "trailer": "Changelog: ci" + }, + { + "value": "chore", + "label": "chore", + "hint": "Other changes that do not modify src or test files", + "emoji": "๐Ÿงน", + "trailer": "Changelog: chore" + }, + { + "value": "", + "label": "none" + } + ] + }, + "commit_scope": { + "enable": true, + "custom_scope": false, + "initial_value": "app", + "options": [ + { + "value": "app", + "label": "app" + }, + { + "value": "shared", + "label": "shared" + }, + { + "value": "server", + "label": "server" + }, + { + "value": "tools", + "label": "tools" + }, + { + "value": "", + "label": "none" + } + ] + }, + "check_ticket": { + "infer_ticket": true, + "confirm_ticket": true, + "add_to_title": true, + "append_hashtag": false, + "prepend_hashtag": "Never", + "surround": "", + "title_position": "start" + }, + "commit_title": { + "max_size": 70 + }, + "commit_body": { + "enable": true, + "required": false + }, + "commit_footer": { + "enable": true, + "initial_value": [], + "options": ["closes", "trailer", "breaking-change", "deprecated", "custom"] + }, + "breaking_change": { + "add_exclamation_to_title": true + }, + "confirm_commit": true, + "confirm_with_editor": false, + "print_commit_output": true, + "branch_pre_commands": [], + "branch_post_commands": [], + "worktree_pre_commands": [], + "worktree_post_commands": [], + "branch_user": { + "enable": true, + "required": false, + "separator": "/" + }, + "branch_type": { + "enable": true, + "separator": "/" + }, + "branch_version": { + "enable": false, + "required": false, + "separator": "/" + }, + "branch_ticket": { + "enable": true, + "required": false, + "separator": "-" + }, + "branch_description": { + "max_length": 70, + "separator": "" + }, + "branch_action_default": "branch", + "branch_order": ["user", "version", "type", "ticket", "description"], + "enable_worktrees": true, + "overrides": { + "shell": "/bin/sh" + } } ``` @@ -265,7 +256,8 @@ Better-commits (& better-branch) are highly flexible with sane defaults. These o > [!NOTE]
> Some properties allow a set of specifc string values -> - See *config file explanations* for possible values +> +> - See _config file explanations_ for possible values ### ๐Ÿ”ญ Config File Explanations @@ -282,78 +274,80 @@ Expand to see explanations and possible values } ``` -| Property | Description | -| -------- | ----------- | -| `check_status` | If true run interactive `git status` | -| `commit_type.enable` | If true include commit type | -| `commit_type.initial_value` | Initial selection of commit type | -| `commit_type.infer_type_from_branch` | If true infer type from branch name | -| `commit_type.append_emoji_to_label` | If true append emoji to prompt | -| `commit_type.append_emoji_to_commit` | If true append emoji to commit | -| `commit_type.options.value` | Commit type prompt value | -| `commit_type.options.label` | Commit type prompt label | -| `commit_type.options.hint` | Commit type inline hint (like this) | -| `commit_type.options.emoji` | Commit type emoji | -| `commit_type.options.trailer` | Commit type trailer | -| `commit_scope.enable` | If true include commit scope | -| `commit_scope.custom_scope` | If true allow custom scope at run-time | -| `commit_scope.initial_value` | Default commit scope selected | -| `commit_scope.options.value` | Commit scope value | -| `commit_scope.options.label` | Commit scope label | -| `check_ticket.infer_ticket` | If true infer ticket from branch name | -| `check_ticket.confirm_ticket` | If true manually confirm inference | -| `check_ticket.add_to_title` | If true add ticket to title | -| `check_ticket.append_hashtag` | **Deprecated**: see prepend_hashtag | -| `check_ticket.prepend_hashtag` | "Never" (default), "Prompt", or "Always" | -| `check_ticket.title_position` | "start" (of description) (default), "end", "before-colon", "beginning" (of the entire commit title) | -| `check_ticket.surround` | "" (default), "[]", "()", "{}" - Wraps ticket in title | -| `commit_title.max_size` | Max size of title including scope, type, etc... | -| `commit_body.enable` | If true include body | -| `commit_body.required` | If true body is required | -| `commit_footer.enable` | If true include footer | -| `commit_footer.initial_value` | Initial values selected in footer | -| `commit_footer.options` | Footer options | -| `breaking_change.add_exclamation_to_title` | If true adds exclamation mark to title for breaking changes | -| `confirm_commit` | If true manually confirm commit at end | -| `confirm_with_editor` | Confirm / Edit commit with $GIT_EDITOR / $EDITOR | -| `print_commit_output` | If true pretty print commit preview | -| `overrides.shell` | Override default shell, useful for windows users | +| Property | Description | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------- | +| `check_status` | If true run interactive `git status` | +| `commit_type.enable` | If true include commit type | +| `commit_type.initial_value` | Initial selection of commit type | +| `commit_type.infer_type_from_branch` | If true infer type from branch name | +| `commit_type.append_emoji_to_label` | If true append emoji to prompt | +| `commit_type.append_emoji_to_commit` | If true append emoji to commit | +| `commit_type.options.value` | Commit type prompt value | +| `commit_type.options.label` | Commit type prompt label | +| `commit_type.options.hint` | Commit type inline hint (like this) | +| `commit_type.options.emoji` | Commit type emoji | +| `commit_type.options.trailer` | Commit type trailer | +| `commit_scope.enable` | If true include commit scope | +| `commit_scope.custom_scope` | If true allow custom scope at run-time | +| `commit_scope.initial_value` | Default commit scope selected | +| `commit_scope.options.value` | Commit scope value | +| `commit_scope.options.label` | Commit scope label | +| `check_ticket.infer_ticket` | If true infer ticket from branch name | +| `check_ticket.confirm_ticket` | If true manually confirm inference | +| `check_ticket.add_to_title` | If true add ticket to title | +| `check_ticket.append_hashtag` | **Deprecated**: see prepend_hashtag | +| `check_ticket.prepend_hashtag` | "Never" (default), "Prompt", or "Always" | +| `check_ticket.title_position` | "start" (of description) (default), "end", "before-colon", "beginning" (of the entire commit title) | +| `check_ticket.surround` | "" (default), "[]", "()", "{}" - Wraps ticket in title | +| `commit_title.max_size` | Max size of title including scope, type, etc... | +| `commit_body.enable` | If true include body | +| `commit_body.required` | If true body is required | +| `commit_footer.enable` | If true include footer | +| `commit_footer.initial_value` | Initial values selected in footer | +| `commit_footer.options` | Footer options | +| `breaking_change.add_exclamation_to_title` | If true adds exclamation mark to title for breaking changes | +| `confirm_commit` | If true manually confirm commit at end | +| `confirm_with_editor` | Confirm / Edit commit with $GIT_EDITOR / $EDITOR | +| `print_commit_output` | If true pretty print commit preview | +| `overrides.shell` | Override default shell, useful for windows users | Branch configuration (same config file, split for readability) -| Property | Description | -| -------- | ----------- | -| `branch_pre_commands` | Array of shell commands to run before branching | -| `branch_post_commands` | Array of shell commands to run after branching | -| `worktree_pre_commands` | Array of shell commands to run before creating worktree | -| `worktree_post_commands` | Array of shell commands to run after creating worktree | -| `branch_user.enable` | If enabled include user name | -| `branch_user.required` | If enabled require user name | -| `branch_user.separator` | Branch delimeter - "/" (default), "-", "_" | -| `branch_type.enable` | If enabled include type | -| `branch_type.separator` | Branch delimeter - "/" (default), "-", "_" | -| `branch_ticket.enable` | If enabled include ticket | -| `branch_ticket.required` | If enabled require ticket | -| `branch_ticket.separator` | Branch delimeter - "/", "-" (default), "_" | -| `branch_description.max_length` | Max length branch name | -| `branch_description.separator` | Branch delimeter - "" (default), "/", "-", "_" | -| `branch_version.enable` | If enabled include version | -| `branch_version.required` | If enabled require version | -| `branch_version.separator` | Branch delimeter - "", "/" (default), "-", "_" | -| `branch_order` | Order of branch name values (doesn't effect prompt order) | -| `branch_action_default` | "branch" or "worktree" | -| `enable_worktrees` | If false, always default to branch action | +| Property | Description | +| ------------------------------- | --------------------------------------------------------- | +| `branch_pre_commands` | Array of shell commands to run before branching | +| `branch_post_commands` | Array of shell commands to run after branching | +| `worktree_pre_commands` | Array of shell commands to run before creating worktree | +| `worktree_post_commands` | Array of shell commands to run after creating worktree | +| `branch_user.enable` | If enabled include user name | +| `branch_user.required` | If enabled require user name | +| `branch_user.separator` | Branch delimeter - "/" (default), "-", "\_" | +| `branch_type.enable` | If enabled include type | +| `branch_type.separator` | Branch delimeter - "/" (default), "-", "\_" | +| `branch_ticket.enable` | If enabled include ticket | +| `branch_ticket.required` | If enabled require ticket | +| `branch_ticket.separator` | Branch delimeter - "/", "-" (default), "\_" | +| `branch_description.max_length` | Max length branch name | +| `branch_description.separator` | Branch delimeter - "" (default), "/", "-", "\_" | +| `branch_version.enable` | If enabled include version | +| `branch_version.required` | If enabled require version | +| `branch_version.separator` | Branch delimeter - "", "/" (default), "-", "\_" | +| `branch_order` | Order of branch name values (doesn't effect prompt order) | +| `branch_action_default` | "branch" or "worktree" | +| `enable_worktrees` | If false, always default to branch action |
### ๐Ÿ”Ž Inference -`better-commits` will attempt to infer the ticket/issue and the commit-type from your branch name. It will auto populate the corresponding field if found. +`better-commits` will attempt to infer the ticket/issue and the commit-type from your branch name. It will auto populate the corresponding field if found. + +**Ticket / Issue-Number** -**Ticket / Issue-Number** - If a `STRING-NUMBER` or `NUMBER` are at the start of the branch name or after a `/` **Commit Type** + - If a type is at the start of the branch or is followed by a `/` ## ๐ŸŒณ Better Branch @@ -363,6 +357,7 @@ Branch configuration (same config file, split for readability) > Make sure to try it out! Better branch is a secondary feature that works with better commits + - Supports consistent branch naming conventions - Uses same type-list/prompt from your config - Enables better-commits to infer type & ticket @@ -386,20 +381,25 @@ The worktree flow creates a folder/worktree with your **branch description** and ### Pre/Post Branch Checkout Hooks Optionally configure pre and post checkout commands, for example: + - checkout and rebase main before branching - run `npm install` before branching - run `npm run dev` after branching -See *branch_pre_commands* and *branch_post_commands* in default config. (or *worktree_pre_commands* and *worktree_post_commands* for creating worktrees) +See _branch_pre_commands_ and _branch_post_commands_ in default config. (or _worktree_pre_commands_ and _worktree_post_commands_ for creating worktrees) ## ๐ŸŒŒ Mildly Interesting ### Building / Versioning + `better-commits` works with [Semantic Release](https://github.com/semantic-release/semantic-release) -- See *package.json* and *.github/workflows/publish.yml* for example + +- See _package.json_ and _.github/workflows/publish.yml_ for example ### Github -If you use `better-commits` to create your *first* commit on a new branch + +If you use `better-commits` to create your _first_ commit on a new branch + - When you open a PR for that branch, it will properly **auto-populate the title and body**. - When you squash/merge, all later commits like "addressing comments" or "fixing mistake". Will be prefixed with an asterisk for easy deletion. This way, you **maintain your pretty commit even when squashing**. @@ -410,22 +410,24 @@ If you're using Github issues to track your work, and select the `closes` footer `better-commits` can append a commit trailer per commit type. This allows you to [automate change logs](https://docs.gitlab.com/ee/user/project/changelogs.html) with tools like Gitlab. ### Fun Facts + [better-commits](https://packagephobia.com/result?p=better-commits) is much smaller than its alternative [commitizen](https://packagephobia.com/result?p=commitizen) `better-commits` uses native `git` commands under the hood. So any hooks, tools, or staging should work as if it was a normal commit. Setting `confirm_with_editor=true` will allow you to edit/confirm a commit with your editor. + - For example, to edit with Neovim: `git config --global core.editor "nvim"` - For VS Code, `git config --global core.editor "code -n --wait"` > [!NOTE]
-> Enjoy learning, open source technology, and note-taking? [Join our Discord!](https://discord.gg/grHVnZwYup) +> Enjoy learning, open source technology, and note-taking? [Join our Discord!](https://discord.gg/grHVnZwYup) -You can add this badge to your repository to display that you're using a better-commits repository config +You can add this badge to your repository to display that you're using a better-commits repository config -| Markdown | Result | -| -------- | ------ | -| `[![better commits is enabled](https://img.shields.io/badge/better--commits-enabled?style=for-the-badge&logo=git&color=a6e3a1&logoColor=D9E0EE&labelColor=302D41)](https://github.com/Everduin94/better-commits)` | [![better commits is enabled](https://img.shields.io/badge/better--commits-enabled?style=for-the-badge&logo=git&color=a6e3a1&logoColor=D9E0EE&labelColor=302D41)](https://github.com/Everduin94/better-commits) | +| Markdown | Result | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `[![better commits is enabled](https://img.shields.io/badge/better--commits-enabled?style=for-the-badge&logo=git&color=a6e3a1&logoColor=D9E0EE&labelColor=302D41)](https://github.com/Everduin94/better-commits)` | [![better commits is enabled](https://img.shields.io/badge/better--commits-enabled?style=for-the-badge&logo=git&color=a6e3a1&logoColor=D9E0EE&labelColor=302D41)](https://github.com/Everduin94/better-commits) | --- @@ -440,19 +442,19 @@ You can add this badge to your repository to display that you're using a better- If your are having issues with multilines for commits on windows, you can override the shell via your `.better-commits.json` config. Example + ```json "overrides": { "shell": "c:\\Program Files\\Git\\bin\\bash.exe" } ``` -

๐ŸŒŸ Sponsors

[![flotes-g-2](https://github.com/Everduin94/Everduin94/assets/14320878/b0fd0aa5-ca9d-4a2d-8579-7616140927a7)](https://flotes.app) - + [Markdown Notetaking - Built for Learning](https://flotes.app)

diff --git a/src/branch.ts b/src/branch.ts index 1d0b57c..9a56f20 100644 --- a/src/branch.ts +++ b/src/branch.ts @@ -130,8 +130,8 @@ async function main(config: z.infer) { }); p.log.info( `Switched to a new branch '${color.bgGreen( - " " + color.black(branch_name) + " " - )}'` + " " + color.black(branch_name) + " ", + )}'`, ); } catch (err) { process.exit(0); @@ -144,18 +144,18 @@ async function main(config: z.infer) { `git worktree add ${worktree_name} ${branch_flag} ${branch_name}`, { stdio: "inherit", - } + }, ); p.log.info( `Created a new worktree ${color.bgGreen( - +" " + color.black(worktree_name) + " " + +" " + color.black(worktree_name) + " ", )}, checked out branch ${color.bgGreen( - " " + color.black(branch_name) + " " - )}` + " " + color.black(branch_name) + " ", + )}`, ); p.log.info( color.bgMagenta(color.black(` cd ${worktree_name} `)) + - " to navigate to your new worktree" + " to navigate to your new worktree", ); chdir(worktree_name); } catch (err) { @@ -179,14 +179,14 @@ async function main(config: z.infer) { function build_branch( branch: z.infer, - config: z.infer + config: z.infer, ) { let res = ""; config.branch_order.forEach((b: z.infer) => { - const config_key: z.infer = `branch_${b}` - if (branch[b]) res += branch[b] + config[config_key].separator - }) - if (res.endsWith('-') || res.endsWith('/') || res.endsWith('_')) { + const config_key: z.infer = `branch_${b}`; + if (branch[b]) res += branch[b] + config[config_key].separator; + }); + if (res.endsWith("-") || res.endsWith("/") || res.endsWith("_")) { return res.slice(0, -1).trim(); } return res.trim(); @@ -198,7 +198,7 @@ function get_user_from_cache(): string { return config_store.get("username") ?? ""; } catch (err) { p.log.warn( - 'There was an issue accessing username from cache. Check that the folder "~/.config" exists' + 'There was an issue accessing username from cache. Check that the folder "~/.config" exists', ); } @@ -212,8 +212,8 @@ function verify_branch_name(branch_name: string): string { execSync(`git show-ref ${branch_name}`, { encoding: "utf-8" }); p.log.warning( color.yellow( - `${branch_name} already exists! Checking out existing branch.` - ) + `${branch_name} already exists! Checking out existing branch.`, + ), ); } catch (err) { // Branch does not exist diff --git a/src/index.ts b/src/index.ts index 4d80859..e274157 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,92 +1,131 @@ #! /usr/bin/env node -import * as p from '@clack/prompts'; -import color from 'picocolors'; -import { execSync } from 'child_process'; +import * as p from "@clack/prompts"; +import color from "picocolors"; +import { execSync } from "child_process"; import { chdir } from "process"; import { z } from "zod"; -import { CommitState, Config } from './zod-state'; -import { load_setup, addNewLine, SPACE_TO_SELECT, REGEX_SLASH_TAG, REGEX_SLASH_NUM, REGEX_START_TAG, REGEX_START_NUM, OPTIONAL_PROMPT, clean_commit_title, COMMIT_FOOTER_OPTIONS, infer_type_from_branch, Z_FOOTER_OPTIONS, CUSTOM_SCOPE_KEY, get_git_root, REGEX_SLASH_UND, REGEX_START_UND } from './utils'; -import { git_add, git_status } from './git'; +import { CommitState, Config } from "./zod-state"; +import { + load_setup, + addNewLine, + SPACE_TO_SELECT, + REGEX_SLASH_TAG, + REGEX_SLASH_NUM, + REGEX_START_TAG, + REGEX_START_NUM, + OPTIONAL_PROMPT, + clean_commit_title, + COMMIT_FOOTER_OPTIONS, + infer_type_from_branch, + Z_FOOTER_OPTIONS, + CUSTOM_SCOPE_KEY, + get_git_root, + REGEX_SLASH_UND, + REGEX_START_UND, +} from "./utils"; +import { git_add, git_status } from "./git"; main(load_setup()); export async function main(config: z.infer) { - let commit_state = CommitState.parse({}) + let commit_state = CommitState.parse({}); chdir(get_git_root()); if (config.check_status) { - let {index, work_tree} = git_status() - p.log.step(color.black(color.bgGreen(' Checking Git Status '))) - const staged_files = index.reduce((acc,curr,i: number) => color.green(acc+curr+addNewLine(index,i)), ''); - p.log.success('Changes to be committed:\n'+staged_files) + let { index, work_tree } = git_status(); + p.log.step(color.black(color.bgGreen(" Checking Git Status "))); + const staged_files = index.reduce( + (acc, curr, i: number) => color.green(acc + curr + addNewLine(index, i)), + "", + ); + p.log.success("Changes to be committed:\n" + staged_files); if (work_tree.length) { - const unstaged_files = work_tree.reduce((acc,curr,i: number) => color.red(acc+curr+addNewLine(work_tree,i)), ''); - p.log.error('Changes not staged for commit:\n'+unstaged_files) - const selected_for_staging = await p.multiselect({ - message: `Some files have not been staged, would you like to add them now? ${SPACE_TO_SELECT}`, - options: [{value: '.', label: '.'}, ...work_tree.map(v => ({value: v, label: v}))], - required: false, - }) as string[] - if (p.isCancel(selected_for_staging)) process.exit(0) + const unstaged_files = work_tree.reduce( + (acc, curr, i: number) => + color.red(acc + curr + addNewLine(work_tree, i)), + "", + ); + p.log.error("Changes not staged for commit:\n" + unstaged_files); + const selected_for_staging = (await p.multiselect({ + message: `Some files have not been staged, would you like to add them now? ${SPACE_TO_SELECT}`, + options: [ + { value: ".", label: "." }, + ...work_tree.map((v) => ({ value: v, label: v })), + ], + required: false, + })) as string[]; + if (p.isCancel(selected_for_staging)) process.exit(0); git_add(selected_for_staging); } - let updated_status = git_status() + let updated_status = git_status(); if (!updated_status.index.length) { - p.log.error(color.red('no changes added to commit (use "git add" and/or "git commit -a")')) + p.log.error( + color.red( + 'no changes added to commit (use "git add" and/or "git commit -a")', + ), + ); process.exit(0); } - } + } if (config.commit_type.enable) { - let message = 'Select a commit type'; - let initial_value = config.commit_type.initial_value + let message = "Select a commit type"; + let initial_value = config.commit_type.initial_value; if (config.commit_type.infer_type_from_branch) { - const options = config.commit_type.options.map(o => o.value) - const type_from_branch = infer_type_from_branch(options) + const options = config.commit_type.options.map((o) => o.value); + const type_from_branch = infer_type_from_branch(options); if (type_from_branch) { - message = `Commit type inferred from branch ${color.dim('(confirm / edit)')}` - initial_value = type_from_branch - } + message = `Commit type inferred from branch ${color.dim("(confirm / edit)")}`; + initial_value = type_from_branch; + } } - const value_to_data: Record = config.commit_type.options.reduce( - (acc, curr) => ({ ...acc, [curr.value]: {emoji: curr.emoji ?? '', trailer: curr.trailer ?? '' }}), {} - ) - const commit_type = await p.select( - { - message, - initialValue: initial_value, - options: config.commit_type.options, - } - ) - if (p.isCancel(commit_type)) process.exit(0) + const value_to_data: Record = + config.commit_type.options.reduce( + (acc, curr) => ({ + ...acc, + [curr.value]: { + emoji: curr.emoji ?? "", + trailer: curr.trailer ?? "", + }, + }), + {}, + ); + const commit_type = await p.select({ + message, + initialValue: initial_value, + options: config.commit_type.options, + }); + if (p.isCancel(commit_type)) process.exit(0); commit_state.trailer = value_to_data[commit_type].trailer; - commit_state.type = config.commit_type.append_emoji_to_commit ? - `${value_to_data[commit_type].emoji} ${commit_type}`.trim() - : commit_type; + commit_state.type = config.commit_type.append_emoji_to_commit + ? `${value_to_data[commit_type].emoji} ${commit_type}`.trim() + : commit_type; } if (config.commit_scope.enable) { - let commit_scope = await p.select({ - message: 'Select a commit scope', + let commit_scope = await p.select({ + message: "Select a commit scope", initialValue: config.commit_scope.initial_value, - options: config.commit_scope.options - }) - if (p.isCancel(commit_scope)) process.exit(0) + options: config.commit_scope.options, + }); + if (p.isCancel(commit_scope)) process.exit(0); if (commit_scope === CUSTOM_SCOPE_KEY && config.commit_scope.custom_scope) { - commit_scope = await p.text({ - message: 'Write a custom scope', - placeholder: '' - }) - if (p.isCancel(commit_scope)) process.exit(0) + commit_scope = await p.text({ + message: "Write a custom scope", + placeholder: "", + }); + if (p.isCancel(commit_scope)) process.exit(0); } commit_state.scope = commit_scope; } if (config.check_ticket.infer_ticket) { try { - const branch = execSync('git branch --show-current', {stdio : 'pipe' }).toString(); + const branch = execSync("git branch --show-current", { + stdio: "pipe", + }).toString(); const found: string[] = [ branch.match(REGEX_START_UND), branch.match(REGEX_SLASH_UND), @@ -95,170 +134,214 @@ export async function main(config: z.infer) { branch.match(REGEX_START_TAG), branch.match(REGEX_START_NUM), ] - .filter(v => v != null) - .map(v => v && v.length >= 2 ? v[1] : '') + .filter((v) => v != null) + .map((v) => (v && v.length >= 2 ? v[1] : "")); if (found.length && found[0]) { - commit_state.ticket = config.check_ticket.append_hashtag - || config.check_ticket.prepend_hashtag === 'Prompt' - ? '#' + found[0] : found[0] + commit_state.ticket = + config.check_ticket.append_hashtag || + config.check_ticket.prepend_hashtag === "Prompt" + ? "#" + found[0] + : found[0]; } - } catch(err: any) { + } catch (err: any) { // Can't find branch, fail silently } } if (config.check_ticket.confirm_ticket) { const user_commit_ticket = await p.text({ - message: commit_state.ticket ? `Ticket / issue inferred from branch ${color.dim('(confirm / edit)')}`: `Add ticket / issue ${OPTIONAL_PROMPT}`, - placeholder: '', + message: commit_state.ticket + ? `Ticket / issue inferred from branch ${color.dim("(confirm / edit)")}` + : `Add ticket / issue ${OPTIONAL_PROMPT}`, + placeholder: "", initialValue: commit_state.ticket, - }) - if (p.isCancel(user_commit_ticket)) process.exit(0) - commit_state.ticket = user_commit_ticket ?? ''; + }); + if (p.isCancel(user_commit_ticket)) process.exit(0); + commit_state.ticket = user_commit_ticket ?? ""; } - if (config.check_ticket.prepend_hashtag === 'Always' - && commit_state.ticket - && !commit_state.ticket.startsWith('#')) { - commit_state.ticket = '#' + commit_state.ticket; + if ( + config.check_ticket.prepend_hashtag === "Always" && + commit_state.ticket && + !commit_state.ticket.startsWith("#") + ) { + commit_state.ticket = "#" + commit_state.ticket; } - const commit_title = await p.text( - { - message: 'Write a brief title describing the commit', - placeholder: '', - validate: (value) => { - if (!value) return 'Please enter a title'; - const commit_scope_size = commit_state.scope ? commit_state.scope.length + 2 : 0 - const commit_type_size = commit_state.type.length; - const commit_ticket_size = config.check_ticket.add_to_title ? commit_state.ticket.length : 0 - if (commit_scope_size + commit_type_size + commit_ticket_size + value.length > config.commit_title.max_size) return `Exceeded max length. Title max [${config.commit_title.max_size}]`; - }, - - } - ) - if (p.isCancel(commit_title)) process.exit(0) + const commit_title = await p.text({ + message: "Write a brief title describing the commit", + placeholder: "", + validate: (value) => { + if (!value) return "Please enter a title"; + const commit_scope_size = commit_state.scope + ? commit_state.scope.length + 2 + : 0; + const commit_type_size = commit_state.type.length; + const commit_ticket_size = config.check_ticket.add_to_title + ? commit_state.ticket.length + : 0; + if ( + commit_scope_size + + commit_type_size + + commit_ticket_size + + value.length > + config.commit_title.max_size + ) + return `Exceeded max length. Title max [${config.commit_title.max_size}]`; + }, + }); + if (p.isCancel(commit_title)) process.exit(0); commit_state.title = clean_commit_title(commit_title); if (config.commit_body.enable) { const commit_body = await p.text({ - message: `Write a detailed description of the changes ${OPTIONAL_PROMPT}`, - placeholder: '', - validate: (val) => { - if (config.commit_body.required && !val) return 'Please enter a description' - } - - }) - if (p.isCancel(commit_body)) process.exit(0) - commit_state.body = commit_body ?? ''; + message: `Write a detailed description of the changes ${OPTIONAL_PROMPT}`, + placeholder: "", + validate: (val) => { + if (config.commit_body.required && !val) + return "Please enter a description"; + }, + }); + if (p.isCancel(commit_body)) process.exit(0); + commit_state.body = commit_body ?? ""; } if (config.commit_footer.enable) { const commit_footer = await p.multiselect({ - message: `Select optional footers ${SPACE_TO_SELECT}`, - initialValues: config.commit_footer.initial_value, - options: COMMIT_FOOTER_OPTIONS as {value: z.infer, label: string, hint: string}[], - required: false - }) - if (p.isCancel(commit_footer)) process.exit(0) - - if (commit_footer.includes('breaking-change')) { + message: `Select optional footers ${SPACE_TO_SELECT}`, + initialValues: config.commit_footer.initial_value, + options: COMMIT_FOOTER_OPTIONS as { + value: z.infer; + label: string; + hint: string; + }[], + required: false, + }); + if (p.isCancel(commit_footer)) process.exit(0); + + if (commit_footer.includes("breaking-change")) { const breaking_changes_title = await p.text({ - message: 'Breaking changes: Write a short title / summary', - placeholder: '', - validate: (value) => { - if (!value) return 'Please enter a title / summary' - } - }) - if (p.isCancel(breaking_changes_title)) process.exit(0) + message: "Breaking changes: Write a short title / summary", + placeholder: "", + validate: (value) => { + if (!value) return "Please enter a title / summary"; + }, + }); + if (p.isCancel(breaking_changes_title)) process.exit(0); const breaking_changes_body = await p.text({ - message: `Breaking Changes: Write a description & migration instructions ${OPTIONAL_PROMPT}`, - placeholder: '', - }) - if (p.isCancel(breaking_changes_body)) process.exit(0) + message: `Breaking Changes: Write a description & migration instructions ${OPTIONAL_PROMPT}`, + placeholder: "", + }); + if (p.isCancel(breaking_changes_body)) process.exit(0); commit_state.breaking_title = breaking_changes_title; commit_state.breaking_body = breaking_changes_body; } - if (commit_footer.includes('deprecated')) { + if (commit_footer.includes("deprecated")) { const deprecated_title = await p.text({ - message: 'Deprecated: Write a short title / summary', - placeholder: '', - validate: (value) => { - if (!value) return 'Please enter a title / summary' - } - }) - if (p.isCancel(deprecated_title)) process.exit(0) + message: "Deprecated: Write a short title / summary", + placeholder: "", + validate: (value) => { + if (!value) return "Please enter a title / summary"; + }, + }); + if (p.isCancel(deprecated_title)) process.exit(0); const deprecated_body = await p.text({ - message: `Deprecated: Write a description ${OPTIONAL_PROMPT}`, - placeholder: '', - }) - if (p.isCancel(deprecated_body)) process.exit(0) + message: `Deprecated: Write a description ${OPTIONAL_PROMPT}`, + placeholder: "", + }); + if (p.isCancel(deprecated_body)) process.exit(0); commit_state.deprecates_body = deprecated_body; commit_state.deprecates_title = deprecated_title; } - if (commit_footer.includes('closes')) { - commit_state.closes = 'Closes:' + if (commit_footer.includes("closes")) { + commit_state.closes = "Closes:"; } - if (commit_footer.includes('custom')) { + if (commit_footer.includes("custom")) { const custom_footer = await p.text({ - message: 'Write a custom footer', - placeholder: '', - }) - if (p.isCancel(custom_footer)) process.exit(0) + message: "Write a custom footer", + placeholder: "", + }); + if (p.isCancel(custom_footer)) process.exit(0); commit_state.custom_footer = custom_footer; } - if (!commit_footer.includes('trailer')) { - commit_state.trailer = ''; + if (!commit_footer.includes("trailer")) { + commit_state.trailer = ""; } } if (config.confirm_with_editor) { - const options = config.overrides.shell ? { shell: config.overrides.shell, stdio: 'inherit' } : { stdio: 'inherit' } - const trailer = commit_state.trailer ? `--trailer="${commit_state.trailer}"` : ''; - execSync(`git commit -m "${build_commit_string(commit_state, config, false, true, false)}" ${trailer} --edit`, options); + const options = config.overrides.shell + ? { shell: config.overrides.shell, stdio: "inherit" } + : { stdio: "inherit" }; + const trailer = commit_state.trailer + ? `--trailer="${commit_state.trailer}"` + : ""; + execSync( + `git commit -m "${build_commit_string(commit_state, config, false, true, false)}" ${trailer} --edit`, + options, + ); process.exit(0); } let continue_commit = true; - p.note(build_commit_string(commit_state, config, true, false, true), 'Commit Preview') + p.note( + build_commit_string(commit_state, config, true, false, true), + "Commit Preview", + ); if (config.confirm_commit) { - continue_commit = await p.confirm({message: 'Confirm Commit?'}) as boolean; - if (p.isCancel(continue_commit)) process.exit(0) + continue_commit = (await p.confirm({ + message: "Confirm Commit?", + })) as boolean; + if (p.isCancel(continue_commit)) process.exit(0); } if (!continue_commit) { - p.log.info('Exiting without commit') - process.exit(0) - } - - try { - const options = config.overrides.shell ? { shell: config.overrides.shell } : {} - const trailer = commit_state.trailer ? `--trailer="${commit_state.trailer}"` : ''; - const output = execSync(`git commit -m "${build_commit_string(commit_state, config, false, true, false)}" ${trailer}`, options).toString().trim(); - if (config.print_commit_output) p.log.info(output) - } catch(err) { - p.log.error('Something went wrong when committing: ' + err) + p.log.info("Exiting without commit"); + process.exit(0); + } + + try { + const options = config.overrides.shell + ? { shell: config.overrides.shell } + : {}; + const trailer = commit_state.trailer + ? `--trailer="${commit_state.trailer}"` + : ""; + const output = execSync( + `git commit -m "${build_commit_string(commit_state, config, false, true, false)}" ${trailer}`, + options, + ) + .toString() + .trim(); + if (config.print_commit_output) p.log.info(output); + } catch (err) { + p.log.error("Something went wrong when committing: " + err); } } -function build_commit_string(commit_state: z.infer, +function build_commit_string( + commit_state: z.infer, config: z.infer, colorize: boolean = false, escape_quotes: boolean = false, - include_trailer: boolean = false + include_trailer: boolean = false, ): string { - let commit_string = ''; + let commit_string = ""; if (commit_state.type) { - commit_string += colorize ? color.blue(commit_state.type) : commit_state.type - } + commit_string += colorize + ? color.blue(commit_state.type) + : commit_state.type; + } if (commit_state.scope) { - const scope = colorize ? color.cyan(commit_state.scope) : commit_state.scope; - commit_string += `(${scope})` + const scope = colorize + ? color.cyan(commit_state.scope) + : commit_state.scope; + commit_string += `(${scope})`; } let title_ticket = commit_state.ticket; @@ -266,84 +349,122 @@ function build_commit_string(commit_state: z.infer, if (commit_state.ticket && surround) { const open_token = surround.charAt(0); const close_token = surround.charAt(1); - title_ticket = `${open_token}${commit_state.ticket}${close_token}` + title_ticket = `${open_token}${commit_state.ticket}${close_token}`; } - - const position_beginning = config.check_ticket.title_position === 'beginning'; + + const position_beginning = config.check_ticket.title_position === "beginning"; if (title_ticket && config.check_ticket.add_to_title && position_beginning) { commit_string = `${colorize ? color.magenta(title_ticket) : title_ticket} ${commit_string}`; } - const position_before_colon = config.check_ticket.title_position === "before-colon" - if (title_ticket && config.check_ticket.add_to_title && position_before_colon) { - const spacing = commit_state.scope || (commit_state.type && !config.check_ticket.surround) ? ' ' : ''; - commit_string += colorize ? color.magenta(spacing + title_ticket) : spacing + title_ticket + const position_before_colon = + config.check_ticket.title_position === "before-colon"; + if ( + title_ticket && + config.check_ticket.add_to_title && + position_before_colon + ) { + const spacing = + commit_state.scope || (commit_state.type && !config.check_ticket.surround) + ? " " + : ""; + commit_string += colorize + ? color.magenta(spacing + title_ticket) + : spacing + title_ticket; } - if (commit_state.breaking_title && config.breaking_change.add_exclamation_to_title) { - commit_string += colorize ? color.red('!') : '!' + if ( + commit_state.breaking_title && + config.breaking_change.add_exclamation_to_title + ) { + commit_string += colorize ? color.red("!") : "!"; } - if (commit_state.scope || commit_state.type || (title_ticket && position_before_colon)) { - commit_string += ': ' + if ( + commit_state.scope || + commit_state.type || + (title_ticket && position_before_colon) + ) { + commit_string += ": "; } - const position_start = config.check_ticket.title_position === "start" - const position_end = config.check_ticket.title_position === "end" - if(title_ticket && config.check_ticket.add_to_title && position_start) { - commit_string += colorize ? color.magenta(title_ticket) + ' ' : title_ticket + ' ' + const position_start = config.check_ticket.title_position === "start"; + const position_end = config.check_ticket.title_position === "end"; + if (title_ticket && config.check_ticket.add_to_title && position_start) { + commit_string += colorize + ? color.magenta(title_ticket) + " " + : title_ticket + " "; } if (commit_state.title) { - commit_string += colorize ? color.reset(commit_state.title) : commit_state.title + commit_string += colorize + ? color.reset(commit_state.title) + : commit_state.title; } - if(title_ticket && config.check_ticket.add_to_title && position_end) { - commit_string += ' ' + (colorize ? color.magenta(title_ticket) : title_ticket) + if (title_ticket && config.check_ticket.add_to_title && position_end) { + commit_string += + " " + (colorize ? color.magenta(title_ticket) : title_ticket); } if (commit_state.body) { - const temp = commit_state.body.split('\\n') // literal \n, not new-line. - const res = temp.map(v => colorize ? color.reset(v.trim()) : v.trim()).join('\n') + const temp = commit_state.body.split("\\n"); // literal \n, not new-line. + const res = temp + .map((v) => (colorize ? color.reset(v.trim()) : v.trim())) + .join("\n"); commit_string += colorize ? `\n\n${res}` : `\n\n${res}`; } if (commit_state.breaking_title) { - const title = colorize ? color.red(`BREAKING CHANGE: ${commit_state.breaking_title}`) : `BREAKING CHANGE: ${commit_state.breaking_title}`; - commit_string += `\n\n${title}` + const title = colorize + ? color.red(`BREAKING CHANGE: ${commit_state.breaking_title}`) + : `BREAKING CHANGE: ${commit_state.breaking_title}`; + commit_string += `\n\n${title}`; } if (commit_state.breaking_body) { - const body = colorize ? color.red(commit_state.breaking_body) : commit_state.breaking_body; - commit_string += `\n\n${body}` + const body = colorize + ? color.red(commit_state.breaking_body) + : commit_state.breaking_body; + commit_string += `\n\n${body}`; } if (commit_state.deprecates_title) { - const title = colorize ? color.yellow(`DEPRECATED: ${commit_state.deprecates_title}`) : `DEPRECATED: ${commit_state.deprecates_title}`; - commit_string += `\n\n${title}` + const title = colorize + ? color.yellow(`DEPRECATED: ${commit_state.deprecates_title}`) + : `DEPRECATED: ${commit_state.deprecates_title}`; + commit_string += `\n\n${title}`; } if (commit_state.deprecates_body) { - const body = colorize ? color.yellow(commit_state.deprecates_body) : commit_state.deprecates_body; - commit_string += `\n\n${body}` + const body = colorize + ? color.yellow(commit_state.deprecates_body) + : commit_state.deprecates_body; + commit_string += `\n\n${body}`; } if (commit_state.custom_footer) { - const temp = commit_state.custom_footer.split('\\n') - const res = temp.map(v => colorize ? color.reset(v.trim()) : v.trim()).join('\n') + const temp = commit_state.custom_footer.split("\\n"); + const res = temp + .map((v) => (colorize ? color.reset(v.trim()) : v.trim())) + .join("\n"); commit_string += colorize ? `\n\n${res}` : `\n\n${res}`; } if (commit_state.closes && commit_state.ticket) { - commit_string += colorize ? `\n\n${color.reset(commit_state.closes)} ${color.magenta(commit_state.ticket)}` : `\n\n${commit_state.closes} ${commit_state.ticket}`; + commit_string += colorize + ? `\n\n${color.reset(commit_state.closes)} ${color.magenta(commit_state.ticket)}` + : `\n\n${commit_state.closes} ${commit_state.ticket}`; } if (include_trailer && commit_state.trailer) { - commit_string += colorize ? `\n\n${color.dim(commit_state.trailer)}` : `\n\n${commit_state.trailer}` + commit_string += colorize + ? `\n\n${color.dim(commit_state.trailer)}` + : `\n\n${commit_state.trailer}`; } if (escape_quotes) { - commit_string = commit_string.replaceAll('"', '\\"') + commit_string = commit_string.replaceAll('"', '\\"'); } return commit_string; diff --git a/src/init.ts b/src/init.ts index 1489349..f605f43 100644 --- a/src/init.ts +++ b/src/init.ts @@ -1,20 +1,24 @@ #! /usr/bin/env node import { Config } from "./zod-state"; -import color from 'picocolors'; -import fs from 'fs' -import * as p from '@clack/prompts'; +import color from "picocolors"; +import fs from "fs"; +import * as p from "@clack/prompts"; import { CONFIG_FILE_NAME, get_git_root, load_setup } from "./utils"; try { console.clear(); - p.intro(`${color.bgCyan(color.black(' better-commits-init '))}`) + p.intro(`${color.bgCyan(color.black(" better-commits-init "))}`); const root = get_git_root(); - const root_path = `${root}/${CONFIG_FILE_NAME}` - const default_config = Config.parse({}) + const root_path = `${root}/${CONFIG_FILE_NAME}`; + const default_config = Config.parse({}); fs.writeFileSync(root_path, JSON.stringify(default_config, null, 4)); - p.log.success(`${color.green('Successfully created .better-commits.json')}`) - p.outro(`Run ${color.bgBlack(color.white('better-commits'))} to start the CLI`) + p.log.success(`${color.green("Successfully created .better-commits.json")}`); + p.outro( + `Run ${color.bgBlack(color.white("better-commits"))} to start the CLI`, + ); } catch (err: any) { - p.log.error(`${color.red('Could not determine git root folder. better-commits-init must be used in a git repository')}`) + p.log.error( + `${color.red("Could not determine git root folder. better-commits-init must be used in a git repository")}`, + ); } diff --git a/src/utils.ts b/src/utils.ts index 6bd12a3..8c31d49 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -10,7 +10,7 @@ import { Config } from "./zod-state"; export const CONFIG_FILE_NAME = ".better-commits.json"; export const SPACE_TO_SELECT = `${color.dim("( to select)")}`; export const A_FOR_ALL = `${color.dim( - "( to select, to select all)" + "( to select, to select all)", )}`; export const OPTIONAL_PROMPT = `${color.dim("(optional)")}`; export const CACHE_PROMPT = `${color.dim("(value will be saved)")}`; @@ -27,56 +27,68 @@ export const REGEX_SLASH_NUM = new RegExp(/\/(\d+)/); export const REGEX_START_NUM = new RegExp(/^(\d+)/); export const DEFAULT_TYPE_OPTIONS = [ - { value: "feat", label: "feat", hint: "A new feature", emoji: "โœจ", trailer: "Changelog: feature"}, - { value: "fix", label: "fix", hint: "A bug fix", emoji: "๐Ÿ›", trailer: "Changelog: fix"}, + { + value: "feat", + label: "feat", + hint: "A new feature", + emoji: "โœจ", + trailer: "Changelog: feature", + }, + { + value: "fix", + label: "fix", + hint: "A bug fix", + emoji: "๐Ÿ›", + trailer: "Changelog: fix", + }, { value: "docs", label: "docs", hint: "Documentation only changes", emoji: "๐Ÿ“š", - trailer: "Changelog: documentation" + trailer: "Changelog: documentation", }, { value: "refactor", label: "refactor", hint: "A code change that neither fixes a bug nor adds a feature", emoji: "๐Ÿ”จ", - trailer: "Changelog: refactor" + trailer: "Changelog: refactor", }, { value: "perf", label: "perf", hint: "A code change that improves performance", emoji: "๐Ÿš€", - trailer: "Changelog: performance" + trailer: "Changelog: performance", }, { value: "test", label: "test", hint: "Adding missing tests or correcting existing tests", emoji: "๐Ÿšจ", - trailer: "Changelog: test" + trailer: "Changelog: test", }, { value: "build", label: "build", hint: "Changes that affect the build system or external dependencies", emoji: "๐Ÿšง", - trailer: "Changelog: build" + trailer: "Changelog: build", }, { value: "ci", label: "ci", hint: "Changes to our CI configuration files and scripts", emoji: "๐Ÿค–", - trailer: "Changelog: ci" + trailer: "Changelog: ci", }, { value: "chore", label: "chore", hint: "Other changes that do not modify src or test files", emoji: "๐Ÿงน", - trailer: "Changelog: chore" + trailer: "Changelog: chore", }, { value: "", label: "none" }, ]; @@ -115,15 +127,27 @@ export const Z_FOOTER_OPTIONS = z.enum([ "deprecated", "custom", ]); -export const Z_BRANCH_FIELDS = z.enum(["user", "version", "type", "ticket", "description"]); +export const Z_BRANCH_FIELDS = z.enum([ + "user", + "version", + "type", + "ticket", + "description", +]); export const Z_BRANCH_CONFIG_FIELDS = z.enum([ "branch_user", "branch_version", "branch_type", "branch_ticket", - "branch_description" + "branch_description", ]); -export const BRANCH_ORDER_DEFAULTS: z.infer[] = ["user", "version", "type", "ticket", "description"] +export const BRANCH_ORDER_DEFAULTS: z.infer[] = [ + "user", + "version", + "type", + "ticket", + "description", +]; export const Z_BRANCH_ACTIONS = z.enum(["branch", "worktree"]); export const FOOTER_OPTION_VALUES: z.infer[] = [ "closes", @@ -143,12 +167,12 @@ export const BRANCH_ACTION_OPTIONS: { /* LOAD */ export function load_setup( - cli_name = " better-commits " + cli_name = " better-commits ", ): z.infer { console.clear(); p.intro(`${color.bgCyan(color.black(cli_name))}`); - let global_config = null + let global_config = null; const home_path = get_default_config_path(); if (fs.existsSync(home_path)) { p.log.step("Found global config"); @@ -160,18 +184,22 @@ export function load_setup( if (fs.existsSync(root_path)) { p.log.step("Found repository config"); const repo_config = read_config_from_path(root_path); - return global_config ? { - ...repo_config, - overrides: global_config.overrides.shell ? global_config.overrides : repo_config.overrides, - confirm_with_editor: global_config.confirm_with_editor - } : repo_config + return global_config + ? { + ...repo_config, + overrides: global_config.overrides.shell + ? global_config.overrides + : repo_config.overrides, + confirm_with_editor: global_config.confirm_with_editor, + } + : repo_config; } - if (global_config) return global_config + if (global_config) return global_config; const default_config = Config.parse({}); p.log.step( - "Config not found. Generating default .better-commit.json at $HOME" + "Config not found. Generating default .better-commit.json at $HOME", ); fs.writeFileSync(home_path, JSON.stringify(default_config, null, 4)); return default_config; @@ -190,7 +218,7 @@ function read_config_from_path(config_path: string) { } function validate_config( - config: z.infer + config: z.infer, ): z.infer { try { return Config.parse(config); @@ -234,7 +262,7 @@ export function get_git_root(): string { path = execSync("git rev-parse --show-toplevel").toString().trim(); } catch (err) { p.log.warn( - "Could not find git root. If in a --bare repository, ignore this warning." + "Could not find git root. If in a --bare repository, ignore this warning.", ); } return path; diff --git a/src/zod-state.ts b/src/zod-state.ts index cb2f4c4..a150f64 100644 --- a/src/zod-state.ts +++ b/src/zod-state.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { - BRANCH_ORDER_DEFAULTS, + BRANCH_ORDER_DEFAULTS, CUSTOM_SCOPE_KEY, DEFAULT_SCOPE_OPTIONS, DEFAULT_TYPE_OPTIONS, @@ -28,7 +28,7 @@ export const Config = z hint: z.string().optional(), emoji: z.string().emoji().optional(), trailer: z.string().optional(), - }) + }), ) .default(DEFAULT_TYPE_OPTIONS), }) @@ -47,7 +47,7 @@ export const Config = z (val) => val.options.map((v) => v.value).includes(val.initial_value), (val) => ({ message: `Type: initial_value "${val.initial_value}" must exist in options`, - }) + }), ), commit_scope: z .object({ @@ -60,7 +60,7 @@ export const Config = z value: z.string(), label: z.string().optional(), hint: z.string().optional(), - }) + }), ) .default(DEFAULT_SCOPE_OPTIONS), }) @@ -89,7 +89,7 @@ export const Config = z }, (val) => ({ message: `Scope: initial_value "${val.initial_value}" must exist in options`, - }) + }), ), check_ticket: z .object({ @@ -97,9 +97,11 @@ export const Config = z confirm_ticket: z.boolean().default(true), add_to_title: z.boolean().default(true), append_hashtag: z.boolean().default(false), - prepend_hashtag: z.enum(['Never', 'Always', 'Prompt']).default("Never"), + prepend_hashtag: z.enum(["Never", "Always", "Prompt"]).default("Never"), surround: z.enum(["", "()", "[]", "{}"]).default(""), - title_position: z.enum(["start", "end", "before-colon", "beginning"]).default("start"), + title_position: z + .enum(["start", "end", "before-colon", "beginning"]) + .default("start"), }) .default({}), commit_title: z @@ -186,7 +188,7 @@ export const CommitState = z deprecates_title: z.string().default(""), deprecates_body: z.string().default(""), custom_footer: z.string().default(""), - trailer: z.string().default("") + trailer: z.string().default(""), }) .default({}); @@ -196,6 +198,6 @@ export const BranchState = z type: z.string().default(""), ticket: z.string().default(""), description: z.string().default(""), - version: z.string().default("") + version: z.string().default(""), }) .default({});