├── .dockerignore
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── PULL_REQUEST_TEMPLATE.md
├── dependabot.yaml
└── workflows
│ ├── bump.yaml
│ ├── ci.yaml
│ ├── lint.yaml
│ └── release.yaml
├── .gitignore
├── .pre-commit-hooks.yaml
├── .vscode
├── extensions.json
└── launch.json
├── Cargo.lock
├── Cargo.toml
├── Dockerfile
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── cli
├── .editorconfig
├── Cargo.toml
├── e2e
│ ├── cli.bats
│ └── default.bats
├── examples
│ └── .commitlint.yaml
└── src
│ ├── args.rs
│ ├── config.rs
│ ├── git.rs
│ ├── lib.rs
│ ├── main.rs
│ ├── message.rs
│ ├── result.rs
│ ├── rule.rs
│ └── rule
│ ├── body_empty.rs
│ ├── body_max_length.rs
│ ├── description_empty.rs
│ ├── description_format.rs
│ ├── description_max_length.rs
│ ├── footers_empty.rs
│ ├── scope.rs
│ ├── scope_empty.rs
│ ├── scope_format.rs
│ ├── scope_max_length.rs
│ ├── subject_empty.rs
│ ├── type.rs
│ ├── type_empty.rs
│ ├── type_format.rs
│ └── type_max_length.rs
├── schema
├── Cargo.toml
└── src
│ └── main.rs
└── web
├── .gitignore
├── .vscode
├── extensions.json
└── launch.json
├── astro.config.mjs
├── package-lock.json
├── package.json
├── public
└── favicon.svg
├── src
├── assets
│ └── checker.png
├── content
│ ├── config.ts
│ └── docs
│ │ ├── config
│ │ ├── IDE.md
│ │ ├── configuration.md
│ │ └── default.md
│ │ ├── index.mdx
│ │ ├── license.md
│ │ ├── rules
│ │ ├── body-empty.md
│ │ ├── body-max-length.md
│ │ ├── description-empty.md
│ │ ├── description-format.md
│ │ ├── description-max-length.md
│ │ ├── footers-empty.md
│ │ ├── scope-empty.md
│ │ ├── scope-format.md
│ │ ├── scope-max-length.md
│ │ ├── scope.md
│ │ ├── subject-empty.md
│ │ ├── type-empty.md
│ │ ├── type-format.md
│ │ ├── type-max-length.md
│ │ └── type.md
│ │ └── setup
│ │ ├── debug.md
│ │ ├── install.md
│ │ └── motivation.md
└── env.d.ts
└── tsconfig.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Allowlist not to add unnecessary files to the repository
2 |
3 | *
4 |
5 | !/Cargo.lock
6 | !/Cargo.toml
7 | !/cli
8 | !/schema
9 | !/LICENSE-APACHE
10 | !/LICENSE-MIT
11 | !/README.md
12 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @KeisukeYamashita
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 |
12 | A clear and concise description of what the bug is.
13 |
14 | ## Config
15 |
16 | If you have any config file, please paste it below:
17 |
18 | ## Error message
19 |
20 | Please paste the error message below:
21 |
22 |
23 | ## Expected behavior
24 |
25 | A clear and concise description of what you expected to happen.
26 |
27 | ## Version
28 |
29 | ```console
30 | # Run the following command
31 | $ commitlint --version
32 | PASTE_HERE
33 | ```
34 |
35 | ## Additional context
36 | Add any other context about the problem here.
37 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the feature**
11 |
12 | A clear and concise description the feature request.
13 |
14 | ## How to run
15 |
16 | If you plan to add a new command, flag or option, please describe how you would like to run it.
17 |
18 | ```console
19 |
20 | ```
21 |
22 | ## Expected behavior
23 |
24 | A clear and concise description of what you expected to happen.
25 |
26 | ## Additional context
27 |
28 | Add any other context about the problem here.
29 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | # Why
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.github/dependabot.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | # Maintain dependencies for Rust (Cargo)
4 | - package-ecosystem: "cargo"
5 | commit-message:
6 | include: scope
7 | prefix: bump
8 | directory: "/"
9 | schedule:
10 | interval: "monthly"
11 | open-pull-requests-limit: 3
12 |
13 | # Maintain dependencies for Docker
14 | - package-ecosystem: "docker"
15 | commit-message:
16 | include: scope
17 | prefix: bump
18 | directory: "/"
19 | schedule:
20 | interval: "monthly"
21 | open-pull-requests-limit: 3
22 |
23 | # Maintain dependencies for GitHub Action
24 | - package-ecosystem: "github-actions"
25 | directory: "/"
26 | schedule:
27 | interval: "monthly"
28 | open-pull-requests-limit: 3
29 |
30 | # Maintain dependencies for npm
31 | - package-ecosystem: "npm"
32 | commit-message:
33 | include: scope
34 | prefix: bump
35 | directory: "/web"
36 | schedule:
37 | interval: "monthly"
38 | open-pull-requests-limit: 3
39 | groups:
40 | npm-development:
41 | dependency-type: development
42 | update-types:
43 | - minor
44 | - patch
45 | npm-production:
46 | dependency-type: production
47 | update-types:
48 | - patch
49 |
--------------------------------------------------------------------------------
/.github/workflows/bump.yaml:
--------------------------------------------------------------------------------
1 | name: Bump
2 | on:
3 | release:
4 | types:
5 | - published
6 |
7 | permissions:
8 | contents: write # To checkout and create PRs
9 | pull-requests: write # To comment to PRs
10 |
11 | jobs:
12 | bump:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 |
17 | - id: bump
18 | uses: tj-actions/cargo-bump@v3
19 |
20 | - uses: peter-evans/create-pull-request@v7
21 | with:
22 | author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
23 | branch: "bump-${{ steps.bump.outputs.new_version }}"
24 | commit-message: "bump(cli): bump version to ${{ steps.bump.outputs.new_version }}"
25 | title: "bump(cli): bump version to ${{ steps.bump.outputs.new_version }}"
26 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | - pull_request
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 | - uses: actions-rs/toolchain@v1
11 | with:
12 | toolchain: nightly
13 | override: true
14 | components: rustfmt, clippy
15 |
16 | - uses: actions-rs/cargo@v1
17 | with:
18 | command: build
19 |
20 | e2e:
21 | runs-on: ubuntu-latest
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | tag: [cli, default]
26 | steps:
27 | - uses: actions/checkout@v4
28 | - uses: actions/cache@v4
29 | with:
30 | path: |
31 | ~/.cargo/bin/
32 | ~/.cargo/registry/index/
33 | ~/.cargo/registry/cache/
34 | ~/.cargo/git/db/
35 | target/
36 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
37 | restore-keys: ${{ runner.os }}-cargo-
38 | - uses: actions-rs/cargo@v1
39 | with:
40 | command: build
41 | args: --release
42 | - run: |
43 | sudo chmod +x target/release/commitlint
44 | sudo mv target/release/commitlint /usr/local/bin/commitlint
45 | - uses: mig4/setup-bats@v1
46 | with:
47 | bats-version: 1.9.0
48 | - run: bats cli/e2e --filter-tags ${{ matrix.tag }}
49 |
50 | schema:
51 | runs-on: ubuntu-latest
52 | steps:
53 | - uses: actions/checkout@v4
54 | - uses: actions-rs/toolchain@v1
55 | with:
56 | profile: minimal
57 | toolchain: stable
58 | override: true
59 | - run: cargo run --package schema -- --path schema.json
60 |
61 | test:
62 | runs-on: ubuntu-latest
63 | steps:
64 | - uses: actions/checkout@v4
65 | - uses: actions-rs/toolchain@v1
66 | with:
67 | toolchain: nightly
68 | override: true
69 | components: rustfmt, clippy
70 | - uses: actions/cache@v4
71 | with:
72 | path: |
73 | ~/.cargo/bin/
74 | ~/.cargo/registry/index/
75 | ~/.cargo/registry/cache/
76 | ~/.cargo/git/db/
77 | target/
78 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
79 | restore-keys: ${{ runner.os }}-cargo-
80 |
81 | - uses: actions-rs/cargo@v1
82 | with:
83 | command: test
84 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yaml:
--------------------------------------------------------------------------------
1 | name: Lint
2 | on:
3 | pull_request:
4 | types:
5 | - opened
6 | - edited
7 | - synchronize
8 |
9 | permissions:
10 | contents: read # To checkout
11 | pull-requests: write # To comment to PRs
12 |
13 | jobs:
14 | actionlint:
15 | name: actionlint
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v4
19 |
20 | - uses: reviewdog/action-actionlint@v1
21 | with:
22 | reporter: github-pr-review
23 |
24 | alex:
25 | runs-on: ubuntu-latest
26 | steps:
27 | - uses: actions/checkout@v4
28 |
29 | - uses: reviewdog/action-alex@v1
30 | with:
31 | reporter: github-pr-review
32 |
33 | assign-author:
34 | runs-on: ubuntu-latest
35 | steps:
36 | - uses: toshimaru/auto-author-assign@v2.1.1
37 |
38 | clippy:
39 | runs-on: ubuntu-latest
40 | steps:
41 | - uses: actions/checkout@v4
42 | - uses: actions-rs/toolchain@v1
43 | with:
44 | toolchain: nightly
45 | override: true
46 | components: rustfmt, clippy
47 |
48 | - uses: actions-rs/cargo@v1
49 | with:
50 | command: clippy
51 |
52 | - uses: sksat/action-clippy@v1.1.0
53 | with:
54 | reporter: github-pr-review
55 |
56 | markdown:
57 | name: markdown
58 | runs-on: ubuntu-latest
59 | steps:
60 | - uses: actions/checkout@v4
61 | - uses: reviewdog/action-markdownlint@v0
62 | with:
63 | level: warning
64 | markdownlint_flags: website/docs/**/*.md
65 | reporter: github-pr-review
66 |
67 | misspell:
68 | name: misspell
69 | runs-on: ubuntu-latest
70 | steps:
71 | - uses: actions/checkout@v4
72 |
73 | - uses: reviewdog/action-misspell@v1
74 | with:
75 | level: warning
76 | reporter: github-pr-review
77 |
78 | title:
79 | runs-on: ubuntu-latest
80 | steps:
81 | - uses: amannn/action-semantic-pull-request@v5
82 | env:
83 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
84 | with:
85 | disallowScopes: |
86 | release
87 | [A-Z]+
88 | requireScope: true
89 | subjectPattern: ^(?![A-Z]).+$
90 | scopes: |
91 | .github
92 | cli
93 | deps
94 | other
95 | web
96 | types: |
97 | bump
98 | chore
99 | doc
100 | feat
101 | fix
102 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | tags:
5 | - "v[0-9]+.[0-9]+.[0-9]+*"
6 |
7 | concurrency:
8 | group: ${{ github.workflow }}
9 | cancel-in-progress: false
10 |
11 | permissions:
12 | contents: write # To write to release
13 | id-token: write # To deploy to GitHub Pages
14 | pages: write # To deploy to GitHub Pages
15 |
16 | jobs:
17 | build:
18 | runs-on: ${{ matrix.config.os }}
19 | strategy:
20 | fail-fast: true
21 | matrix:
22 | config:
23 | # See details fore GitHub Actions runners
24 | # https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners
25 | - os: ubuntu-20.04
26 | rust_target: x86_64-unknown-linux-gnu
27 | ext: ""
28 | args: ""
29 | - os: macos-13 # (Intel x86)
30 | rust_target: x86_64-apple-darwin
31 | ext: ""
32 | args: ""
33 | - os: macos-latest # (Apple Silicon)
34 | rust_target: aarch64-apple-darwin
35 | ext: ""
36 | args: ""
37 | - os: windows-latest
38 | rust_target: x86_64-pc-windows-msvc
39 | ext: ".exe"
40 | args: ""
41 | - os: windows-latest
42 | rust_target: aarch64-pc-windows-msvc
43 | ext: ".exe"
44 | args: "--no-default-features --features native-tls-vendored"
45 | steps:
46 | - uses: actions/checkout@v4
47 |
48 | - uses: actions-rs/toolchain@v1
49 | with:
50 | profile: minimal
51 | toolchain: stable
52 | override: true
53 |
54 | - run: cargo build --package commitlint-rs --release
55 |
56 | - run: tar czvf commitlint-${{ github.ref_name }}-${{ matrix.config.rust_target }}.tar.gz -C target/release commitlint${{ matrix.config.ext }}
57 |
58 | - uses: actions/upload-artifact@v4
59 | with:
60 | name: commitlint-${{ matrix.config.rust_target }}
61 | path: commitlint-${{ github.ref_name }}-${{ matrix.config.rust_target }}.tar.gz
62 | if-no-files-found: error
63 |
64 | crate:
65 | runs-on: ubuntu-latest
66 | environment: crate
67 | steps:
68 | - uses: actions/checkout@v4
69 | - uses: actions-rs/toolchain@v1
70 | with:
71 | toolchain: stable
72 | override: true
73 | - uses: actions/cache@v4
74 | with:
75 | path: |
76 | ~/.cargo/bin/
77 | ~/.cargo/registry/index/
78 | ~/.cargo/registry/cache/
79 | ~/.cargo/git/db/
80 | target/
81 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
82 | restore-keys: ${{ runner.os }}-cargo-
83 | - run: cargo publish --package commitlint-rs
84 | env:
85 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
86 |
87 | docker:
88 | runs-on: ubuntu-latest
89 | environment: docker
90 | steps:
91 | - uses: docker/setup-qemu-action@v3
92 |
93 | - uses: docker/setup-buildx-action@v3
94 |
95 | - uses: docker/login-action@v3
96 | with:
97 | username: ${{ secrets.DOCKERHUB_USERNAME }}
98 | password: ${{ secrets.DOCKERHUB_TOKEN }}
99 |
100 | - uses: docker/metadata-action@v5
101 | id: meta
102 | with:
103 | images: 1915keke/commitlint
104 | tags: |
105 | type=raw,value=latest
106 | type=semver,pattern={{version}}
107 | type=semver,pattern={{major}}.{{minor}}
108 | type=semver,pattern={{major}}
109 |
110 | - uses: docker/build-push-action@v6
111 | with:
112 | platforms: linux/amd64,linux/arm64
113 | push: true
114 | tags: ${{ steps.meta.outputs.tags }}
115 | cache-from: type=gha
116 | cache-to: type=gha,mode=max
117 |
118 | publish:
119 | runs-on: ubuntu-latest
120 | needs:
121 | - build
122 | - crate
123 | - docker
124 | - schema
125 | - web
126 | steps:
127 | - uses: actions/download-artifact@v4
128 | with:
129 | path: commitlint
130 | pattern: commitlint-*
131 | merge-multiple: true
132 | - uses: ncipollo/release-action@v1
133 | with:
134 | artifacts: commitlint/commitlint-*.tar.gz,schema.json
135 | generateReleaseNotes: true
136 |
137 | schema:
138 | runs-on: ubuntu-latest
139 | steps:
140 | - uses: actions/checkout@v4
141 | - uses: actions-rs/toolchain@v1
142 | with:
143 | profile: minimal
144 | toolchain: stable
145 | override: true
146 | - run: cargo run --package schema -- --path schema.json
147 | - uses: actions/upload-artifact@v4
148 | with:
149 | name: schema.json
150 | path: schema.json
151 | if-no-files-found: error
152 |
153 | web:
154 | runs-on: ubuntu-latest
155 | environment:
156 | name: web
157 | url: ${{ steps.deployment.outputs.page_url }}
158 | steps:
159 | - uses: actions/checkout@v4
160 | - uses: withastro/action@v2
161 | with:
162 | path: ./web
163 | - id: deployment
164 | uses: actions/deploy-pages@v4
165 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ### Generated by gibo (https://github.com/simonwhitaker/gibo)
2 | ### https://raw.github.com/github/gitignore/4488915eec0b3a45b5c63ead28f286819c0917de/Rust.gitignore
3 |
4 | # Generated by Cargo
5 | # will have compiled files and executables
6 | debug/
7 | target/
8 |
9 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
10 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
11 | Cargo.lock
12 |
13 | # These are backup files generated by rustfmt
14 | **/*.rs.bk
15 |
16 | # MSVC Windows builds of rustc generate these, which store debugging information
17 | *.pdb
18 |
19 | # Commitlint binary
20 | commitlint
21 |
22 | # Commitlint config file
23 | .commitlintrc
24 | .commitlintrc.*
25 |
26 | # JSON schema
27 | schema.json
28 |
--------------------------------------------------------------------------------
/.pre-commit-hooks.yaml:
--------------------------------------------------------------------------------
1 | - id: commitlint
2 | name: Assert Conventional Commit Messages
3 | description: 'Asserts that Conventional Commits have been used for all commit messages according to the rules for this repo.'
4 | entry: commitlint --edit
5 | language: rust
6 | stages: [prepare-commit-msg]
7 | pass_filenames: false
8 | require_serial: true
9 | verbose: true
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "DavidAnson.vscode-markdownlint",
4 | "esbenp.prettier-vscode",
5 | "rust-lang.rust-analyzer",
6 | "streetsidesoftware.code-spell-checker",
7 | "usernamehw.errorlens"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "lldb",
9 | "request": "launch",
10 | "name": "Debug executable 'commitlint'",
11 | "cargo": {
12 | "args": ["build", "--bin=commitlint", "--package=commitlint-rs"],
13 | "filter": {
14 | "name": "commitlint",
15 | "kind": "bin"
16 | }
17 | },
18 | "args": [],
19 | "cwd": "${workspaceFolder}"
20 | },
21 | {
22 | "type": "lldb",
23 | "request": "launch",
24 | "name": "Debug unit tests in executable 'commitlint'",
25 | "cargo": {
26 | "args": [
27 | "test",
28 | "--no-run",
29 | "--bin=commitlint",
30 | "--package=commitlint-rs"
31 | ],
32 | "filter": {
33 | "name": "commitlint",
34 | "kind": "bin"
35 | }
36 | },
37 | "args": [],
38 | "cwd": "${workspaceFolder}"
39 | }
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 4
4 |
5 | [[package]]
6 | name = "addr2line"
7 | version = "0.24.2"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
10 | dependencies = [
11 | "gimli",
12 | ]
13 |
14 | [[package]]
15 | name = "adler2"
16 | version = "2.0.0"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
19 |
20 | [[package]]
21 | name = "aho-corasick"
22 | version = "1.1.3"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
25 | dependencies = [
26 | "memchr",
27 | ]
28 |
29 | [[package]]
30 | name = "anstream"
31 | version = "0.6.18"
32 | source = "registry+https://github.com/rust-lang/crates.io-index"
33 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
34 | dependencies = [
35 | "anstyle",
36 | "anstyle-parse",
37 | "anstyle-query",
38 | "anstyle-wincon",
39 | "colorchoice",
40 | "is_terminal_polyfill",
41 | "utf8parse",
42 | ]
43 |
44 | [[package]]
45 | name = "anstyle"
46 | version = "1.0.10"
47 | source = "registry+https://github.com/rust-lang/crates.io-index"
48 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
49 |
50 | [[package]]
51 | name = "anstyle-parse"
52 | version = "0.2.6"
53 | source = "registry+https://github.com/rust-lang/crates.io-index"
54 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
55 | dependencies = [
56 | "utf8parse",
57 | ]
58 |
59 | [[package]]
60 | name = "anstyle-query"
61 | version = "1.1.2"
62 | source = "registry+https://github.com/rust-lang/crates.io-index"
63 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
64 | dependencies = [
65 | "windows-sys 0.59.0",
66 | ]
67 |
68 | [[package]]
69 | name = "anstyle-wincon"
70 | version = "3.0.6"
71 | source = "registry+https://github.com/rust-lang/crates.io-index"
72 | checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
73 | dependencies = [
74 | "anstyle",
75 | "windows-sys 0.59.0",
76 | ]
77 |
78 | [[package]]
79 | name = "autocfg"
80 | version = "1.4.0"
81 | source = "registry+https://github.com/rust-lang/crates.io-index"
82 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
83 |
84 | [[package]]
85 | name = "backtrace"
86 | version = "0.3.74"
87 | source = "registry+https://github.com/rust-lang/crates.io-index"
88 | checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
89 | dependencies = [
90 | "addr2line",
91 | "cfg-if",
92 | "libc",
93 | "miniz_oxide",
94 | "object",
95 | "rustc-demangle",
96 | "windows-targets",
97 | ]
98 |
99 | [[package]]
100 | name = "bitflags"
101 | version = "2.6.0"
102 | source = "registry+https://github.com/rust-lang/crates.io-index"
103 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
104 |
105 | [[package]]
106 | name = "bytes"
107 | version = "1.8.0"
108 | source = "registry+https://github.com/rust-lang/crates.io-index"
109 | checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
110 |
111 | [[package]]
112 | name = "cfg-if"
113 | version = "1.0.0"
114 | source = "registry+https://github.com/rust-lang/crates.io-index"
115 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
116 |
117 | [[package]]
118 | name = "clap"
119 | version = "4.5.21"
120 | source = "registry+https://github.com/rust-lang/crates.io-index"
121 | checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
122 | dependencies = [
123 | "clap_builder",
124 | "clap_derive",
125 | ]
126 |
127 | [[package]]
128 | name = "clap_builder"
129 | version = "4.5.21"
130 | source = "registry+https://github.com/rust-lang/crates.io-index"
131 | checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
132 | dependencies = [
133 | "anstream",
134 | "anstyle",
135 | "clap_lex",
136 | "strsim",
137 | ]
138 |
139 | [[package]]
140 | name = "clap_derive"
141 | version = "4.5.18"
142 | source = "registry+https://github.com/rust-lang/crates.io-index"
143 | checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
144 | dependencies = [
145 | "heck",
146 | "proc-macro2",
147 | "quote",
148 | "syn",
149 | ]
150 |
151 | [[package]]
152 | name = "clap_lex"
153 | version = "0.7.2"
154 | source = "registry+https://github.com/rust-lang/crates.io-index"
155 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
156 |
157 | [[package]]
158 | name = "colorchoice"
159 | version = "1.0.3"
160 | source = "registry+https://github.com/rust-lang/crates.io-index"
161 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
162 |
163 | [[package]]
164 | name = "commitlint-rs"
165 | version = "0.2.2"
166 | dependencies = [
167 | "clap",
168 | "futures",
169 | "regex",
170 | "schemars",
171 | "serde",
172 | "serde_json",
173 | "serde_yaml",
174 | "tokio",
175 | ]
176 |
177 | [[package]]
178 | name = "dyn-clone"
179 | version = "1.0.17"
180 | source = "registry+https://github.com/rust-lang/crates.io-index"
181 | checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125"
182 |
183 | [[package]]
184 | name = "equivalent"
185 | version = "1.0.1"
186 | source = "registry+https://github.com/rust-lang/crates.io-index"
187 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
188 |
189 | [[package]]
190 | name = "futures"
191 | version = "0.3.31"
192 | source = "registry+https://github.com/rust-lang/crates.io-index"
193 | checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
194 | dependencies = [
195 | "futures-channel",
196 | "futures-core",
197 | "futures-executor",
198 | "futures-io",
199 | "futures-sink",
200 | "futures-task",
201 | "futures-util",
202 | ]
203 |
204 | [[package]]
205 | name = "futures-channel"
206 | version = "0.3.31"
207 | source = "registry+https://github.com/rust-lang/crates.io-index"
208 | checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
209 | dependencies = [
210 | "futures-core",
211 | "futures-sink",
212 | ]
213 |
214 | [[package]]
215 | name = "futures-core"
216 | version = "0.3.31"
217 | source = "registry+https://github.com/rust-lang/crates.io-index"
218 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
219 |
220 | [[package]]
221 | name = "futures-executor"
222 | version = "0.3.31"
223 | source = "registry+https://github.com/rust-lang/crates.io-index"
224 | checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
225 | dependencies = [
226 | "futures-core",
227 | "futures-task",
228 | "futures-util",
229 | ]
230 |
231 | [[package]]
232 | name = "futures-io"
233 | version = "0.3.31"
234 | source = "registry+https://github.com/rust-lang/crates.io-index"
235 | checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
236 |
237 | [[package]]
238 | name = "futures-macro"
239 | version = "0.3.31"
240 | source = "registry+https://github.com/rust-lang/crates.io-index"
241 | checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
242 | dependencies = [
243 | "proc-macro2",
244 | "quote",
245 | "syn",
246 | ]
247 |
248 | [[package]]
249 | name = "futures-sink"
250 | version = "0.3.31"
251 | source = "registry+https://github.com/rust-lang/crates.io-index"
252 | checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
253 |
254 | [[package]]
255 | name = "futures-task"
256 | version = "0.3.31"
257 | source = "registry+https://github.com/rust-lang/crates.io-index"
258 | checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
259 |
260 | [[package]]
261 | name = "futures-util"
262 | version = "0.3.31"
263 | source = "registry+https://github.com/rust-lang/crates.io-index"
264 | checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
265 | dependencies = [
266 | "futures-channel",
267 | "futures-core",
268 | "futures-io",
269 | "futures-macro",
270 | "futures-sink",
271 | "futures-task",
272 | "memchr",
273 | "pin-project-lite",
274 | "pin-utils",
275 | "slab",
276 | ]
277 |
278 | [[package]]
279 | name = "gimli"
280 | version = "0.31.1"
281 | source = "registry+https://github.com/rust-lang/crates.io-index"
282 | checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
283 |
284 | [[package]]
285 | name = "hashbrown"
286 | version = "0.15.1"
287 | source = "registry+https://github.com/rust-lang/crates.io-index"
288 | checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3"
289 |
290 | [[package]]
291 | name = "heck"
292 | version = "0.5.0"
293 | source = "registry+https://github.com/rust-lang/crates.io-index"
294 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
295 |
296 | [[package]]
297 | name = "hermit-abi"
298 | version = "0.3.9"
299 | source = "registry+https://github.com/rust-lang/crates.io-index"
300 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
301 |
302 | [[package]]
303 | name = "indexmap"
304 | version = "2.6.0"
305 | source = "registry+https://github.com/rust-lang/crates.io-index"
306 | checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
307 | dependencies = [
308 | "equivalent",
309 | "hashbrown",
310 | ]
311 |
312 | [[package]]
313 | name = "is_terminal_polyfill"
314 | version = "1.70.1"
315 | source = "registry+https://github.com/rust-lang/crates.io-index"
316 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
317 |
318 | [[package]]
319 | name = "itoa"
320 | version = "1.0.11"
321 | source = "registry+https://github.com/rust-lang/crates.io-index"
322 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
323 |
324 | [[package]]
325 | name = "libc"
326 | version = "0.2.171"
327 | source = "registry+https://github.com/rust-lang/crates.io-index"
328 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
329 |
330 | [[package]]
331 | name = "lock_api"
332 | version = "0.4.12"
333 | source = "registry+https://github.com/rust-lang/crates.io-index"
334 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
335 | dependencies = [
336 | "autocfg",
337 | "scopeguard",
338 | ]
339 |
340 | [[package]]
341 | name = "memchr"
342 | version = "2.7.4"
343 | source = "registry+https://github.com/rust-lang/crates.io-index"
344 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
345 |
346 | [[package]]
347 | name = "miniz_oxide"
348 | version = "0.8.0"
349 | source = "registry+https://github.com/rust-lang/crates.io-index"
350 | checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
351 | dependencies = [
352 | "adler2",
353 | ]
354 |
355 | [[package]]
356 | name = "mio"
357 | version = "1.0.2"
358 | source = "registry+https://github.com/rust-lang/crates.io-index"
359 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec"
360 | dependencies = [
361 | "hermit-abi",
362 | "libc",
363 | "wasi",
364 | "windows-sys 0.52.0",
365 | ]
366 |
367 | [[package]]
368 | name = "object"
369 | version = "0.36.5"
370 | source = "registry+https://github.com/rust-lang/crates.io-index"
371 | checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
372 | dependencies = [
373 | "memchr",
374 | ]
375 |
376 | [[package]]
377 | name = "parking_lot"
378 | version = "0.12.3"
379 | source = "registry+https://github.com/rust-lang/crates.io-index"
380 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
381 | dependencies = [
382 | "lock_api",
383 | "parking_lot_core",
384 | ]
385 |
386 | [[package]]
387 | name = "parking_lot_core"
388 | version = "0.9.10"
389 | source = "registry+https://github.com/rust-lang/crates.io-index"
390 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
391 | dependencies = [
392 | "cfg-if",
393 | "libc",
394 | "redox_syscall",
395 | "smallvec",
396 | "windows-targets",
397 | ]
398 |
399 | [[package]]
400 | name = "pin-project-lite"
401 | version = "0.2.15"
402 | source = "registry+https://github.com/rust-lang/crates.io-index"
403 | checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
404 |
405 | [[package]]
406 | name = "pin-utils"
407 | version = "0.1.0"
408 | source = "registry+https://github.com/rust-lang/crates.io-index"
409 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
410 |
411 | [[package]]
412 | name = "proc-macro2"
413 | version = "1.0.89"
414 | source = "registry+https://github.com/rust-lang/crates.io-index"
415 | checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
416 | dependencies = [
417 | "unicode-ident",
418 | ]
419 |
420 | [[package]]
421 | name = "quote"
422 | version = "1.0.37"
423 | source = "registry+https://github.com/rust-lang/crates.io-index"
424 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
425 | dependencies = [
426 | "proc-macro2",
427 | ]
428 |
429 | [[package]]
430 | name = "redox_syscall"
431 | version = "0.5.7"
432 | source = "registry+https://github.com/rust-lang/crates.io-index"
433 | checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
434 | dependencies = [
435 | "bitflags",
436 | ]
437 |
438 | [[package]]
439 | name = "regex"
440 | version = "1.11.1"
441 | source = "registry+https://github.com/rust-lang/crates.io-index"
442 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
443 | dependencies = [
444 | "aho-corasick",
445 | "memchr",
446 | "regex-automata",
447 | "regex-syntax",
448 | ]
449 |
450 | [[package]]
451 | name = "regex-automata"
452 | version = "0.4.8"
453 | source = "registry+https://github.com/rust-lang/crates.io-index"
454 | checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
455 | dependencies = [
456 | "aho-corasick",
457 | "memchr",
458 | "regex-syntax",
459 | ]
460 |
461 | [[package]]
462 | name = "regex-syntax"
463 | version = "0.8.5"
464 | source = "registry+https://github.com/rust-lang/crates.io-index"
465 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
466 |
467 | [[package]]
468 | name = "rustc-demangle"
469 | version = "0.1.24"
470 | source = "registry+https://github.com/rust-lang/crates.io-index"
471 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
472 |
473 | [[package]]
474 | name = "ryu"
475 | version = "1.0.18"
476 | source = "registry+https://github.com/rust-lang/crates.io-index"
477 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
478 |
479 | [[package]]
480 | name = "schema"
481 | version = "0.2.2"
482 | dependencies = [
483 | "clap",
484 | "commitlint-rs",
485 | "schemars",
486 | "serde",
487 | "serde_json",
488 | ]
489 |
490 | [[package]]
491 | name = "schemars"
492 | version = "0.8.21"
493 | source = "registry+https://github.com/rust-lang/crates.io-index"
494 | checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92"
495 | dependencies = [
496 | "dyn-clone",
497 | "schemars_derive",
498 | "serde",
499 | "serde_json",
500 | ]
501 |
502 | [[package]]
503 | name = "schemars_derive"
504 | version = "0.8.21"
505 | source = "registry+https://github.com/rust-lang/crates.io-index"
506 | checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e"
507 | dependencies = [
508 | "proc-macro2",
509 | "quote",
510 | "serde_derive_internals",
511 | "syn",
512 | ]
513 |
514 | [[package]]
515 | name = "scopeguard"
516 | version = "1.2.0"
517 | source = "registry+https://github.com/rust-lang/crates.io-index"
518 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
519 |
520 | [[package]]
521 | name = "serde"
522 | version = "1.0.215"
523 | source = "registry+https://github.com/rust-lang/crates.io-index"
524 | checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
525 | dependencies = [
526 | "serde_derive",
527 | ]
528 |
529 | [[package]]
530 | name = "serde_derive"
531 | version = "1.0.215"
532 | source = "registry+https://github.com/rust-lang/crates.io-index"
533 | checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
534 | dependencies = [
535 | "proc-macro2",
536 | "quote",
537 | "syn",
538 | ]
539 |
540 | [[package]]
541 | name = "serde_derive_internals"
542 | version = "0.29.1"
543 | source = "registry+https://github.com/rust-lang/crates.io-index"
544 | checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
545 | dependencies = [
546 | "proc-macro2",
547 | "quote",
548 | "syn",
549 | ]
550 |
551 | [[package]]
552 | name = "serde_json"
553 | version = "1.0.140"
554 | source = "registry+https://github.com/rust-lang/crates.io-index"
555 | checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
556 | dependencies = [
557 | "itoa",
558 | "memchr",
559 | "ryu",
560 | "serde",
561 | ]
562 |
563 | [[package]]
564 | name = "serde_yaml"
565 | version = "0.9.34+deprecated"
566 | source = "registry+https://github.com/rust-lang/crates.io-index"
567 | checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
568 | dependencies = [
569 | "indexmap",
570 | "itoa",
571 | "ryu",
572 | "serde",
573 | "unsafe-libyaml",
574 | ]
575 |
576 | [[package]]
577 | name = "signal-hook-registry"
578 | version = "1.4.2"
579 | source = "registry+https://github.com/rust-lang/crates.io-index"
580 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
581 | dependencies = [
582 | "libc",
583 | ]
584 |
585 | [[package]]
586 | name = "slab"
587 | version = "0.4.9"
588 | source = "registry+https://github.com/rust-lang/crates.io-index"
589 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
590 | dependencies = [
591 | "autocfg",
592 | ]
593 |
594 | [[package]]
595 | name = "smallvec"
596 | version = "1.13.2"
597 | source = "registry+https://github.com/rust-lang/crates.io-index"
598 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
599 |
600 | [[package]]
601 | name = "socket2"
602 | version = "0.5.7"
603 | source = "registry+https://github.com/rust-lang/crates.io-index"
604 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c"
605 | dependencies = [
606 | "libc",
607 | "windows-sys 0.52.0",
608 | ]
609 |
610 | [[package]]
611 | name = "strsim"
612 | version = "0.11.1"
613 | source = "registry+https://github.com/rust-lang/crates.io-index"
614 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
615 |
616 | [[package]]
617 | name = "syn"
618 | version = "2.0.87"
619 | source = "registry+https://github.com/rust-lang/crates.io-index"
620 | checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
621 | dependencies = [
622 | "proc-macro2",
623 | "quote",
624 | "unicode-ident",
625 | ]
626 |
627 | [[package]]
628 | name = "tokio"
629 | version = "1.43.1"
630 | source = "registry+https://github.com/rust-lang/crates.io-index"
631 | checksum = "492a604e2fd7f814268a378409e6c92b5525d747d10db9a229723f55a417958c"
632 | dependencies = [
633 | "backtrace",
634 | "bytes",
635 | "libc",
636 | "mio",
637 | "parking_lot",
638 | "pin-project-lite",
639 | "signal-hook-registry",
640 | "socket2",
641 | "tokio-macros",
642 | "windows-sys 0.52.0",
643 | ]
644 |
645 | [[package]]
646 | name = "tokio-macros"
647 | version = "2.5.0"
648 | source = "registry+https://github.com/rust-lang/crates.io-index"
649 | checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
650 | dependencies = [
651 | "proc-macro2",
652 | "quote",
653 | "syn",
654 | ]
655 |
656 | [[package]]
657 | name = "unicode-ident"
658 | version = "1.0.13"
659 | source = "registry+https://github.com/rust-lang/crates.io-index"
660 | checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
661 |
662 | [[package]]
663 | name = "unsafe-libyaml"
664 | version = "0.2.11"
665 | source = "registry+https://github.com/rust-lang/crates.io-index"
666 | checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
667 |
668 | [[package]]
669 | name = "utf8parse"
670 | version = "0.2.2"
671 | source = "registry+https://github.com/rust-lang/crates.io-index"
672 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
673 |
674 | [[package]]
675 | name = "wasi"
676 | version = "0.11.0+wasi-snapshot-preview1"
677 | source = "registry+https://github.com/rust-lang/crates.io-index"
678 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
679 |
680 | [[package]]
681 | name = "windows-sys"
682 | version = "0.52.0"
683 | source = "registry+https://github.com/rust-lang/crates.io-index"
684 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
685 | dependencies = [
686 | "windows-targets",
687 | ]
688 |
689 | [[package]]
690 | name = "windows-sys"
691 | version = "0.59.0"
692 | source = "registry+https://github.com/rust-lang/crates.io-index"
693 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
694 | dependencies = [
695 | "windows-targets",
696 | ]
697 |
698 | [[package]]
699 | name = "windows-targets"
700 | version = "0.52.6"
701 | source = "registry+https://github.com/rust-lang/crates.io-index"
702 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
703 | dependencies = [
704 | "windows_aarch64_gnullvm",
705 | "windows_aarch64_msvc",
706 | "windows_i686_gnu",
707 | "windows_i686_gnullvm",
708 | "windows_i686_msvc",
709 | "windows_x86_64_gnu",
710 | "windows_x86_64_gnullvm",
711 | "windows_x86_64_msvc",
712 | ]
713 |
714 | [[package]]
715 | name = "windows_aarch64_gnullvm"
716 | version = "0.52.6"
717 | source = "registry+https://github.com/rust-lang/crates.io-index"
718 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
719 |
720 | [[package]]
721 | name = "windows_aarch64_msvc"
722 | version = "0.52.6"
723 | source = "registry+https://github.com/rust-lang/crates.io-index"
724 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
725 |
726 | [[package]]
727 | name = "windows_i686_gnu"
728 | version = "0.52.6"
729 | source = "registry+https://github.com/rust-lang/crates.io-index"
730 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
731 |
732 | [[package]]
733 | name = "windows_i686_gnullvm"
734 | version = "0.52.6"
735 | source = "registry+https://github.com/rust-lang/crates.io-index"
736 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
737 |
738 | [[package]]
739 | name = "windows_i686_msvc"
740 | version = "0.52.6"
741 | source = "registry+https://github.com/rust-lang/crates.io-index"
742 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
743 |
744 | [[package]]
745 | name = "windows_x86_64_gnu"
746 | version = "0.52.6"
747 | source = "registry+https://github.com/rust-lang/crates.io-index"
748 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
749 |
750 | [[package]]
751 | name = "windows_x86_64_gnullvm"
752 | version = "0.52.6"
753 | source = "registry+https://github.com/rust-lang/crates.io-index"
754 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
755 |
756 | [[package]]
757 | name = "windows_x86_64_msvc"
758 | version = "0.52.6"
759 | source = "registry+https://github.com/rust-lang/crates.io-index"
760 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
761 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = ["cli", "schema"]
3 | resolver = "2"
4 |
5 | [workspace.package]
6 | version = "0.2.2"
7 | authors = ["KeisukeYamashita <19yamashita15@gmail.com>"]
8 | license = "MIT OR Apache-2.0"
9 | documentation = "https://keisukeyamashita.github.io/commitlint-rs"
10 | keywords = ["conventional-commits", "lint"]
11 | categories = ["command-line-utilities"]
12 | readme = "README.md"
13 | repository = "https://github.com/KeisukeYamashita/commitlint-rs"
14 | exclude = ["/web"]
15 | edition = "2021"
16 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 | FROM rust:1.83-alpine as builder
3 | WORKDIR /app
4 |
5 | RUN --mount=type=cache,target=/var/cache/apk,sharing=locked \
6 | apk update \
7 | && apk add --no-cache musl-dev
8 |
9 | COPY . .
10 | RUN cargo install --path ./cli && \
11 | commitlint --version
12 |
13 | FROM alpine
14 | LABEL maintainer="KeisukeYamashita <19yamashita15@gmail.com>"
15 |
16 | RUN --mount=type=cache,target=/var/cache/apk,sharing=locked \
17 | apk update \
18 | && apk add --no-cache musl-dev
19 |
20 | COPY --from=builder /usr/local/cargo/bin/commitlint /usr/local/bin/commitlint
21 |
22 | ENTRYPOINT [ "commitlint" ]
23 |
--------------------------------------------------------------------------------
/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2023 KeisukeYamashita
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Copyright (c) 2023- KeisukeYamashita
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Commitlint
4 |

5 |
6 |
🔦 Lint commit messages with conventional commit messages
7 |
8 | Documents are available [here](https://keisukeyamashita.github.io/commitlint-rs)
9 |
10 | [](https://crates.io/crates/commitlint-rs)
11 | [](https://crates.io/crates/commitlint-rs)
12 | [](https://github.com/KeisukeYamashita/commitlint-rs)
13 | [](https://hub.docker.com/repository/docker/1915keke/commitlint)
14 | [](https://github.com/KeisukeYamashita/commitlint-rs)
15 | [](https://github.com/KeisukeYamashita/commitlint-rs)
16 | [](https://app.fossa.com/projects/git%2Bgithub.com%2FKeisukeYamashita%2Fcommitlint-rs?ref=badge_shield)
17 |
18 |
19 |
20 | ## License
21 |
22 | Commitlint source code is licensed either [MIT](LICENSE-MIT) or [Apache-2.0](./LICENSE-APACHE).
23 |
24 |
Chinese-checkers icons created by Freepik - Flaticon
25 |
26 |
27 |
--------------------------------------------------------------------------------
/cli/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.yml]
4 | indent_size = 2
5 |
--------------------------------------------------------------------------------
/cli/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "commitlint-rs"
3 | description = "CLI tool to lint commits by Conventional Commits"
4 | documentation.workspace = true
5 | authors.workspace = true
6 | keywords.workspace = true
7 | categories.workspace = true
8 | version.workspace = true
9 | readme.workspace = true
10 | repository.workspace = true
11 | license.workspace = true
12 | edition.workspace = true
13 |
14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
15 |
16 | [[bin]]
17 | name = "commitlint"
18 | path = "src/main.rs"
19 |
20 | [dependencies]
21 | clap = { version = "4.5.4", features = ["derive", "env", "string"] }
22 | futures = "0.3.30"
23 | regex = "1.10.5"
24 | schemars = { version = "0.8.21", optional = true }
25 | serde = { version = "1.0.201", features = ["derive"] }
26 | serde_json = "1.0.140"
27 | serde_yaml = "0.9.34"
28 | tokio = { version = "1.43.1", features = ["full"] }
29 |
30 | [features]
31 | schemars = ["dep:schemars"]
32 | default = []
33 |
34 | [package.metadata.binstall]
35 | pkg-url = "{ repo }/releases/download/v{ version }/commitlint-v{ version }-{ target }{ archive-suffix }"
36 |
--------------------------------------------------------------------------------
/cli/e2e/cli.bats:
--------------------------------------------------------------------------------
1 | # bats test_tags=cli
2 | @test "not existing config file" {
3 | run bash -c 'echo "feat(cli): impl -a flag" | commitlint --config not-existing-config.js'
4 | [ "$status" -eq 1 ]
5 | }
6 |
--------------------------------------------------------------------------------
/cli/e2e/default.bats:
--------------------------------------------------------------------------------
1 | # bats test_tags=default
2 | @test "empty body" {
3 | run bash -c 'echo "feat(cli): impl -a flag" | commitlint'
4 | [ "$status" -eq 0 ]
5 | }
6 |
7 | # bats test_tags=default
8 | @test "body max length" {
9 | run bash -c "echo \"feat(cli): impl -a flag
10 |
11 | Hello, I'm longer than 72 charactures. I'm very long so that it is not suitable as it's hard to read many long commit messages.
12 | Make it smart. But we should not be opinionated so the default is ignored.\" | commitlint"
13 | [ "$status" -eq 0 ]
14 | }
15 |
16 | # bats test_tags=default
17 | @test "description empty" {
18 | run bash -c 'echo "feat(cli): " | commitlint'
19 | [ "$status" -eq 1 ]
20 | }
21 |
22 | # bats test_tags=default
23 | @test "description format" {
24 | run bash -c 'echo "feat(other): add script" | commitlint'
25 | [ "$status" -eq 0 ]
26 | }
27 |
28 | # bats test_tags=default
29 | @test "description max length" {
30 | run bash -c 'echo "feat(other): add script" | commitlint'
31 | [ "$status" -eq 0 ]
32 | }
33 |
34 | @test "empty" {
35 | run bash -c 'echo "" | commitlint'
36 | [ "$status" -eq 1 ]
37 | }
38 |
39 | # bats test_tags=default
40 | @test "scope" {
41 | run bash -c 'echo "feat(other): add script" | commitlint'
42 | [ "$status" -eq 0 ]
43 | }
44 |
45 | # bats test_tags=default
46 | @test "scope format" {
47 | run bash -c 'echo "feat(other): add script" | commitlint'
48 | [ "$status" -eq 0 ]
49 | }
50 |
51 | # bats test_tags=default
52 | @test "scope max length" {
53 | run bash -c 'echo "feat(other): add script" | commitlint'
54 | [ "$status" -eq 0 ]
55 | }
56 |
57 | # bats test_tags=default
58 | @test "subject empty" {
59 | run bash -c 'echo "feat(other): add script" | commitlint'
60 | [ "$status" -eq 0 ]
61 | }
62 |
63 | # bats test_tags=default
64 | @test "type" {
65 | run bash -c 'echo "feat(other): add script" | commitlint'
66 | [ "$status" -eq 0 ]
67 | }
68 |
69 | # bats test_tags=default
70 | @test "type empty" {
71 | run bash -c 'echo "(other): add script" | commitlint'
72 | [ "$status" -eq 1 ]
73 | }
74 |
75 | # bats test_tags=default
76 | @test "type format" {
77 | run bash -c 'echo "feat(other): add script" | commitlint'
78 | [ "$status" -eq 0 ]
79 | }
80 |
81 | # bats test_tags=default
82 | @test "type max length" {
83 | run bash -c 'echo "feat(other): add script" | commitlint'
84 | [ "$status" -eq 0 ]
85 | }
86 |
--------------------------------------------------------------------------------
/cli/examples/.commitlint.yaml:
--------------------------------------------------------------------------------
1 | rules:
2 | body-empty:
3 | level: warning
4 |
--------------------------------------------------------------------------------
/cli/src/args.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fmt::Error,
3 | io::{stdin, IsTerminal, Read},
4 | path::PathBuf,
5 | };
6 |
7 | use clap::Parser;
8 |
9 | use crate::git::{self, ReadCommitMessageOptions};
10 | use crate::message::Message;
11 |
12 | /// Cli represents the command line arguments.
13 | ///
14 | /// Note that the arguments are following the [conventional-changelog/commitlint](https://commitlint.js.org/#/reference-cli)
15 | /// command line interface to reduce halation and ease onboarding of existing users.
16 | #[derive(Parser, Debug)]
17 | #[command(author, about = "CLI to lint with conventional commits", long_about = None, version)]
18 | pub struct Args {
19 | /// Path to the config file
20 | #[arg(short = 'g', long)]
21 | pub config: Option,
22 |
23 | /// Directory to execute in
24 | #[arg(short = 'd', long, default_value = ".")]
25 | pub cwd: String,
26 |
27 | /// Read last commit from the specified file or fallbacks to ./.git/COMMIT_EDITMSG
28 | #[arg(short = 'e', long)]
29 | pub edit: Option,
30 |
31 | /// Lower end of the commit range to lint
32 | #[arg(short = 'f', long)]
33 | pub from: Option,
34 |
35 | /// Print resolved config
36 | #[arg(long = "print-config")]
37 | pub print_config: bool,
38 |
39 | /// Upper end of the commit range to lint
40 | #[arg(short = 't', long)]
41 | pub to: Option,
42 | }
43 |
44 | impl Args {
45 | /// Check wether the commit message is from stdin or not.
46 | ///
47 | /// Inspired by https://github.com/conventional-changelog/commitlint/blob/af2f3a82d38ea0272578c8066565a0e6cf5810b0/%40commitlint/cli/src/cli.ts#L336
48 | fn has_stdin(&self) -> bool {
49 | !stdin().is_terminal()
50 | }
51 |
52 | /// Read commit messages from stdin.
53 | pub fn read(&self) -> Result, Error> {
54 | // Check first whether or not the --edit option was supplied. When running from tooling such as
55 | // `pre-commit`, stdin exists, so this needs to come first.
56 | if let Some(edit) = self.edit.as_deref() {
57 | if edit != "false" {
58 | let msg = std::fs::read_to_string(edit)
59 | .expect(format!("Failed to read commit message from {}", edit).as_str());
60 | return Ok(vec![Message::new(msg)]);
61 | }
62 | }
63 |
64 | // Otherwise, check for stdin and use the incoming text buffer from there if so.
65 | if self.has_stdin() {
66 | let mut buffer = String::new();
67 | stdin()
68 | .read_to_string(&mut buffer)
69 | .expect("Failed to read commit messages from stdin");
70 | return Ok(vec![Message::new(buffer)]);
71 | }
72 |
73 | if self.from.is_some() || self.to.is_some() {
74 | // Reading directly from Git if from or to is specified.
75 | let config = ReadCommitMessageOptions {
76 | from: self.from.clone(),
77 | path: self.cwd.clone(),
78 | to: self.to.clone(),
79 | };
80 |
81 | let messages = git::read(config)
82 | .iter()
83 | .map(|s| Message::new(s.to_string()))
84 | .collect();
85 |
86 | return Ok(messages);
87 | }
88 |
89 | let default_path = std::path::PathBuf::from(".git").join("COMMIT_EDITMSG");
90 | let msg = std::fs::read_to_string(&default_path).expect(
91 | format!(
92 | "Failed to read commit message from {}",
93 | default_path.display()
94 | )
95 | .as_str(),
96 | );
97 | Ok(vec![Message::new(msg)])
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/cli/src/config.rs:
--------------------------------------------------------------------------------
1 | use serde::{Deserialize, Serialize};
2 | use std::fmt;
3 | use std::{fs, path::PathBuf};
4 |
5 | use crate::rule::Rules;
6 |
7 | /// Default Root config file path to search for.
8 | const DEFAULT_CONFIG_ROOT: &str = ".";
9 |
10 | /// Default commitlintrc configuration files
11 | /// If the user didn't specify a configuration file with -c or --config argument,
12 | /// we will try to find one of these files in the current directory.
13 | const DEFAULT_CONFIG_FILE: [&str; 4] = [
14 | ".commitlintrc",
15 | ".commitlintrc.json",
16 | ".commitlintrc.yaml",
17 | ".commitlintrc.yml",
18 | ];
19 |
20 | /// Config represents the configuration of commitlint.
21 | #[derive(Clone, Debug, Default, Deserialize, Serialize)]
22 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
23 | pub struct Config {
24 | /// Rules represents the rules of commitlint.
25 | pub rules: Rules,
26 | }
27 |
28 | impl fmt::Display for Config {
29 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 | let s = serde_yaml::to_string(&self).unwrap();
31 | write!(f, "{}", s)
32 | }
33 | }
34 |
35 | /// Load configuration from the specified path.
36 | pub async fn load(path: Option) -> Result {
37 | let config_file = match &path {
38 | Some(p) => Some(p.clone()),
39 | None => find_config_file(PathBuf::from(DEFAULT_CONFIG_ROOT)),
40 | };
41 |
42 | match (config_file, path) {
43 | // If the file was specified and found, load it.
44 | (Some(p), _) => load_config_file(p).await,
45 | // If the file was not specified and not found, return default config.
46 | (None, None) => Ok(Config::default()),
47 | // If the was explicitly specified but not found, return an error.
48 | (None, Some(p)) => Err(format!("Configuration file not found in {}", p.display())),
49 | }
50 | }
51 |
52 | /// Find configuration file in the specified path.
53 | /// Note that the first file found will be returned.
54 | pub fn find_config_file(path: PathBuf) -> Option {
55 | let mut path = path;
56 | for file in DEFAULT_CONFIG_FILE.iter() {
57 | path.push(file);
58 | if path.exists() {
59 | return Some(path);
60 | }
61 | path.pop();
62 | }
63 |
64 | None
65 | }
66 |
67 | /// Load config file from the specified path.
68 | pub async fn load_config_file(path: PathBuf) -> Result {
69 | if !path.exists() {
70 | return Err(format!(
71 | "Configuration file not found in {}",
72 | path.display()
73 | ));
74 | }
75 |
76 | match path.extension() {
77 | Some(ext) => match ext.to_str() {
78 | Some("json") => load_json_config_file(path).await,
79 | Some("yaml") | Some("yml") => load_yaml_config_file(path).await,
80 | _ => load_unknown_config_file(path).await,
81 | },
82 | None => Err(format!(
83 | "Unsupported configuration file format: {}",
84 | path.display()
85 | )),
86 | }
87 | }
88 |
89 | /// Load JSON config file from the specified path.
90 | async fn load_json_config_file(path: PathBuf) -> Result {
91 | let text = fs::read_to_string(path).unwrap();
92 |
93 | match serde_json::from_str::(&text) {
94 | Ok(config) => Ok(config),
95 | Err(err) => Err(format!("Failed to parse configuration file: {}", err)),
96 | }
97 | }
98 |
99 | /// Load YAML config file from the specified path.
100 | async fn load_yaml_config_file(path: PathBuf) -> Result {
101 | let text = fs::read_to_string(path).unwrap();
102 |
103 | match serde_yaml::from_str::(&text) {
104 | Ok(config) => Ok(config),
105 | Err(err) => Err(format!("Failed to parse configuration file: {}", err)),
106 | }
107 | }
108 |
109 | /// Try to load configuration file from the specified path.
110 | /// First try to load it as JSON, then as YAML.
111 | /// If both fail, return an error.
112 | async fn load_unknown_config_file(path: PathBuf) -> Result {
113 | let text = fs::read_to_string(path.clone()).unwrap();
114 |
115 | if let Ok(config) = serde_json::from_str::(&text) {
116 | return Ok(config);
117 | }
118 |
119 | if let Ok(config) = serde_yaml::from_str::(&text) {
120 | return Ok(config);
121 | }
122 |
123 | Err(format!(
124 | "Failed to parse configuration file: {}",
125 | path.display()
126 | ))
127 | }
128 |
--------------------------------------------------------------------------------
/cli/src/git.rs:
--------------------------------------------------------------------------------
1 | use regex::Regex;
2 | use std::{collections::HashMap, process::Command};
3 | /// ReadCommitMessageOptions represents the options for reading commit messages.
4 | /// Transparently, it is defined to be similar to the behavior of the git log command.
5 | #[derive(Clone, Debug)]
6 | pub struct ReadCommitMessageOptions {
7 | /// From is the starting commit hash to read from.
8 | pub from: Option,
9 |
10 | /// Path is the path to read commit messages from.
11 | pub path: String,
12 |
13 | /// To is the ending commit hash to read to.
14 | pub to: Option,
15 | }
16 |
17 | /// Get commit messages from git.
18 | pub fn read(options: ReadCommitMessageOptions) -> Vec {
19 | // Configure revision range following the git spec.
20 | //
21 | // See: https://git-scm.com/docs/git-log#Documentation/git-log.txt-ltrevision-rangegt
22 | //
23 | // Make a range if both `from` and `to` are specified, then assign from..to.
24 | // If both are not specified, then assign HEAD.
25 | let range = match (options.from, options.to) {
26 | (Some(from), Some(to)) => format!("{}..{}", from, to),
27 | (Some(from), None) => format!("{}..HEAD", from),
28 | (None, Some(to)) => format!("HEAD..{}", to),
29 | (None, None) => "HEAD".to_string(),
30 | };
31 |
32 | // See https://git-scm.com/docs/git-log
33 | let stdout = Command::new("git")
34 | .arg("log")
35 | .arg("--pretty=%B")
36 | .arg("--no-merges")
37 | .arg("--no-decorate")
38 | .arg("--reverse")
39 | .arg(range)
40 | .arg("--") // Explicitly specify the end of options as described https://git-scm.com/docs/git-log#Documentation/git-log.txt---ltpathgt82308203
41 | .arg(options.path)
42 | .output()
43 | .expect("Failed to execute git log")
44 | .stdout;
45 |
46 | let stdout = String::from_utf8_lossy(&stdout);
47 | extract_commit_messages(&stdout)
48 | }
49 |
50 | fn extract_commit_messages(input: &str) -> Vec {
51 | let commit_delimiter = Regex::new(r"(?m)^commit [0-9a-f]{40}$").unwrap();
52 | let commits: Vec<&str> = commit_delimiter.split(input).collect();
53 |
54 | let mut messages: Vec = Vec::new();
55 |
56 | for commit in commits {
57 | let message_lines: Vec<&str> = commit.trim().lines().collect();
58 | let message = message_lines.join("\n");
59 | messages.push(message);
60 | }
61 |
62 | messages
63 | }
64 |
65 | /// Parse a commit message and return the subject, body, and footers.
66 | ///
67 | /// Please refer the official documentation for the commit message format.
68 | /// See: https://www.conventionalcommits.org/en/v1.0.0/#summary
69 | ///
70 | /// ```ignore
71 | /// [optional scope]: <-- Subject
72 | ///
73 | /// [optional body] <-- Body
74 | ///
75 | /// [optional footer(s)] <-- Footer
76 | /// ```
77 | pub fn parse_commit_message(
78 | message: &str,
79 | ) -> (String, Option, Option>) {
80 | let lines: Vec<&str> = message.lines().collect();
81 | let mut lines_iter = lines.iter();
82 |
83 | let subject = lines_iter.next().unwrap_or(&"").trim().to_string();
84 | let mut body = None;
85 | let mut footer = None;
86 |
87 | let mut in_body = false;
88 | let mut in_footer = false;
89 |
90 | for line in lines_iter {
91 | if line.trim().is_empty() {
92 | if in_body {
93 | in_body = false;
94 | in_footer = true;
95 | }
96 | } else if in_footer {
97 | let parts: Vec<&str> = line.splitn(2, ':').map(|part| part.trim()).collect();
98 | if parts.len() == 2 {
99 | let key = parts[0].to_string();
100 | let value = parts[1].to_string();
101 | let footer_map = footer.get_or_insert(HashMap::new());
102 | footer_map.insert(key, value);
103 | }
104 | } else if !in_body {
105 | in_body = true;
106 | body = Some(line.trim().to_string());
107 | } else if let Some(b) = body.as_mut() {
108 | b.push('\n');
109 | b.push_str(line.trim());
110 | }
111 | }
112 |
113 | (subject, body, footer)
114 | }
115 |
116 | /// Parse a commit message subject and return the type, scope, and description.
117 | ///
118 | /// Note that exclamation mark is not respected as the existing commitlint
119 | /// does not have any rules for it.
120 | /// See: https://commitlint.js.org/reference/rules.html
121 | pub fn parse_subject(subject: &str) -> (Option, Option, Option) {
122 | let re = regex::Regex::new(
123 | r"^(?P\w+)(?:\((?P[^\)]+)\))?(?:!)?\:\s?(?P.*)$",
124 | )
125 | .unwrap();
126 | if let Some(captures) = re.captures(subject) {
127 | let r#type = captures.name("type").map(|m| m.as_str().to_string());
128 | let scope = captures.name("scope").map(|m| m.as_str().to_string());
129 | let description = captures.name("description").map(|m| m.as_str().to_string());
130 |
131 | return (r#type, scope, description);
132 | }
133 | // Fall back to the description.
134 | (None, None, Some(subject.to_string()))
135 | }
136 |
137 | #[cfg(test)]
138 | mod tests {
139 | use super::*;
140 |
141 | #[test]
142 | fn test_single_line_parse_commit_message() {
143 | let input = "feat(cli): add dummy option";
144 | let (subject, body, footer) = parse_commit_message(input);
145 | assert_eq!(subject, "feat(cli): add dummy option");
146 | assert_eq!(body, None);
147 | assert_eq!(footer, None);
148 | }
149 |
150 | #[test]
151 | fn test_body_parse_commit_message() {
152 | let input = "feat(cli): add dummy option
153 |
154 | Hello, there!";
155 | let (subject, body, footer) = parse_commit_message(input);
156 | assert_eq!(subject, "feat(cli): add dummy option");
157 | assert_eq!(body, Some("Hello, there!".to_string()));
158 | assert_eq!(footer, None);
159 | }
160 |
161 | #[test]
162 | fn test_footer_parse_commit_message() {
163 | let input = "feat(cli): add dummy option
164 |
165 | Hello, there!
166 |
167 | Link: Hello";
168 | let (subject, body, footer) = parse_commit_message(input);
169 |
170 | let mut f = HashMap::new();
171 | f.insert("Link".to_string(), "Hello".to_string());
172 | assert_eq!(subject, "feat(cli): add dummy option");
173 | assert_eq!(body, Some("Hello, there!".to_string()));
174 | assert!(footer.is_some());
175 | assert_eq!(f.get("Link"), Some(&"Hello".to_string()));
176 | }
177 |
178 | #[test]
179 | fn test_footer_with_multiline_body_parse_commit_message() {
180 | let input = "feat(cli): add dummy option
181 |
182 | Hello, there!
183 | I'm from Japan!
184 |
185 | Link: Hello";
186 | let (subject, body, footer) = parse_commit_message(input);
187 |
188 | let mut f = HashMap::new();
189 | f.insert("Link".to_string(), "Hello".to_string());
190 | assert_eq!(subject, "feat(cli): add dummy option");
191 | assert_eq!(
192 | body,
193 | Some(
194 | "Hello, there!
195 | I'm from Japan!"
196 | .to_string()
197 | )
198 | );
199 | assert!(footer.is_some());
200 | assert_eq!(f.get("Link"), Some(&"Hello".to_string()));
201 | }
202 |
203 | #[test]
204 | fn test_multiple_footers_parse_commit_message() {
205 | let input = "feat(cli): add dummy option
206 |
207 | Hello, there!
208 |
209 | Link: Hello
210 | Name: Keke";
211 | let (subject, body, footer) = parse_commit_message(input);
212 |
213 | assert_eq!(subject, "feat(cli): add dummy option");
214 | assert_eq!(body, Some("Hello, there!".to_string()));
215 | assert!(footer.is_some());
216 | assert_eq!(
217 | footer.clone().unwrap().get("Link"),
218 | Some(&"Hello".to_string())
219 | );
220 | assert_eq!(footer.unwrap().get("Name"), Some(&"Keke".to_string()));
221 | }
222 |
223 | #[test]
224 | fn test_parse_subject_with_scope() {
225 | let input = "feat(cli): add dummy option";
226 | assert_eq!(
227 | parse_subject(input),
228 | (
229 | Some("feat".to_string()),
230 | Some("cli".to_string()),
231 | Some("add dummy option".to_string())
232 | )
233 | );
234 | }
235 |
236 | #[test]
237 | fn test_parse_subject_with_emphasized_type_with_scope() {
238 | let input = "feat(cli)!: add dummy option";
239 | assert_eq!(
240 | parse_subject(input),
241 | (
242 | Some("feat".to_string()),
243 | Some("cli".to_string()),
244 | Some("add dummy option".to_string())
245 | )
246 | );
247 | }
248 |
249 | #[test]
250 | fn test_parse_subject_without_scope() {
251 | let input = "feat: add dummy option";
252 | assert_eq!(
253 | parse_subject(input),
254 | (
255 | Some("feat".to_string()),
256 | None,
257 | Some("add dummy option".to_string())
258 | )
259 | );
260 | }
261 |
262 | #[test]
263 | fn test_parse_subject_with_emphasized_type_without_scope() {
264 | let input = "feat!: add dummy option";
265 | assert_eq!(
266 | parse_subject(input),
267 | (
268 | Some("feat".to_string()),
269 | None,
270 | Some("add dummy option".to_string())
271 | )
272 | );
273 | }
274 |
275 | #[test]
276 | fn test_parse_subject_with_empty_description() {
277 | let input = "feat(cli): ";
278 | assert_eq!(
279 | parse_subject(input),
280 | (
281 | Some("feat".to_string()),
282 | Some("cli".to_string()),
283 | Some("".to_string())
284 | )
285 | );
286 | }
287 |
288 | #[test]
289 | fn test_parse_subject_with_empty_scope() {
290 | let input = "feat: add dummy commit";
291 | assert_eq!(
292 | parse_subject(input),
293 | (
294 | Some("feat".to_string()),
295 | None,
296 | Some("add dummy commit".to_string())
297 | )
298 | );
299 | }
300 |
301 | #[test]
302 | fn test_parse_subject_without_message() {
303 | let input = "";
304 | assert_eq!(parse_subject(input), (None, None, Some("".to_string())));
305 | }
306 |
307 | #[test]
308 | fn test_parse_subject_with_error_message() {
309 | let input = "test";
310 | assert_eq!(parse_subject(input), (None, None, Some("test".to_string())));
311 | }
312 | }
313 |
--------------------------------------------------------------------------------
/cli/src/lib.rs:
--------------------------------------------------------------------------------
1 | pub mod config;
2 | pub mod git;
3 | pub mod message;
4 | pub mod result;
5 | pub mod rule;
6 |
--------------------------------------------------------------------------------
/cli/src/main.rs:
--------------------------------------------------------------------------------
1 | mod args;
2 | mod config;
3 | mod git;
4 | mod message;
5 | mod result;
6 | mod rule;
7 |
8 | use args::Args;
9 | use clap::Parser;
10 | use message::validate;
11 |
12 | use std::process::exit;
13 |
14 | #[tokio::main]
15 | async fn main() {
16 | let args = Args::parse();
17 |
18 | let config = match config::load(args.config.clone()).await {
19 | Ok(c) => c,
20 | Err(err) => {
21 | eprintln!("Failed to load config: {}", err);
22 | exit(1)
23 | }
24 | };
25 |
26 | if args.print_config {
27 | println!("{}", config);
28 | }
29 |
30 | let messages = match args.read() {
31 | Ok(messages) => messages,
32 | Err(err) => {
33 | eprintln!("Failed to read commit messages: {}", err);
34 | exit(1)
35 | }
36 | };
37 |
38 | let threads = messages
39 | .into_iter()
40 | .map(|message| {
41 | let config = config.clone();
42 | tokio::spawn(async move { validate(&message, &config).await })
43 | })
44 | .collect::>();
45 |
46 | let results = futures::future::join_all(threads).await;
47 |
48 | let mut has_error: bool = false;
49 | for result in &results {
50 | if let Err(err) = result {
51 | eprintln!("{}", err);
52 | }
53 |
54 | if let Ok(Ok(h)) = result {
55 | if !h.violations.is_empty() {
56 | for violation in &h.violations {
57 | match violation.level {
58 | rule::Level::Error => {
59 | eprintln!("{}", violation.message);
60 | has_error = true
61 | }
62 | rule::Level::Warning => {
63 | println!("{}", violation.message);
64 | }
65 | _ => {}
66 | }
67 | }
68 | }
69 | }
70 | }
71 |
72 | if has_error {
73 | exit(1)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/cli/src/message.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | config::Config,
3 | git::{parse_commit_message, parse_subject},
4 | result::Result as LintResult,
5 | };
6 | use std::{collections::HashMap, fmt::Error};
7 |
8 | /// Message represents a single commit message.
9 | ///
10 | ///
11 | /// ```code
12 | /// [optional scope]:
13 | ///
14 | /// [optional body]
15 | ///
16 | /// [optional footer(s)]
17 | /// ```
18 | ///
19 | #[derive(Clone, Debug)]
20 | pub struct Message {
21 | /// Body part of the commit message.
22 | pub body: Option,
23 |
24 | /// Description part of the commit message.
25 | pub description: Option,
26 | /// Footers part of the commit message.
27 | pub footers: Option>,
28 |
29 | #[allow(dead_code)]
30 | /// Raw commit message (or any input from stdin) including the body and footers.
31 | pub raw: String,
32 |
33 | /// Type part of the commit message.
34 | pub r#type: Option,
35 |
36 | /// Scope part of the commit message.
37 | pub scope: Option,
38 |
39 | /// Subject part of the commit message.
40 | pub subject: Option,
41 | }
42 |
43 | /// Message represents a commit message.
44 | impl Message {
45 | /// Create a new Message.
46 | pub fn new(raw: String) -> Self {
47 | let (subject, body, footers) = parse_commit_message(&raw);
48 | let (r#type, scope, description) = parse_subject(&subject);
49 | Self {
50 | body,
51 | description,
52 | footers,
53 | raw,
54 | r#type,
55 | scope,
56 | subject: Some(subject),
57 | }
58 | }
59 | }
60 |
61 | /// validate the raw commit message.
62 | pub async fn validate(msg: &Message, config: &Config) -> Result {
63 | let violations = config.rules.validate(msg);
64 | Ok(LintResult { violations })
65 | }
66 |
--------------------------------------------------------------------------------
/cli/src/result.rs:
--------------------------------------------------------------------------------
1 | use crate::rule::Level;
2 |
3 | /// Result of the check.
4 | #[derive(Clone, Debug)]
5 | pub struct Result {
6 | /// List of violations to be printed.
7 | /// If it is empty, then there is no violation.
8 | pub violations: Vec,
9 | }
10 |
11 | /// Violation is a message that will be printed.
12 | #[derive(Clone, Debug)]
13 | pub struct Violation {
14 | /// Level of the violation.
15 | pub level: Level,
16 |
17 | /// Message of the violation.
18 | pub message: String,
19 | }
20 |
--------------------------------------------------------------------------------
/cli/src/rule.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Debug;
2 |
3 | use crate::{message::Message, result::Violation};
4 | use serde::{Deserialize, Serialize};
5 |
6 | use self::{
7 | body_empty::BodyEmpty, body_max_length::BodyMaxLength, description_empty::DescriptionEmpty,
8 | description_format::DescriptionFormat, description_max_length::DescriptionMaxLength,
9 | footers_empty::FootersEmpty, r#type::Type, scope::Scope, scope_empty::ScopeEmpty,
10 | scope_format::ScopeFormat, scope_max_length::ScopeMaxLength, subject_empty::SubjectEmpty,
11 | type_empty::TypeEmpty, type_format::TypeFormat, type_max_length::TypeMaxLength,
12 | };
13 |
14 | pub mod body_empty;
15 | pub mod body_max_length;
16 | pub mod description_empty;
17 | pub mod description_format;
18 | pub mod description_max_length;
19 | pub mod footers_empty;
20 | pub mod scope;
21 | pub mod scope_empty;
22 | pub mod scope_format;
23 | pub mod scope_max_length;
24 | pub mod subject_empty;
25 | pub mod r#type;
26 | pub mod type_empty;
27 | pub mod type_format;
28 | pub mod type_max_length;
29 |
30 | /// Rules represents the rules of commitlint.
31 | /// See: https://commitlint.js.org/reference/rules.html
32 | #[derive(Clone, Debug, Deserialize, Serialize)]
33 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
34 | pub struct Rules {
35 | #[serde(rename = "body-empty")]
36 | #[serde(skip_serializing_if = "Option::is_none")]
37 | pub body_empty: Option,
38 |
39 | #[serde(rename = "body-max-length")]
40 | #[serde(skip_serializing_if = "Option::is_none")]
41 | pub body_max_length: Option,
42 |
43 | #[serde(rename = "description-empty")]
44 | #[serde(skip_serializing_if = "Option::is_none")]
45 | pub description_empty: Option,
46 |
47 | #[serde(rename = "description-format")]
48 | #[serde(skip_serializing_if = "Option::is_none")]
49 | pub description_format: Option,
50 |
51 | #[serde(rename = "description-max-length")]
52 | #[serde(skip_serializing_if = "Option::is_none")]
53 | pub description_max_length: Option,
54 |
55 | #[serde(rename = "footers-empty")]
56 | #[serde(skip_serializing_if = "Option::is_none")]
57 | pub footers_empty: Option,
58 |
59 | #[serde(rename = "scope")]
60 | #[serde(skip_serializing_if = "Option::is_none")]
61 | pub scope: Option,
62 |
63 | #[serde(rename = "scope-empty")]
64 | #[serde(skip_serializing_if = "Option::is_none")]
65 | pub scope_empty: Option,
66 |
67 | #[serde(rename = "scope-format")]
68 | #[serde(skip_serializing_if = "Option::is_none")]
69 | pub scope_format: Option,
70 |
71 | #[serde(rename = "scope-max-length")]
72 | #[serde(skip_serializing_if = "Option::is_none")]
73 | pub scope_max_length: Option,
74 |
75 | #[serde(rename = "subject-empty")]
76 | #[serde(skip_serializing_if = "Option::is_none")]
77 | pub subject_empty: Option,
78 |
79 | #[serde(rename = "type")]
80 | #[serde(skip_serializing_if = "Option::is_none")]
81 | pub r#type: Option,
82 |
83 | #[serde(rename = "type-empty")]
84 | #[serde(skip_serializing_if = "Option::is_none")]
85 | pub type_empty: Option,
86 |
87 | #[serde(rename = "type-format")]
88 | #[serde(skip_serializing_if = "Option::is_none")]
89 | pub type_format: Option,
90 |
91 | #[serde(rename = "type-max-length")]
92 | #[serde(skip_serializing_if = "Option::is_none")]
93 | pub type_max_length: Option,
94 | }
95 |
96 | /// Rule is a collection of rules.
97 | impl Rules {
98 | pub fn validate(&self, message: &Message) -> Vec {
99 | let mut results = Vec::new();
100 |
101 | if let Some(rule) = &self.body_empty {
102 | if let Some(validation) = rule.validate(message) {
103 | results.push(validation);
104 | }
105 | }
106 |
107 | if let Some(rule) = &self.body_max_length {
108 | if let Some(validation) = rule.validate(message) {
109 | results.push(validation);
110 | }
111 | }
112 |
113 | if let Some(rule) = &self.description_empty {
114 | if let Some(validation) = rule.validate(message) {
115 | results.push(validation);
116 | }
117 | }
118 |
119 | if let Some(rule) = &self.description_format {
120 | if let Some(validation) = rule.validate(message) {
121 | results.push(validation);
122 | }
123 | }
124 |
125 | if let Some(rule) = &self.description_max_length {
126 | if let Some(validation) = rule.validate(message) {
127 | results.push(validation);
128 | }
129 | }
130 |
131 | if let Some(rule) = &self.scope {
132 | if let Some(validation) = rule.validate(message) {
133 | results.push(validation);
134 | }
135 | }
136 |
137 | if let Some(rule) = &self.scope_empty {
138 | if let Some(validation) = rule.validate(message) {
139 | results.push(validation);
140 | }
141 | }
142 |
143 | if let Some(rule) = &self.scope_format {
144 | if let Some(validation) = rule.validate(message) {
145 | results.push(validation);
146 | }
147 | }
148 |
149 | if let Some(rule) = &self.scope_max_length {
150 | if let Some(validation) = rule.validate(message) {
151 | results.push(validation);
152 | }
153 | }
154 |
155 | if let Some(rule) = &self.subject_empty {
156 | if let Some(validation) = rule.validate(message) {
157 | results.push(validation);
158 | }
159 | }
160 |
161 | if let Some(rule) = &self.r#type {
162 | if let Some(validation) = rule.validate(message) {
163 | results.push(validation);
164 | }
165 | }
166 |
167 | if let Some(rule) = &self.type_empty {
168 | if let Some(validation) = rule.validate(message) {
169 | results.push(validation);
170 | }
171 | }
172 |
173 | if let Some(rule) = &self.type_format {
174 | if let Some(validation) = rule.validate(message) {
175 | results.push(validation);
176 | }
177 | }
178 |
179 | if let Some(rule) = &self.type_max_length {
180 | if let Some(validation) = rule.validate(message) {
181 | results.push(validation);
182 | }
183 | }
184 |
185 | results
186 | }
187 | }
188 |
189 | /// Default implementation of Rules.
190 | /// If no config files are specified, this will be used.
191 | impl Default for Rules {
192 | fn default() -> Self {
193 | Self {
194 | body_empty: None,
195 | body_max_length: None,
196 | description_empty: DescriptionEmpty::default().into(),
197 | description_format: None,
198 | description_max_length: None,
199 | footers_empty: None,
200 | scope: None,
201 | scope_empty: None,
202 | scope_format: None,
203 | scope_max_length: None,
204 | subject_empty: SubjectEmpty::default().into(),
205 | r#type: None,
206 | type_empty: TypeEmpty::default().into(),
207 | type_format: None,
208 | type_max_length: None,
209 | }
210 | }
211 | }
212 |
213 | /// Rule trait represents a rule that can be applied to a text.
214 | pub trait Rule: Default {
215 | /// The name of the rule.
216 | /// Note that it should be unique
217 | const NAME: &'static str;
218 |
219 | /// The message to display when the rule fails.
220 | fn message(&self, message: &Message) -> String;
221 |
222 | /// The level of the rule.
223 | const LEVEL: Level;
224 |
225 | /// Validate the given text.
226 | fn validate(&self, message: &Message) -> Option;
227 | }
228 |
229 | /// Level represents the level of a rule.
230 | #[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
231 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
232 | pub enum Level {
233 | #[serde(rename = "error")]
234 | Error,
235 |
236 | #[serde(rename = "ignore")]
237 | Ignore,
238 |
239 | #[serde(rename = "warning")]
240 | Warning,
241 | }
242 |
--------------------------------------------------------------------------------
/cli/src/rule/body_empty.rs:
--------------------------------------------------------------------------------
1 | use crate::{message::Message, result::Violation, rule::Rule};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Level;
5 |
6 | /// BodyEmpty represents the body-empty rule.
7 | #[derive(Clone, Debug, Deserialize, Serialize)]
8 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9 | pub struct BodyEmpty {
10 | /// Level represents the level of the rule.
11 | ///
12 | // Note that currently the default literal is not supported.
13 | // See: https://github.com/serde-rs/serde/issues/368
14 | level: Option,
15 | }
16 |
17 | /// BodyEmpty represents the body-empty rule.
18 | impl Rule for BodyEmpty {
19 | const NAME: &'static str = "body-empty";
20 | const LEVEL: Level = Level::Error;
21 |
22 | fn message(&self, _message: &Message) -> String {
23 | "body is empty".to_string()
24 | }
25 |
26 | fn validate(&self, message: &Message) -> Option {
27 | if message.body.is_none() {
28 | return Some(Violation {
29 | level: self.level.unwrap_or(Self::LEVEL),
30 | message: self.message(message),
31 | });
32 | }
33 |
34 | None
35 | }
36 | }
37 |
38 | /// Default implementation of BodyEmpty.
39 | impl Default for BodyEmpty {
40 | fn default() -> Self {
41 | Self {
42 | level: Some(Self::LEVEL),
43 | }
44 | }
45 | }
46 |
47 | #[cfg(test)]
48 | mod tests {
49 | use super::*;
50 |
51 | #[test]
52 | fn test_non_empty_body() {
53 | let rule = BodyEmpty::default();
54 | let message = Message {
55 | body: Some("Hello world".to_string()),
56 | description: Some("broadcast $destroy event on scope destruction".to_string()),
57 | footers: None,
58 | r#type: Some("feat".to_string()),
59 | raw: "feat(scope): broadcast $destroy event on scope destruction
60 |
61 | Hello world"
62 | .to_string(),
63 | scope: Some("scope".to_string()),
64 | subject: Some("feat(scope): broadcast $destroy event on scope destruction".to_string()),
65 | };
66 |
67 | assert!(rule.validate(&message).is_none());
68 | }
69 |
70 | #[test]
71 | fn test_empty_body() {
72 | let rule = BodyEmpty::default();
73 | let message = Message {
74 | body: None,
75 | description: None,
76 | footers: None,
77 | r#type: Some("feat".to_string()),
78 | raw: "feat(scope): broadcast $destroy event on scope destruction".to_string(),
79 | scope: Some("scope".to_string()),
80 | subject: None,
81 | };
82 |
83 | let violation = rule.validate(&message);
84 | assert!(violation.is_some());
85 | assert_eq!(violation.clone().unwrap().level, Level::Error);
86 | assert_eq!(violation.unwrap().message, "body is empty".to_string());
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/cli/src/rule/body_max_length.rs:
--------------------------------------------------------------------------------
1 | use crate::{message::Message, result::Violation, rule::Rule};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Level;
5 |
6 | /// BodyMaxLength represents the body-max-length rule.
7 | #[derive(Clone, Debug, Deserialize, Serialize)]
8 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9 | pub struct BodyMaxLength {
10 | /// Level represents the level of the rule.
11 | ///
12 | // Note that currently the default literal is not supported.
13 | // See: https://github.com/serde-rs/serde/issues/368
14 | level: Option,
15 |
16 | /// Length represents the maximum length of the body.
17 | length: usize,
18 | }
19 |
20 | /// BodyMaxLength represents the body-max-length rule.
21 | impl Rule for BodyMaxLength {
22 | const NAME: &'static str = "body-max-length";
23 | const LEVEL: Level = Level::Error;
24 |
25 | fn message(&self, _message: &Message) -> String {
26 | format!("body is longer than {} characters", self.length)
27 | }
28 |
29 | fn validate(&self, message: &Message) -> Option {
30 | match &message.body {
31 | Some(body) => {
32 | if body.len() >= self.length {
33 | return Some(Violation {
34 | level: self.level.unwrap_or(Self::LEVEL),
35 | message: self.message(message),
36 | });
37 | }
38 | }
39 | None => {
40 | return Some(Violation {
41 | level: self.level.unwrap_or(Self::LEVEL),
42 | message: self.message(message),
43 | })
44 | }
45 | }
46 |
47 | None
48 | }
49 | }
50 |
51 | /// Default implementation of BodyMaxLength.
52 | impl Default for BodyMaxLength {
53 | fn default() -> Self {
54 | Self {
55 | level: Some(Self::LEVEL),
56 | length: 72,
57 | }
58 | }
59 | }
60 |
61 | #[cfg(test)]
62 | mod tests {
63 | use super::*;
64 |
65 | #[test]
66 | fn test_long_body() {
67 | let rule = BodyMaxLength {
68 | length: usize::MAX, // Long length for testing
69 | ..Default::default()
70 | };
71 | let message = Message {
72 | body: Some("Hello world".to_string()),
73 | description: Some("broadcast $destroy event on scope destruction".to_string()),
74 | footers: None,
75 | r#type: Some("feat".to_string()),
76 | raw: "feat(scope): broadcast $destroy event on scope destruction
77 |
78 | Hey!"
79 | .to_string(),
80 | scope: Some("scope".to_string()),
81 | subject: Some("feat(scope): broadcast $destroy event on scope destruction".to_string()),
82 | };
83 |
84 | assert!(rule.validate(&message).is_none());
85 | }
86 |
87 | #[test]
88 | fn test_short_body() {
89 | let rule = BodyMaxLength {
90 | length: 10, // Short length for testing
91 | ..Default::default()
92 | };
93 | let message = Message {
94 | body: Some("Hello, I'm a long body".to_string()),
95 | description: None,
96 | footers: None,
97 | r#type: Some("feat".to_string()),
98 | raw: "feat(scope): broadcast $destroy event on scope destruction
99 |
100 | Hello, I'm a long body"
101 | .to_string(),
102 | scope: Some("scope".to_string()),
103 | subject: None,
104 | };
105 |
106 | let violation = rule.validate(&message);
107 | assert!(violation.is_some());
108 | assert_eq!(violation.clone().unwrap().level, Level::Error);
109 | assert_eq!(
110 | violation.unwrap().message,
111 | format!("body is longer than {} characters", rule.length)
112 | );
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/cli/src/rule/description_empty.rs:
--------------------------------------------------------------------------------
1 | use crate::{message::Message, result::Violation, rule::Rule};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Level;
5 |
6 | /// DescriptionEmpty represents the subject-empty rule.
7 | #[derive(Clone, Debug, Deserialize, Serialize)]
8 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9 | pub struct DescriptionEmpty {
10 | /// Level represents the level of the rule.
11 | ///
12 | // Note that currently the default literal is not supported.
13 | // See: https://github.com/serde-rs/serde/issues/368
14 | level: Option,
15 | }
16 |
17 | /// DescriptionEmpty represents the description-empty rule.
18 | impl Rule for DescriptionEmpty {
19 | const NAME: &'static str = "description-empty";
20 | const LEVEL: Level = Level::Error;
21 |
22 | fn message(&self, _message: &Message) -> String {
23 | "description is empty or missing space in the beginning".to_string()
24 | }
25 |
26 | fn validate(&self, message: &Message) -> Option {
27 | match message.description {
28 | None => Some(Violation {
29 | level: self.level.unwrap_or(Self::LEVEL),
30 | message: self.message(message),
31 | }),
32 | Some(ref desc) if desc.is_empty() => Some(Violation {
33 | level: self.level.unwrap_or(Self::LEVEL),
34 | message: self.message(message),
35 | }),
36 | _ => None,
37 | }
38 | }
39 | }
40 |
41 | /// Default implementation of DescriptionEmpty.
42 | impl Default for DescriptionEmpty {
43 | fn default() -> Self {
44 | Self {
45 | level: Some(Self::LEVEL),
46 | }
47 | }
48 | }
49 |
50 | #[cfg(test)]
51 | mod tests {
52 | use super::*;
53 |
54 | #[test]
55 | fn test_non_empty_description() {
56 | let rule = DescriptionEmpty::default();
57 | let message = Message {
58 | body: None,
59 | description: Some("broadcast $destroy event on scope destruction".to_string()),
60 | footers: None,
61 | r#type: Some("feat".to_string()),
62 | raw: "feat(scope): broadcast $destroy event on scope destruction".to_string(),
63 | scope: Some("scope".to_string()),
64 | subject: None,
65 | };
66 |
67 | assert!(rule.validate(&message).is_none());
68 | }
69 |
70 | #[test]
71 | fn test_empty_description() {
72 | let rule = DescriptionEmpty::default();
73 | let message = Message {
74 | body: None,
75 | description: None,
76 | footers: None,
77 | r#type: Some("feat".to_string()),
78 | raw: "(scope):".to_string(),
79 | scope: Some("scope".to_string()),
80 | subject: None,
81 | };
82 |
83 | let violation = rule.validate(&message);
84 | assert!(violation.is_some());
85 | assert_eq!(violation.clone().unwrap().level, Level::Error);
86 | assert_eq!(
87 | violation.unwrap().message,
88 | "description is empty or missing space in the beginning".to_string()
89 | );
90 | }
91 |
92 | #[test]
93 | fn test_blank_description() {
94 | let rule = DescriptionEmpty::default();
95 | let message = Message {
96 | body: None,
97 | description: Some("".to_string()),
98 | footers: None,
99 | r#type: Some("feat".to_string()),
100 | raw: "(scope):".to_string(),
101 | scope: Some("scope".to_string()),
102 | subject: None,
103 | };
104 |
105 | let violation = rule.validate(&message);
106 | assert!(violation.is_some());
107 | assert_eq!(violation.clone().unwrap().level, Level::Error);
108 | assert_eq!(
109 | violation.unwrap().message,
110 | "description is empty or missing space in the beginning".to_string()
111 | );
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/cli/src/rule/description_format.rs:
--------------------------------------------------------------------------------
1 | use crate::{message::Message, result::Violation, rule::Rule};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Level;
5 |
6 | /// DescriptionFormat represents the description-format rule.
7 | #[derive(Clone, Debug, Deserialize, Serialize)]
8 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9 | pub struct DescriptionFormat {
10 | /// Level represents the level of the rule.
11 | ///
12 | // Note that currently the default literal is not supported.
13 | // See: https://github.com/serde-rs/serde/issues/368
14 | level: Option,
15 |
16 | /// Format represents the format of the description.
17 | format: Option,
18 | }
19 |
20 | /// DescriptionFormat represents the description-format rule.
21 | impl Rule for DescriptionFormat {
22 | const NAME: &'static str = "description-format";
23 | const LEVEL: Level = Level::Error;
24 |
25 | fn message(&self, _message: &Message) -> String {
26 | format!(
27 | "description format does not match format: {}",
28 | self.format.as_ref().unwrap()
29 | )
30 | }
31 |
32 | fn validate(&self, message: &Message) -> Option {
33 | if let Some(format) = &self.format {
34 | let regex = match regex::Regex::new(format) {
35 | Ok(regex) => regex,
36 | Err(err) => {
37 | return Some(Violation {
38 | level: self.level.unwrap_or(Self::LEVEL),
39 | message: err.to_string(),
40 | });
41 | }
42 | };
43 |
44 | match &message.description {
45 | None => {
46 | return Some(Violation {
47 | level: self.level.unwrap_or(Self::LEVEL),
48 | message: "found no description".to_string(),
49 | });
50 | }
51 | Some(description) => {
52 | if !regex.is_match(description) {
53 | return Some(Violation {
54 | level: self.level.unwrap_or(Self::LEVEL),
55 | message: self.message(message),
56 | });
57 | }
58 | }
59 | }
60 | }
61 |
62 | None
63 | }
64 | }
65 |
66 | /// Default implementation of DescriptionFormat.
67 | impl Default for DescriptionFormat {
68 | fn default() -> Self {
69 | Self {
70 | level: Some(Self::LEVEL),
71 | format: None,
72 | }
73 | }
74 | }
75 |
76 | #[cfg(test)]
77 | mod tests {
78 | use super::*;
79 |
80 | #[test]
81 | fn test_invalid_description_format() {
82 | let rule = DescriptionFormat {
83 | format: Some(r"^[a-z].*".to_string()),
84 | ..Default::default()
85 | };
86 |
87 | let message = Message {
88 | body: None,
89 | description: Some("add new flag".to_string()),
90 | footers: None,
91 | r#type: Some("feat".to_string()),
92 | raw: "feat(scope): add new flag".to_string(),
93 | scope: Some("scope".to_string()),
94 | subject: None,
95 | };
96 |
97 | assert!(rule.validate(&message).is_none());
98 | }
99 |
100 | #[test]
101 | fn test_valid_description_format() {
102 | let rule = DescriptionFormat {
103 | format: Some(r"^[a-z].*".to_string()),
104 | ..Default::default()
105 | };
106 |
107 | let message = Message {
108 | body: None,
109 | description: Some("Add new flag".to_string()),
110 | footers: None,
111 | r#type: Some("feat".to_string()),
112 | raw: "feat(scope): Add new flag".to_string(),
113 | scope: Some("scope".to_string()),
114 | subject: None,
115 | };
116 |
117 | let violation = rule.validate(&message);
118 | assert!(violation.is_some());
119 | assert_eq!(violation.clone().unwrap().level, Level::Error);
120 | assert_eq!(
121 | violation.unwrap().message,
122 | "description format does not match format: ^[a-z].*".to_string()
123 | );
124 | }
125 |
126 | #[test]
127 | fn test_invalid_regex() {
128 | let rule = DescriptionFormat {
129 | format: Some(r"(".to_string()),
130 | ..Default::default()
131 | };
132 |
133 | let message = Message {
134 | body: None,
135 | description: Some("Add regex".to_string()),
136 | footers: None,
137 | r#type: Some("feat".to_string()),
138 | raw: "feat(scope): Add regex".to_string(),
139 | scope: Some("scope".to_string()),
140 | subject: None,
141 | };
142 |
143 | let violation = rule.validate(&message);
144 | assert!(violation.is_some());
145 | assert_eq!(violation.clone().unwrap().level, Level::Error);
146 | assert!(violation.unwrap().message.contains("regex parse error"));
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/cli/src/rule/description_max_length.rs:
--------------------------------------------------------------------------------
1 | use crate::{message::Message, result::Violation, rule::Rule};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Level;
5 |
6 | /// DescriptionMaxLength represents the description-max-length rule.
7 | #[derive(Clone, Debug, Deserialize, Serialize)]
8 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9 | pub struct DescriptionMaxLength {
10 | /// Level represents the level of the rule.
11 | ///
12 | // Note that currently the default literal is not supported.
13 | // See: https://github.com/serde-rs/serde/issues/368
14 | level: Option,
15 |
16 | /// Length represents the maximum length of the description.
17 | length: usize,
18 | }
19 |
20 | /// DescriptionMaxLength represents the description-max-length rule.
21 | impl Rule for DescriptionMaxLength {
22 | const NAME: &'static str = "description-max-length";
23 | const LEVEL: Level = Level::Error;
24 |
25 | fn message(&self, _message: &Message) -> String {
26 | format!("description is longer than {} characters", self.length)
27 | }
28 |
29 | fn validate(&self, message: &Message) -> Option {
30 | if let Some(desc) = &message.description {
31 | if desc.len() >= self.length {
32 | return Some(Violation {
33 | level: self.level.unwrap_or(Self::LEVEL),
34 | message: self.message(message),
35 | });
36 | }
37 | }
38 |
39 | None
40 | }
41 | }
42 |
43 | /// Default implementation of DescriptionMaxLength.
44 | impl Default for DescriptionMaxLength {
45 | fn default() -> Self {
46 | Self {
47 | level: Some(Self::LEVEL),
48 | length: 72,
49 | }
50 | }
51 | }
52 |
53 | #[cfg(test)]
54 | mod tests {
55 | use super::*;
56 |
57 | #[test]
58 | fn test_long_description() {
59 | let rule = DescriptionMaxLength {
60 | length: usize::MAX, // Long length for testing
61 | ..Default::default()
62 | };
63 | let message = Message {
64 | body: None,
65 | description: Some("desc".to_string()),
66 | footers: None,
67 | r#type: Some("feat".to_string()),
68 | raw: "feat(scope): desc".to_string(),
69 | scope: Some("scope".to_string()),
70 | subject: Some("feat(scope): desc".to_string()),
71 | };
72 |
73 | assert!(rule.validate(&message).is_none());
74 | }
75 |
76 | #[test]
77 | fn test_empty_description() {
78 | let rule = DescriptionMaxLength {
79 | length: 10, // Short length for testing
80 | ..Default::default()
81 | };
82 | let message = Message {
83 | body: None,
84 | description: None,
85 | footers: None,
86 | r#type: Some("feat".to_string()),
87 | raw: "feat(scope)".to_string(),
88 | scope: Some("scope".to_string()),
89 | subject: None,
90 | };
91 |
92 | assert!(rule.validate(&message).is_none());
93 | }
94 |
95 | #[test]
96 | fn test_short_description() {
97 | let rule = DescriptionMaxLength {
98 | length: 10, // Short length for testing
99 | ..Default::default()
100 | };
101 | let message = Message {
102 | body: None,
103 | description: Some("feat(scope): I'm long description".to_string()),
104 | footers: None,
105 | r#type: Some("feat".to_string()),
106 | raw: "feat(scope): I'm long description".to_string(),
107 | scope: Some("scope".to_string()),
108 | subject: None,
109 | };
110 |
111 | let violation = rule.validate(&message);
112 | assert!(violation.is_some());
113 | assert_eq!(violation.clone().unwrap().level, Level::Error);
114 | assert_eq!(
115 | violation.unwrap().message,
116 | format!("description is longer than {} characters", rule.length)
117 | );
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/cli/src/rule/footers_empty.rs:
--------------------------------------------------------------------------------
1 | use crate::{message::Message, result::Violation, rule::Rule};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Level;
5 |
6 | /// FootersEmpty represents the footer-empty rule.
7 | #[derive(Clone, Debug, Deserialize, Serialize)]
8 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9 | pub struct FootersEmpty {
10 | /// Level represents the level of the rule.
11 | ///
12 | // Note that currently the default literal is not supported.
13 | // See: https://github.com/serde-rs/serde/issues/368
14 | level: Option,
15 | }
16 |
17 | /// FooterEmpty represents the footer-empty rule.
18 | impl Rule for FootersEmpty {
19 | const NAME: &'static str = "footers-empty";
20 | const LEVEL: Level = Level::Error;
21 |
22 | fn message(&self, _message: &Message) -> String {
23 | "footers are empty".to_string()
24 | }
25 |
26 | fn validate(&self, message: &Message) -> Option {
27 | if message.footers.is_none() {
28 | return Some(Violation {
29 | level: self.level.unwrap_or(Self::LEVEL),
30 | message: self.message(message),
31 | });
32 | }
33 |
34 | None
35 | }
36 | }
37 |
38 | /// Default implementation of FooterEmpty.
39 | impl Default for FootersEmpty {
40 | fn default() -> Self {
41 | Self {
42 | level: Some(Self::LEVEL),
43 | }
44 | }
45 | }
46 |
47 | #[cfg(test)]
48 | mod tests {
49 | use std::collections::HashMap;
50 |
51 | use super::*;
52 |
53 | #[test]
54 | fn test_non_empty_footer() {
55 | let rule = FootersEmpty::default();
56 |
57 | let mut f = HashMap::new();
58 | f.insert("Link".to_string(), "hello".to_string());
59 |
60 | let message = Message {
61 | body: Some("Hello world".to_string()),
62 | description: Some("broadcast $destroy event on scope destruction".to_string()),
63 | footers: Some(f),
64 | r#type: Some("feat".to_string()),
65 | raw: "feat(scope): broadcast $destroy event on scope destruction
66 |
67 | Hello world
68 |
69 | Link: hello"
70 | .to_string(),
71 | scope: Some("scope".to_string()),
72 | subject: Some("feat(scope): broadcast $destroy event on scope destruction".to_string()),
73 | };
74 |
75 | assert!(rule.validate(&message).is_none());
76 | }
77 |
78 | #[test]
79 | fn test_empty_footer() {
80 | let rule = FootersEmpty::default();
81 | let message = Message {
82 | body: None,
83 | description: None,
84 | footers: None,
85 | r#type: Some("feat".to_string()),
86 | raw: "feat(scope): broadcast $destroy event on scope destruction".to_string(),
87 | scope: Some("scope".to_string()),
88 | subject: None,
89 | };
90 |
91 | let violation = rule.validate(&message);
92 | assert!(violation.is_some());
93 | assert_eq!(violation.clone().unwrap().level, Level::Error);
94 | assert_eq!(violation.unwrap().message, "footers are empty".to_string());
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/cli/src/rule/scope.rs:
--------------------------------------------------------------------------------
1 | use crate::{message::Message, result::Violation, rule::Rule};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Level;
5 |
6 | /// Scope represents the subject-empty rule.
7 | #[derive(Clone, Debug, Deserialize, Serialize)]
8 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9 | pub struct Scope {
10 | /// Level represents the level of the rule.
11 | ///
12 | // Note that currently the default literal is not supported.
13 | // See: https://github.com/serde-rs/serde/issues/368
14 | level: Option,
15 |
16 | /// Options represents the options of the rule.
17 | /// If the option is empty, it means that no scope is allowed.
18 | options: Vec,
19 |
20 | /// Optional scope.
21 | /// If true, even if the scope is not present, it is allowed.
22 | optional: bool,
23 | }
24 |
25 | /// Scope represents the scope rule.
26 | impl Rule for Scope {
27 | const NAME: &'static str = "scope";
28 | const LEVEL: Level = Level::Error;
29 |
30 | fn message(&self, message: &Message) -> String {
31 | if self.options.is_empty() {
32 | return "scopes are not allowed".to_string();
33 | }
34 |
35 | format!(
36 | "scope {} is not allowed. Only {:?} are allowed",
37 | message.scope.as_ref().unwrap_or(&"".to_string()),
38 | self.options
39 | )
40 | }
41 |
42 | fn validate(&self, message: &Message) -> Option {
43 | match &message.scope {
44 | None => {
45 | if self.options.is_empty() || self.optional {
46 | return None;
47 | }
48 | }
49 | Some(scope) if scope.is_empty() => {
50 | if self.options.is_empty() {
51 | return None;
52 | }
53 | }
54 | Some(scope) if self.options.contains(scope) => {
55 | return None;
56 | }
57 | _ => {}
58 | }
59 |
60 | Some(Violation {
61 | level: self.level.unwrap_or(Self::LEVEL),
62 | message: self.message(message),
63 | })
64 | }
65 | }
66 |
67 | /// Default implementation of Scope.
68 | impl Default for Scope {
69 | fn default() -> Self {
70 | Self {
71 | level: Some(Self::LEVEL),
72 | optional: false,
73 | options: vec![],
74 | }
75 | }
76 | }
77 |
78 | #[cfg(test)]
79 | mod tests {
80 | use super::*;
81 |
82 | mod empty_options {
83 | use super::*;
84 |
85 | #[test]
86 | fn test_empty_scope() {
87 | let rule = Scope::default();
88 |
89 | let message = Message {
90 | body: None,
91 | description: None,
92 | footers: None,
93 | r#type: None,
94 | raw: "".to_string(),
95 | scope: Some("".to_string()),
96 | subject: None,
97 | };
98 |
99 | let violation = rule.validate(&message);
100 | assert!(violation.is_none());
101 | }
102 |
103 | #[test]
104 | fn test_none_scope() {
105 | let rule = Scope::default();
106 |
107 | let message = Message {
108 | body: None,
109 | description: None,
110 | footers: None,
111 | r#type: None,
112 | raw: "".to_string(),
113 | scope: None,
114 | subject: None,
115 | };
116 |
117 | let violation = rule.validate(&message);
118 | assert!(violation.is_none());
119 | }
120 |
121 | #[test]
122 | fn test_scope() {
123 | let rule = Scope::default();
124 |
125 | let message = Message {
126 | body: None,
127 | description: None,
128 | footers: None,
129 | r#type: Some("feat".to_string()),
130 | raw: "feat(web): broadcast $destroy event on scope destruction".to_string(),
131 | scope: Some("web".to_string()),
132 | subject: None,
133 | };
134 |
135 | let violation = rule.validate(&message);
136 | assert!(violation.is_some());
137 | assert_eq!(violation.clone().unwrap().level, Level::Error);
138 | assert_eq!(
139 | violation.unwrap().message,
140 | "scopes are not allowed".to_string()
141 | );
142 | }
143 | }
144 |
145 | mod scopes {
146 | use super::*;
147 | #[test]
148 | fn test_empty_scope() {
149 | let rule = Scope {
150 | options: vec!["api".to_string(), "web".to_string()],
151 | ..Default::default()
152 | };
153 |
154 | let message = Message {
155 | body: None,
156 | description: None,
157 | footers: None,
158 | r#type: None,
159 | raw: "".to_string(),
160 | scope: Some("".to_string()),
161 | subject: None,
162 | };
163 |
164 | let violation = rule.validate(&message);
165 | assert!(violation.is_some());
166 | assert_eq!(violation.clone().unwrap().level, Level::Error);
167 | assert_eq!(
168 | violation.unwrap().message,
169 | "scope is not allowed. Only [\"api\", \"web\"] are allowed"
170 | );
171 | }
172 |
173 | #[test]
174 | fn test_none_scope() {
175 | let rule = Scope {
176 | options: vec!["api".to_string(), "web".to_string()],
177 | ..Default::default()
178 | };
179 |
180 | let message = Message {
181 | body: None,
182 | description: None,
183 | footers: None,
184 | r#type: None,
185 | raw: "".to_string(),
186 | scope: None,
187 | subject: None,
188 | };
189 |
190 | let violation = rule.validate(&message);
191 | assert!(violation.is_some());
192 | assert_eq!(violation.clone().unwrap().level, Level::Error);
193 | assert_eq!(
194 | violation.unwrap().message,
195 | "scope is not allowed. Only [\"api\", \"web\"] are allowed".to_string()
196 | );
197 | }
198 |
199 | #[test]
200 | fn test_valid_scope() {
201 | let rule = Scope {
202 | options: vec!["api".to_string(), "web".to_string()],
203 | ..Default::default()
204 | };
205 |
206 | let message = Message {
207 | body: None,
208 | description: None,
209 | footers: None,
210 | r#type: Some("feat".to_string()),
211 | raw: "feat(web): broadcast $destroy event on scope destruction".to_string(),
212 | scope: Some("web".to_string()),
213 | subject: None,
214 | };
215 |
216 | assert!(rule.validate(&message).is_none());
217 | }
218 |
219 | #[test]
220 | fn test_invalid_scope() {
221 | let rule = Scope {
222 | options: vec!["api".to_string(), "web".to_string()],
223 | ..Default::default()
224 | };
225 |
226 | let message = Message {
227 | body: None,
228 | description: None,
229 | footers: None,
230 | r#type: Some("feat".to_string()),
231 | raw: "feat(invalid): broadcast $destroy event on scope destruction".to_string(),
232 | scope: Some("invalid".to_string()),
233 | subject: None,
234 | };
235 |
236 | let violation = rule.validate(&message);
237 | assert!(violation.is_some());
238 | assert_eq!(violation.clone().unwrap().level, Level::Error);
239 | assert_eq!(
240 | violation.unwrap().message,
241 | "scope invalid is not allowed. Only [\"api\", \"web\"] are allowed".to_string()
242 | );
243 | }
244 |
245 | #[test]
246 | fn test_optional_scope_with_non_empty_scope() {
247 | let rule = Scope {
248 | options: vec!["api".to_string(), "web".to_string()],
249 | optional: true,
250 | ..Default::default()
251 | };
252 |
253 | let message = Message {
254 | body: None,
255 | description: None,
256 | footers: None,
257 | r#type: Some("feat".to_string()),
258 | raw: "feat(invalid): broadcast $destroy event on scope destruction".to_string(),
259 | scope: Some("invalid".to_string()),
260 | subject: None,
261 | };
262 |
263 | let violation = rule.validate(&message);
264 | assert!(violation.is_some());
265 | assert_eq!(violation.clone().unwrap().level, Level::Error);
266 | assert_eq!(
267 | violation.unwrap().message,
268 | "scope invalid is not allowed. Only [\"api\", \"web\"] are allowed".to_string()
269 | );
270 | }
271 |
272 | #[test]
273 | fn test_optional_scope_with_empty_scope() {
274 | let rule = Scope {
275 | options: vec!["api".to_string(), "web".to_string()],
276 | optional: true,
277 | ..Default::default()
278 | };
279 |
280 | let message = Message {
281 | body: None,
282 | description: None,
283 | footers: None,
284 | r#type: Some("feat".to_string()),
285 | raw: "feat: broadcast $destroy event on scope destruction".to_string(),
286 | scope: None,
287 | subject: None,
288 | };
289 |
290 | let violation = rule.validate(&message);
291 | assert!(violation.is_none());
292 | }
293 | }
294 | }
295 |
--------------------------------------------------------------------------------
/cli/src/rule/scope_empty.rs:
--------------------------------------------------------------------------------
1 | use crate::{message::Message, result::Violation, rule::Rule};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Level;
5 |
6 | /// ScopeEmpty represents the subject-empty rule.
7 | #[derive(Clone, Debug, Deserialize, Serialize)]
8 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9 | pub struct ScopeEmpty {
10 | /// Level represents the level of the rule.
11 | ///
12 | // Note that currently the default literal is not supported.
13 | // See: https://github.com/serde-rs/serde/issues/368
14 | level: Option,
15 | }
16 |
17 | /// ScopeEmpty represents the scope-empty rule.
18 | impl Rule for ScopeEmpty {
19 | const NAME: &'static str = "scope-empty";
20 | const LEVEL: Level = Level::Error;
21 |
22 | fn message(&self, _message: &Message) -> String {
23 | "scope is empty".to_string()
24 | }
25 |
26 | fn validate(&self, message: &Message) -> Option {
27 | if message.scope.is_none() {
28 | return Some(Violation {
29 | level: self.level.unwrap_or(Self::LEVEL),
30 | message: self.message(message),
31 | });
32 | }
33 |
34 | None
35 | }
36 | }
37 |
38 | /// Default implementation of ScopeEmpty.
39 | impl Default for ScopeEmpty {
40 | fn default() -> Self {
41 | Self {
42 | level: Some(Self::LEVEL),
43 | }
44 | }
45 | }
46 |
47 | #[cfg(test)]
48 | mod tests {
49 | use super::*;
50 |
51 | #[test]
52 | fn test_non_empty_subject() {
53 | let rule = ScopeEmpty::default();
54 | let message = Message {
55 | body: None,
56 | description: None,
57 | footers: None,
58 | r#type: Some("feat".to_string()),
59 | raw: "feat(scope): broadcast $destroy event on scope destruction".to_string(),
60 | scope: Some("scope".to_string()),
61 | subject: None,
62 | };
63 |
64 | assert!(rule.validate(&message).is_none());
65 | }
66 |
67 | #[test]
68 | fn test_no_subject() {
69 | let rule = ScopeEmpty::default();
70 | let message = Message {
71 | body: None,
72 | description: None,
73 | footers: None,
74 | r#type: Some("feat".to_string()),
75 | raw: "feat: broadcast $destroy event on scope destruction".to_string(),
76 | scope: None,
77 | subject: None,
78 | };
79 |
80 | let violation = rule.validate(&message);
81 | assert!(violation.is_some());
82 | assert_eq!(violation.clone().unwrap().level, Level::Error);
83 | assert_eq!(violation.unwrap().message, "scope is empty".to_string());
84 | }
85 |
86 | #[test]
87 | fn test_empty_subject() {
88 | let rule = ScopeEmpty::default();
89 | let message = Message {
90 | body: None,
91 | description: None,
92 | footers: None,
93 | r#type: Some("feat".to_string()),
94 | raw: "feat(): broadcast $destroy event on scope destruction".to_string(),
95 | scope: None,
96 | subject: None,
97 | };
98 |
99 | let violation = rule.validate(&message);
100 | assert!(violation.is_some());
101 | assert_eq!(violation.clone().unwrap().level, Level::Error);
102 | assert_eq!(violation.unwrap().message, "scope is empty".to_string());
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/cli/src/rule/scope_format.rs:
--------------------------------------------------------------------------------
1 | use crate::{message::Message, result::Violation, rule::Rule};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Level;
5 |
6 | /// ScopeFormat represents the scope-format rule.
7 | #[derive(Clone, Debug, Deserialize, Serialize)]
8 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9 | pub struct ScopeFormat {
10 | /// Level represents the level of the rule.
11 | ///
12 | // Note that currently the default literal is not supported.
13 | // See: https://github.com/serde-rs/serde/issues/368
14 | level: Option,
15 |
16 | /// Format represents the format of the scope.
17 | format: Option,
18 | }
19 |
20 | /// ScopeFormat represents the scope-format rule.
21 | impl Rule for ScopeFormat {
22 | const NAME: &'static str = "scope-format";
23 | const LEVEL: Level = Level::Error;
24 |
25 | fn message(&self, _message: &Message) -> String {
26 | format!(
27 | "scope format does not match format: {}",
28 | self.format.as_ref().unwrap()
29 | )
30 | }
31 |
32 | fn validate(&self, message: &Message) -> Option {
33 | if let Some(format) = &self.format {
34 | let regex = match regex::Regex::new(format) {
35 | Ok(regex) => regex,
36 | Err(err) => {
37 | return Some(Violation {
38 | level: self.level.unwrap_or(Self::LEVEL),
39 | message: err.to_string(),
40 | });
41 | }
42 | };
43 |
44 | match &message.scope {
45 | None => {
46 | return Some(Violation {
47 | level: self.level.unwrap_or(Self::LEVEL),
48 | message: "found no scope".to_string(),
49 | });
50 | }
51 | Some(description) => {
52 | if !regex.is_match(description) {
53 | return Some(Violation {
54 | level: self.level.unwrap_or(Self::LEVEL),
55 | message: self.message(message),
56 | });
57 | }
58 | }
59 | }
60 | }
61 |
62 | None
63 | }
64 | }
65 |
66 | /// Default implementation of ScopeFormat.
67 | impl Default for ScopeFormat {
68 | fn default() -> Self {
69 | Self {
70 | level: Some(Self::LEVEL),
71 | format: None,
72 | }
73 | }
74 | }
75 |
76 | #[cfg(test)]
77 | mod tests {
78 | use super::*;
79 |
80 | #[test]
81 | fn test_invalid_description_format() {
82 | let rule = ScopeFormat {
83 | format: Some(r"^[a-z].*".to_string()),
84 | ..Default::default()
85 | };
86 |
87 | let message = Message {
88 | body: None,
89 | description: Some("Add new flag".to_string()),
90 | footers: None,
91 | r#type: Some("feat".to_string()),
92 | raw: "feat(scope): Add new flag".to_string(),
93 | scope: Some("scope".to_string()),
94 | subject: None,
95 | };
96 |
97 | assert!(rule.validate(&message).is_none());
98 | }
99 |
100 | #[test]
101 | fn test_valid_description_format() {
102 | let rule = ScopeFormat {
103 | format: Some(r"^[a-z].*".to_string()),
104 | ..Default::default()
105 | };
106 |
107 | let message = Message {
108 | body: None,
109 | description: Some("Add new flag".to_string()),
110 | footers: None,
111 | r#type: Some("feat".to_string()),
112 | raw: "feat(Scope): Add new flag".to_string(),
113 | scope: Some("Scope".to_string()),
114 | subject: None,
115 | };
116 |
117 | let violation = rule.validate(&message);
118 | assert!(violation.is_some());
119 | assert_eq!(violation.clone().unwrap().level, Level::Error);
120 | assert_eq!(
121 | violation.unwrap().message,
122 | "scope format does not match format: ^[a-z].*".to_string()
123 | );
124 | }
125 |
126 | #[test]
127 | fn test_invalid_regex() {
128 | let rule = ScopeFormat {
129 | format: Some(r"(".to_string()),
130 | ..Default::default()
131 | };
132 |
133 | let message = Message {
134 | body: None,
135 | description: Some("Add regex".to_string()),
136 | footers: None,
137 | r#type: Some("feat".to_string()),
138 | raw: "feat(scope): Add regex".to_string(),
139 | scope: Some("scope".to_string()),
140 | subject: None,
141 | };
142 |
143 | let violation = rule.validate(&message);
144 | assert!(violation.is_some());
145 | assert_eq!(violation.clone().unwrap().level, Level::Error);
146 | assert!(violation.unwrap().message.contains("regex parse error"));
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/cli/src/rule/scope_max_length.rs:
--------------------------------------------------------------------------------
1 | use crate::{message::Message, result::Violation, rule::Rule};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Level;
5 |
6 | /// ScopeMaxLength represents the description-max-length rule.
7 | #[derive(Clone, Debug, Deserialize, Serialize)]
8 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9 | pub struct ScopeMaxLength {
10 | /// Level represents the level of the rule.
11 | ///
12 | // Note that currently the default literal is not supported.
13 | // See: https://github.com/serde-rs/serde/issues/368
14 | level: Option,
15 |
16 | /// Length represents the maximum length of the scope.
17 | length: usize,
18 | }
19 |
20 | /// ScopeMaxLength represents the scope-max-length rule.
21 | impl Rule for ScopeMaxLength {
22 | const NAME: &'static str = "scope-max-length";
23 | const LEVEL: Level = Level::Error;
24 |
25 | fn message(&self, _message: &Message) -> String {
26 | format!("scope is longer than {} characters", self.length)
27 | }
28 |
29 | fn validate(&self, message: &Message) -> Option {
30 | match &message.scope {
31 | Some(scope) => {
32 | if scope.len() >= self.length {
33 | return Some(Violation {
34 | level: self.level.unwrap_or(Self::LEVEL),
35 | message: self.message(message),
36 | });
37 | }
38 | }
39 | None => {
40 | return Some(Violation {
41 | level: self.level.unwrap_or(Self::LEVEL),
42 | message: self.message(message),
43 | })
44 | }
45 | }
46 |
47 | None
48 | }
49 | }
50 |
51 | /// Default implementation of ScopeMaxLength.
52 | impl Default for ScopeMaxLength {
53 | fn default() -> Self {
54 | Self {
55 | level: Some(Self::LEVEL),
56 | length: 72,
57 | }
58 | }
59 | }
60 |
61 | #[cfg(test)]
62 | mod tests {
63 | use super::*;
64 |
65 | #[test]
66 | fn test_long_scope() {
67 | let rule = ScopeMaxLength {
68 | length: usize::MAX, // Long length for testing
69 | ..Default::default()
70 | };
71 | let message = Message {
72 | body: None,
73 | description: Some("desc".to_string()),
74 | footers: None,
75 | r#type: Some("feat".to_string()),
76 | raw: "feat(scope): desc".to_string(),
77 | scope: Some("scope".to_string()),
78 | subject: Some("feat(scope): desc".to_string()),
79 | };
80 |
81 | assert!(rule.validate(&message).is_none());
82 | }
83 |
84 | #[test]
85 | fn test_short_scope() {
86 | let rule = ScopeMaxLength {
87 | length: 3, // Short length for testing
88 | ..Default::default()
89 | };
90 | let message = Message {
91 | body: None,
92 | description: Some("feat(scope): I'm long description".to_string()),
93 | footers: None,
94 | r#type: Some("feat".to_string()),
95 | raw: "feat(scope): I'm long description".to_string(),
96 | scope: Some("scope".to_string()),
97 | subject: None,
98 | };
99 |
100 | let violation = rule.validate(&message);
101 | assert!(violation.is_some());
102 | assert_eq!(violation.clone().unwrap().level, Level::Error);
103 | assert_eq!(
104 | violation.unwrap().message,
105 | format!("scope is longer than {} characters", rule.length)
106 | );
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/cli/src/rule/subject_empty.rs:
--------------------------------------------------------------------------------
1 | use crate::{message::Message, result::Violation, rule::Rule};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Level;
5 |
6 | /// SubjectEmpty represents the subject-empty rule.
7 | #[derive(Clone, Debug, Deserialize, Serialize)]
8 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9 | pub struct SubjectEmpty {
10 | /// Level represents the level of the rule.
11 | ///
12 | // Note that currently the default literal is not supported.
13 | // See: https://github.com/serde-rs/serde/issues/368
14 | level: Option,
15 | }
16 |
17 | /// SubjectEmpty represents the subject-empty rule.
18 | impl Rule for SubjectEmpty {
19 | const NAME: &'static str = "subject-empty";
20 | const LEVEL: Level = Level::Error;
21 |
22 | fn message(&self, _message: &Message) -> String {
23 | "subject is empty".to_string()
24 | }
25 |
26 | fn validate(&self, message: &Message) -> Option {
27 | if message.subject.is_none() {
28 | return Some(Violation {
29 | level: self.level.unwrap_or(Self::LEVEL),
30 | message: self.message(message),
31 | });
32 | }
33 |
34 | None
35 | }
36 | }
37 |
38 | /// Default implementation of SubjectEmpty.
39 | impl Default for SubjectEmpty {
40 | fn default() -> Self {
41 | Self {
42 | level: Some(Self::LEVEL),
43 | }
44 | }
45 | }
46 |
47 | #[cfg(test)]
48 | mod tests {
49 | use super::*;
50 |
51 | #[test]
52 | fn test_non_empty_subject() {
53 | let rule = SubjectEmpty::default();
54 | let message = Message {
55 | body: None,
56 | description: Some("broadcast $destroy event on scope destruction".to_string()),
57 | footers: None,
58 | r#type: Some("feat".to_string()),
59 | raw: "feat(scope): broadcast $destroy event on scope destruction
60 |
61 | Hello world"
62 | .to_string(),
63 | scope: Some("scope".to_string()),
64 | subject: Some("feat(scope): broadcast $destroy event on scope destruction".to_string()),
65 | };
66 |
67 | assert!(rule.validate(&message).is_none());
68 | }
69 |
70 | #[test]
71 | fn test_empty_description() {
72 | let rule = SubjectEmpty::default();
73 | let message = Message {
74 | body: None,
75 | description: None,
76 | footers: None,
77 | r#type: Some("feat".to_string()),
78 | raw: "
79 |
80 | Hello world"
81 | .to_string(),
82 | scope: Some("scope".to_string()),
83 | subject: None,
84 | };
85 |
86 | let violation = rule.validate(&message);
87 | assert!(violation.is_some());
88 | assert_eq!(violation.clone().unwrap().level, Level::Error);
89 | assert_eq!(violation.unwrap().message, "subject is empty".to_string());
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/cli/src/rule/type.rs:
--------------------------------------------------------------------------------
1 | use crate::{message::Message, result::Violation, rule::Rule};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Level;
5 |
6 | /// Type represents the subject-empty rule.
7 | #[derive(Clone, Debug, Deserialize, Serialize)]
8 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9 | pub struct Type {
10 | /// Level represents the level of the rule.
11 | ///
12 | // Note that currently the default literal is not supported.
13 | // See: https://github.com/serde-rs/serde/issues/368
14 | level: Option,
15 |
16 | /// Options represents the options of the rule.
17 | /// If the option is empty, it means that no Type is allowed.
18 | options: Vec,
19 | }
20 |
21 | /// Type represents the type rule.
22 | impl Rule for Type {
23 | const NAME: &'static str = "type";
24 | const LEVEL: Level = Level::Error;
25 | fn message(&self, message: &Message) -> String {
26 | if self.options.is_empty() {
27 | return "types are not allowed".to_string();
28 | }
29 |
30 | format!(
31 | "type {} is not allowed. Only {:?} are allowed",
32 | message.r#type.as_ref().unwrap_or(&"".to_string()),
33 | self.options
34 | )
35 | }
36 |
37 | fn validate(&self, message: &Message) -> Option {
38 | match &message.r#type {
39 | None => {
40 | if self.options.is_empty() {
41 | return None;
42 | }
43 | }
44 | Some(r#type) if r#type.is_empty() => {
45 | if self.options.is_empty() {
46 | return None;
47 | }
48 | }
49 | Some(r#type) if self.options.contains(r#type) => {
50 | return None;
51 | }
52 | _ => {}
53 | }
54 |
55 | Some(Violation {
56 | level: self.level.unwrap_or(Self::LEVEL),
57 | message: self.message(message),
58 | })
59 | }
60 | }
61 |
62 | /// Default implementation of Type.
63 | impl Default for Type {
64 | fn default() -> Self {
65 | Self {
66 | level: Some(Self::LEVEL),
67 | options: vec![],
68 | }
69 | }
70 | }
71 |
72 | #[cfg(test)]
73 | mod tests {
74 | use super::*;
75 |
76 | mod empty_options {
77 | use super::*;
78 |
79 | #[test]
80 | fn test_empty_type() {
81 | let rule = Type::default();
82 |
83 | let message = Message {
84 | body: None,
85 | description: None,
86 | footers: None,
87 | r#type: None,
88 | raw: "".to_string(),
89 | scope: Some("".to_string()),
90 | subject: None,
91 | };
92 |
93 | let violation = rule.validate(&message);
94 | assert!(violation.is_none());
95 | }
96 |
97 | #[test]
98 | fn test_none_type() {
99 | let rule = Type::default();
100 |
101 | let message = Message {
102 | body: None,
103 | description: None,
104 | footers: None,
105 | r#type: None,
106 | raw: "".to_string(),
107 | scope: None,
108 | subject: None,
109 | };
110 |
111 | let violation = rule.validate(&message);
112 | assert!(violation.is_none());
113 | }
114 |
115 | #[test]
116 | fn test_type() {
117 | let rule = Type::default();
118 |
119 | let message = Message {
120 | body: None,
121 | description: None,
122 | footers: None,
123 | r#type: Some("feat".to_string()),
124 | raw: "feat(web): broadcast $destroy event on scope destruction".to_string(),
125 | scope: Some("web".to_string()),
126 | subject: None,
127 | };
128 |
129 | let violation = rule.validate(&message);
130 | assert!(violation.is_some());
131 | assert_eq!(violation.clone().unwrap().level, Level::Error);
132 | assert_eq!(
133 | violation.unwrap().message,
134 | "types are not allowed".to_string()
135 | );
136 | }
137 | }
138 |
139 | mod scopes {
140 | use super::*;
141 | #[test]
142 | fn test_empty_type() {
143 | let rule = Type {
144 | options: vec!["feat".to_string(), "chore".to_string()],
145 | ..Default::default()
146 | };
147 |
148 | let message = Message {
149 | body: None,
150 | description: None,
151 | footers: None,
152 | r#type: None,
153 | raw: "".to_string(),
154 | scope: Some("".to_string()),
155 | subject: None,
156 | };
157 |
158 | let violation = rule.validate(&message);
159 | assert!(violation.is_some());
160 | assert_eq!(violation.clone().unwrap().level, Level::Error);
161 | assert_eq!(
162 | violation.unwrap().message,
163 | "type is not allowed. Only [\"feat\", \"chore\"] are allowed"
164 | );
165 | }
166 |
167 | #[test]
168 | fn test_none_type() {
169 | let rule = Type {
170 | options: vec!["feat".to_string(), "chore".to_string()],
171 | ..Default::default()
172 | };
173 |
174 | let message = Message {
175 | body: None,
176 | description: None,
177 | footers: None,
178 | r#type: None,
179 | raw: "".to_string(),
180 | scope: None,
181 | subject: None,
182 | };
183 |
184 | let violation = rule.validate(&message);
185 | assert!(violation.is_some());
186 | assert_eq!(violation.clone().unwrap().level, Level::Error);
187 | assert_eq!(
188 | violation.unwrap().message,
189 | "type is not allowed. Only [\"feat\", \"chore\"] are allowed".to_string()
190 | );
191 | }
192 |
193 | #[test]
194 | fn test_valid_type() {
195 | let rule = Type {
196 | options: vec!["feat".to_string(), "chore".to_string()],
197 | ..Default::default()
198 | };
199 |
200 | let message = Message {
201 | body: None,
202 | description: None,
203 | footers: None,
204 | r#type: Some("feat".to_string()),
205 | raw: "feat(web): broadcast $destroy event on scope destruction".to_string(),
206 | scope: Some("web".to_string()),
207 | subject: None,
208 | };
209 |
210 | assert!(rule.validate(&message).is_none());
211 | }
212 |
213 | #[test]
214 | fn test_invalid_type() {
215 | let rule = Type {
216 | options: vec!["feat".to_string(), "chore".to_string()],
217 | ..Default::default()
218 | };
219 |
220 | let message = Message {
221 | body: None,
222 | description: None,
223 | footers: None,
224 | r#type: Some("invalid".to_string()),
225 | raw: "invalid(web): broadcast $destroy event on scope destruction".to_string(),
226 | scope: Some("web".to_string()),
227 | subject: None,
228 | };
229 |
230 | let violation = rule.validate(&message);
231 | assert!(violation.is_some());
232 | assert_eq!(violation.clone().unwrap().level, Level::Error);
233 | assert_eq!(
234 | violation.unwrap().message,
235 | "type invalid is not allowed. Only [\"feat\", \"chore\"] are allowed".to_string()
236 | );
237 | }
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/cli/src/rule/type_empty.rs:
--------------------------------------------------------------------------------
1 | use crate::{message::Message, result::Violation, rule::Rule};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Level;
5 |
6 | /// TypeEmpty represents the type-empty rule.
7 | #[derive(Clone, Debug, Deserialize, Serialize)]
8 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9 | pub struct TypeEmpty {
10 | /// Level represents the level of the rule.
11 | ///
12 | // Note that currently the default literal is not supported.
13 | // See: https://github.com/serde-rs/serde/issues/368
14 | level: Option,
15 | }
16 |
17 | /// TypeEmpty represents the type-empty rule.
18 | impl Rule for TypeEmpty {
19 | const NAME: &'static str = "type-empty";
20 | const LEVEL: Level = Level::Error;
21 |
22 | fn message(&self, _message: &Message) -> String {
23 | "type is empty".to_string()
24 | }
25 |
26 | fn validate(&self, message: &Message) -> Option {
27 | if message.r#type.is_none() || message.r#type.as_ref().unwrap().is_empty() {
28 | return Some(Violation {
29 | level: self.level.unwrap_or(Self::LEVEL),
30 | message: self.message(message),
31 | });
32 | }
33 |
34 | None
35 | }
36 | }
37 |
38 | /// Default implementation of TypeEmpty.
39 | impl Default for TypeEmpty {
40 | fn default() -> Self {
41 | Self {
42 | level: Some(Self::LEVEL),
43 | }
44 | }
45 | }
46 |
47 | #[cfg(test)]
48 | mod tests {
49 | use super::*;
50 |
51 | #[test]
52 | fn test_non_empty_type() {
53 | let rule = TypeEmpty::default();
54 | let message = Message {
55 | body: None,
56 | description: None,
57 | footers: None,
58 | r#type: Some("feat".to_string()),
59 | raw: "feat(scope): broadcast $destroy event on scope destruction".to_string(),
60 | scope: None,
61 | subject: None,
62 | };
63 |
64 | assert!(rule.validate(&message).is_none());
65 | }
66 |
67 | #[test]
68 | fn test_empty_type() {
69 | let rule = TypeEmpty::default();
70 | let message = Message {
71 | body: None,
72 | description: None,
73 | footers: None,
74 | r#type: None,
75 | raw: "(scope): broadcast $destroy event on scope destruction".to_string(),
76 | scope: None,
77 | subject: None,
78 | };
79 |
80 | let violation = rule.validate(&message);
81 | assert!(violation.is_some());
82 | assert_eq!(violation.clone().unwrap().level, Level::Error);
83 | assert_eq!(violation.unwrap().message, "type is empty".to_string());
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/cli/src/rule/type_format.rs:
--------------------------------------------------------------------------------
1 | use crate::{message::Message, result::Violation, rule::Rule};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Level;
5 |
6 | /// TypeFormat represents the type-format rule.
7 | #[derive(Clone, Debug, Deserialize, Serialize)]
8 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9 | pub struct TypeFormat {
10 | /// Level represents the level of the rule.
11 | ///
12 | // Note that currently the default literal is not supported.
13 | // See: https://github.com/serde-rs/serde/issues/368
14 | level: Option,
15 |
16 | /// Format represents the format of the type.
17 | format: Option,
18 | }
19 |
20 | /// TypeFormat represents the type-format rule.
21 | impl Rule for TypeFormat {
22 | const NAME: &'static str = "type-format";
23 | const LEVEL: Level = Level::Error;
24 |
25 | fn message(&self, _message: &Message) -> String {
26 | format!(
27 | "type format does not match format: {}",
28 | self.format.as_ref().unwrap()
29 | )
30 | }
31 |
32 | fn validate(&self, message: &Message) -> Option {
33 | if let Some(format) = &self.format {
34 | let regex = match regex::Regex::new(format) {
35 | Ok(regex) => regex,
36 | Err(err) => {
37 | return Some(Violation {
38 | level: self.level.unwrap_or(Self::LEVEL),
39 | message: err.to_string(),
40 | });
41 | }
42 | };
43 |
44 | match &message.r#type {
45 | None => {
46 | return Some(Violation {
47 | level: self.level.unwrap_or(Self::LEVEL),
48 | message: "found no type".to_string(),
49 | });
50 | }
51 | Some(description) => {
52 | if !regex.is_match(description) {
53 | return Some(Violation {
54 | level: self.level.unwrap_or(Self::LEVEL),
55 | message: self.message(message),
56 | });
57 | }
58 | }
59 | }
60 | }
61 |
62 | None
63 | }
64 | }
65 |
66 | /// Default implementation of TypeFormat.
67 | impl Default for TypeFormat {
68 | fn default() -> Self {
69 | Self {
70 | level: Some(Self::LEVEL),
71 | format: None,
72 | }
73 | }
74 | }
75 |
76 | #[cfg(test)]
77 | mod tests {
78 | use super::*;
79 |
80 | #[test]
81 | fn test_invalid_description_format() {
82 | let rule = TypeFormat {
83 | format: Some(r"^[a-z].*".to_string()),
84 | ..Default::default()
85 | };
86 |
87 | let message = Message {
88 | body: None,
89 | description: Some("Add new flag".to_string()),
90 | footers: None,
91 | r#type: Some("feat".to_string()),
92 | raw: "feat(scope): Add new flag".to_string(),
93 | scope: Some("scope".to_string()),
94 | subject: None,
95 | };
96 |
97 | assert!(rule.validate(&message).is_none());
98 | }
99 |
100 | #[test]
101 | fn test_valid_description_format() {
102 | let rule = TypeFormat {
103 | format: Some(r"^[a-z].*".to_string()),
104 | ..Default::default()
105 | };
106 |
107 | let message = Message {
108 | body: None,
109 | description: Some("Add new flag".to_string()),
110 | footers: None,
111 | r#type: Some("Feat".to_string()),
112 | raw: "Feat(scope): Add new flag".to_string(),
113 | scope: Some("Scope".to_string()),
114 | subject: None,
115 | };
116 |
117 | let violation = rule.validate(&message);
118 | assert!(violation.is_some());
119 | assert_eq!(violation.clone().unwrap().level, Level::Error);
120 | assert_eq!(
121 | violation.unwrap().message,
122 | "type format does not match format: ^[a-z].*".to_string()
123 | );
124 | }
125 |
126 | #[test]
127 | fn test_invalid_regex() {
128 | let rule = TypeFormat {
129 | format: Some(r"(".to_string()),
130 | ..Default::default()
131 | };
132 |
133 | let message = Message {
134 | body: None,
135 | description: Some("Invalid regex".to_string()),
136 | footers: None,
137 | r#type: Some("feat".to_string()),
138 | raw: "feat(scope): Invalid regex".to_string(),
139 | scope: Some("scope".to_string()),
140 | subject: None,
141 | };
142 |
143 | let violation = rule.validate(&message);
144 | assert!(violation.is_some());
145 | assert_eq!(violation.clone().unwrap().level, Level::Error);
146 | assert!(violation.unwrap().message.contains("regex parse error"));
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/cli/src/rule/type_max_length.rs:
--------------------------------------------------------------------------------
1 | use crate::{message::Message, result::Violation, rule::Rule};
2 | use serde::{Deserialize, Serialize};
3 |
4 | use super::Level;
5 |
6 | /// TypeMaxLength represents the description-max-length rule.
7 | #[derive(Clone, Debug, Deserialize, Serialize)]
8 | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
9 | pub struct TypeMaxLength {
10 | /// Level represents the level of the rule.
11 | ///
12 | // Note that currently the default literal is not supported.
13 | // See: https://github.com/serde-rs/serde/issues/368
14 | level: Option,
15 |
16 | /// Length represents the maximum length of the type.
17 | length: usize,
18 | }
19 |
20 | /// TypeMaxLength represents the type-max-length rule.
21 | impl Rule for TypeMaxLength {
22 | const NAME: &'static str = "type-max-length";
23 | const LEVEL: Level = Level::Error;
24 |
25 | fn message(&self, _message: &Message) -> String {
26 | format!("type is longer than {} characters", self.length)
27 | }
28 |
29 | fn validate(&self, message: &Message) -> Option {
30 | match &message.r#type {
31 | Some(t) => {
32 | if t.len() >= self.length {
33 | return Some(Violation {
34 | level: self.level.unwrap_or(Self::LEVEL),
35 | message: self.message(message),
36 | });
37 | }
38 | }
39 | None => {
40 | return Some(Violation {
41 | level: self.level.unwrap_or(Self::LEVEL),
42 | message: self.message(message),
43 | })
44 | }
45 | }
46 |
47 | None
48 | }
49 | }
50 |
51 | /// Default implementation of TypeMaxLength.
52 | impl Default for TypeMaxLength {
53 | fn default() -> Self {
54 | Self {
55 | level: Some(Self::LEVEL),
56 | length: 72,
57 | }
58 | }
59 | }
60 |
61 | #[cfg(test)]
62 | mod tests {
63 | use super::*;
64 |
65 | #[test]
66 | fn test_long_type() {
67 | let rule = TypeMaxLength {
68 | length: usize::MAX, // Long length for testing
69 | ..Default::default()
70 | };
71 | let message = Message {
72 | body: None,
73 | description: Some("desc".to_string()),
74 | footers: None,
75 | r#type: Some("feat".to_string()),
76 | raw: "feat(scope): desc".to_string(),
77 | scope: Some("scope".to_string()),
78 | subject: Some("feat(scope): desc".to_string()),
79 | };
80 |
81 | assert!(rule.validate(&message).is_none());
82 | }
83 |
84 | #[test]
85 | fn test_short_type() {
86 | let rule = TypeMaxLength {
87 | length: 3, // Short length for testing
88 | ..Default::default()
89 | };
90 | let message = Message {
91 | body: None,
92 | description: Some("feat(scope): I'm long description".to_string()),
93 | footers: None,
94 | r#type: Some("feat".to_string()),
95 | raw: "feat(scope): I'm long description".to_string(),
96 | scope: Some("scope".to_string()),
97 | subject: None,
98 | };
99 |
100 | let violation = rule.validate(&message);
101 | assert!(violation.is_some());
102 | assert_eq!(violation.clone().unwrap().level, Level::Error);
103 | assert_eq!(
104 | violation.unwrap().message,
105 | format!("type is longer than {} characters", rule.length)
106 | );
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/schema/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "schema"
3 | version.workspace = true
4 | authors.workspace = true
5 | license.workspace = true
6 | documentation.workspace = true
7 | keywords.workspace = true
8 | categories.workspace = true
9 | readme.workspace = true
10 | repository.workspace = true
11 | exclude.workspace = true
12 | edition.workspace = true
13 |
14 | [dependencies]
15 | clap = { version = "4.5.21", features = ["derive"] }
16 | cli = { path = "../cli", features = ["schemars"], package = "commitlint-rs" }
17 | schemars = { version = "0.8.21" }
18 | serde = { version = "1.0.215", features = ["derive"] }
19 | serde_json = "1.0.140"
20 |
--------------------------------------------------------------------------------
/schema/src/main.rs:
--------------------------------------------------------------------------------
1 | use clap::Parser;
2 | use cli::config::Config;
3 | use std::fs;
4 |
5 | /// CLI Arguments
6 | #[derive(Parser, Debug)]
7 | #[command(author, version, about, long_about = None)]
8 | struct Args {
9 | /// Path to save the JSON schema
10 | #[arg(long, short)]
11 | path: String,
12 | }
13 |
14 | fn main() {
15 | let args = Args::parse();
16 |
17 | let config_schema = schemars::schema_for!(Config);
18 | let config_schema_json = serde_json::to_string_pretty(&config_schema).unwrap();
19 | fs::write(&args.path, config_schema_json).unwrap();
20 | }
21 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | # build output
2 | dist/
3 | # generated types
4 | .astro/
5 |
6 | # dependencies
7 | node_modules/
8 |
9 | # logs
10 | npm-debug.log*
11 | yarn-debug.log*
12 | yarn-error.log*
13 | pnpm-debug.log*
14 |
15 |
16 | # environment variables
17 | .env
18 | .env.production
19 |
20 | # macOS-specific files
21 | .DS_Store
22 |
--------------------------------------------------------------------------------
/web/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["astro-build.astro-vscode"],
3 | "unwantedRecommendations": []
4 | }
5 |
--------------------------------------------------------------------------------
/web/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "command": "./node_modules/.bin/astro dev",
6 | "name": "Development server",
7 | "request": "launch",
8 | "type": "node-terminal"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/web/astro.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'astro/config';
2 | import starlight from '@astrojs/starlight';
3 |
4 | // https://astro.build/config
5 | export default defineConfig({
6 | site: 'https://keisukeyamashita.github.io',
7 | base: '/commitlint-rs',
8 | integrations: [
9 | starlight({
10 | title: 'Commitlint',
11 | logo: {
12 | src: '/src/assets/checker.png',
13 | },
14 | social: {
15 | github: 'https://github.com/KeisukeYamashita/commitlint-rs',
16 | },
17 | sidebar: [
18 | {
19 | label: '🚀 Get Started',
20 | autogenerate: { directory: 'setup' },
21 | },
22 | {
23 | label: '🔧 Configuration',
24 | autogenerate: { directory: 'config' },
25 | },
26 | {
27 | label: '✅ Rule',
28 | autogenerate: { directory: 'rules' },
29 | },
30 | {
31 | label: '🔐 License',
32 | items: [
33 | { label: "License", link: "/license" },
34 | ]
35 | },
36 | ],
37 | }),
38 | ],
39 |
40 | // Process images with sharp: https://docs.astro.build/en/guides/assets/#using-sharp
41 | image: { service: { entrypoint: 'astro/assets/services/sharp' } },
42 | });
43 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "type": "module",
4 | "version": "0.1.1",
5 | "scripts": {
6 | "dev": "astro dev",
7 | "start": "astro dev",
8 | "build": "astro build",
9 | "preview": "astro preview",
10 | "astro": "astro"
11 | },
12 | "dependencies": {
13 | "@astrojs/starlight": "^0.25.2",
14 | "astro": "^4.16.18",
15 | "sharp": "^0.33.4"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/web/public/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
63 |
--------------------------------------------------------------------------------
/web/src/assets/checker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KeisukeYamashita/commitlint-rs/efaf9035ae78ef0fefe82ea3ffd3b40c1b508cd1/web/src/assets/checker.png
--------------------------------------------------------------------------------
/web/src/content/config.ts:
--------------------------------------------------------------------------------
1 | import { defineCollection } from 'astro:content';
2 | import { docsSchema, i18nSchema } from '@astrojs/starlight/schema';
3 |
4 | export const collections = {
5 | docs: defineCollection({ schema: docsSchema() }),
6 | i18n: defineCollection({ type: 'data', schema: i18nSchema() }),
7 | };
8 |
--------------------------------------------------------------------------------
/web/src/content/docs/config/IDE.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: IDE
3 | description: Guide how to setup your IDE to work with commitlint
4 | ---
5 |
6 | Commitlint offers schema by supporting [JSON schema](https://json-schema.org/) so that you can configure your IDE to work with Commitlint and have better developer experience.
7 |
8 | :::tip
9 |
10 | If you want to pin the schema to a specific version, you can configure the version in the URL.
11 |
12 | ```console
13 | https://github.com/KeisukeYamashita/commitlint-rs/releases/download/v0.2.0/schema.json
14 | ```
15 |
16 | In this case, the schema is pinned to version `0.2.0`.
17 |
18 | :::
19 |
20 | ## Visual Studio Code
21 |
22 | Configure your [Visual Studio Code](https://code.visualstudio.com/) to work with Commitlint.
23 |
24 | ### Edit in `settings.json`
25 |
26 | Update your user `settings.json` or workspace settings (`/.vscode/settings.json`) to configure the schema.
27 |
28 | #### JSON
29 |
30 | ```json
31 | "json.schemas": [
32 | {
33 | "fileMatch": [
34 | ".commitlintrc",
35 | ".commitlintrc.json"
36 | ],
37 | "url": "https://github.com/KeisukeYamashita/commitlint-rs/releases/latest/download/schema.json"
38 | }
39 | ]
40 | ```
41 |
42 | #### YAML
43 |
44 | Associating schemas with YAMLs are supported by the [YAML language server](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml).
45 |
46 | ```json
47 | "yaml.schemas": {
48 | "https://github.com/KeisukeYamashita/commitlint-rs/releases/latest/download/schema.json": [
49 | ".commitlintrc",
50 | ".commitlintrc.yaml",
51 | ".commitlint.yml"
52 | ]
53 | }
54 | ```
55 |
56 | ### Specify schema in the configuration file
57 |
58 | You can also specify the schema in the configuration file directly.
59 |
60 | #### JSON
61 |
62 | Add the following comment in your `.commitlintrc` or `.commitlintrc.json` file.
63 |
64 | ```json
65 | {
66 | "$schema": "https://github.com/KeisukeYamashita/commitlint-rs/releases/latest/download/schema.json",
67 | "rules": {}
68 | }
69 | ```
70 |
71 | #### YAML
72 |
73 | Associating schemas with YAMLs are supported by the [YAML language server](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml). Add the following comment in your `.commitlintrc`, `.commitlintrc.yaml` or `.commitlintrc.yml` file.
74 |
75 | ```yaml
76 | # yaml-language-server: $schema=https://github.com/KeisukeYamashita/commitlint-rs/releases/latest/download/schema.json
77 | rules:
78 | ```
79 |
--------------------------------------------------------------------------------
/web/src/content/docs/config/configuration.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Configuration
3 | description: Guide how to configure commitlint
4 | ---
5 |
6 | Commitlint can be configured in many ways. You can use the default configuration or you can specify your own configuration file.
7 |
8 | ## Default configuration
9 |
10 | If you don't specify any configuration file and you don't have any commitlint configuration files in your environment, the CLI will use the default configurations of each rules.
11 |
12 | See the [default rules](/commitlint-rs/config/default) page for details.
13 |
14 | ## Location
15 |
16 | Commitlint will look for configuration in the following places.
17 |
18 | ### Default
19 |
20 | If no flag (see next section) is specified, the Commitlint will look for configuration in the following places **in the current working directory**:
21 |
22 | * `.commitlintrc` (JSON or YAML file)
23 | * `.commitlintrc.json` (JSON file)
24 | * `.commitlintrc.yml` (YAML file)
25 | * `.commitlintrc.yaml` (YAML file)
26 |
27 | :::tip
28 |
29 | Note that it is searched in the order written above and the first one found is loaded. Therefore, if you have `.commitlintrc` and `.commitlintrc.yml` in the same directory, the `.commitlintrc` will be loaded and the second one will be ignored.
30 |
31 | :::
32 |
33 | ### Using the flag
34 |
35 | Configuration file can be specified by using the `--config` flag or the short `-c` flag.
36 |
37 | ```console
38 | # Using --config flag
39 | $ commitlint --config path/to/.commitlintrc.yml
40 |
41 | # Using -c flag
42 | $ commitlint -c path/to/.commitlintrc.yml
43 | ```
44 |
45 | If you specify a file and the file is not found, Commitlint will throw an error.
46 |
47 | ## Debug configuration
48 |
49 | You can use the `--print-config` flag to print the configuration that will be used by Commitlint.
50 |
51 | ```console
52 | $ commitlint --print-config
53 | rules:
54 | description-empty: # Description must not be empty
55 | level: warning
56 | subject-empty: # Subject line must not be empty
57 | level: error
58 | type-empty: # Type must not be empty
59 | level: error
60 | ```
61 |
--------------------------------------------------------------------------------
/web/src/content/docs/config/default.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Default Rules
3 | description: List of the default rules
4 | ---
5 |
6 | If you don't specify any configuration file and you don't have any commitlint configuration files in your environment, the CLI will use the default configurations of each rules describes in this page.
7 |
8 | :::tip
9 |
10 | You can also check the default values on the page of each rule.
11 |
12 | :::
13 |
14 | ```yaml
15 | rules:
16 | description-empty: # Description shouldn't be empty
17 | level: warning
18 | subject-empty: # Subject line should exist
19 | level: error
20 | type-empty: # Type must not be empty
21 | level: error
22 | ```
23 |
--------------------------------------------------------------------------------
/web/src/content/docs/index.mdx:
--------------------------------------------------------------------------------
1 | ---
2 | title: Welcome to Commitlint
3 | description: Lint commit messages with conventional commits
4 | template: splash
5 | hero:
6 | tagline: Guard rail for your commit messages for sustainable development
7 | image:
8 | file: ../../assets/checker.png
9 | actions:
10 | - text: Install to your project
11 | link: /commitlint-rs/setup/install
12 | icon: right-arrow
13 | variant: primary
14 | - text: Read the rules
15 | link: /commitlint-rs/rules/body-empty
16 | icon: external
17 | ---
18 |
19 | import { Card, CardGrid } from "@astrojs/starlight/components";
20 |
21 | ## Next steps
22 |
23 |
24 |
25 | Learn more in the [Conventional
26 | Commits](https://www.conventionalcommits.org/en/v1.0.0/) specification.
27 |
28 |
29 | Run `commitlint` to lint your commit messages. See [the
30 | guides](/commitlint-rs/setup/installation) about the CLI.
31 |
32 |
33 | Check each rules to see what is allowed and what is not. See [the
34 | rules](/commitlint-rs/rules/body-empty) for more information.
35 |
36 |
37 | Edit commitlint configurations to customize your rules. See [the
38 | rules](/commitlint-rs/rules/body-empty) for more information.
39 |
40 |
41 | Report issues to the [GitHub
42 | repository](https://github.com/KeisukeYamashita/commitlint-rs/issues).
43 |
44 |
45 |
--------------------------------------------------------------------------------
/web/src/content/docs/license.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: License
3 | description: About the license of this project
4 | ---
5 |
6 | ## Assets
7 |
8 | * Logo: [Chinese-checkers icons created by Freepik - Flaticon](https://www.flaticon.com/free-icons/chinese-checkers)
9 |
--------------------------------------------------------------------------------
/web/src/content/docs/rules/body-empty.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Body Empty
3 | description: Check if the body exists
4 | ---
5 |
6 | * Default: `ignore`
7 |
8 | ## ❌ Bad
9 |
10 | ```console
11 | feat(cli): add new flag
12 | ```
13 |
14 | ## ✅ Good
15 |
16 | ```console
17 | feat(cli): add new flag
18 |
19 | Add new flag --help for https://github.com/KeisukeYamashita/commitlint-rs/issues/20
20 | ```
21 |
22 | ## Example
23 |
24 | ```yaml
25 | rules:
26 | body-empty:
27 | level: error
28 | ```
29 |
--------------------------------------------------------------------------------
/web/src/content/docs/rules/body-max-length.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Body Max Length
3 | description: Check if the body length is less than or equal to the specified length
4 | ---
5 |
6 | * Default:
7 | * Level: `ignore`
8 |
9 | In this page, we will use the following commit message as an example.
10 |
11 | ```yaml
12 | rules:
13 | body-max-length:
14 | level: error
15 | length: 4
16 | ```
17 |
18 | ## ❌ Bad
19 |
20 | ```console
21 | feat(cli): add new flag
22 |
23 | Hello, I'm a body of the commit message.
24 | ```
25 |
26 | ## ✅ Good
27 |
28 | ```console
29 | feat(cli): add new flag
30 |
31 | Hey.
32 | ```
33 |
34 | ## Example
35 |
36 | ### Body length should be less than or equal to 72
37 |
38 | ```yaml
39 | rules:
40 | body-max-length:
41 | level: error
42 | length: 72
43 | ```
44 |
--------------------------------------------------------------------------------
/web/src/content/docs/rules/description-empty.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Description Empty
3 | description: Check if the description exists
4 | ---
5 |
6 | * Default: `error`
7 |
8 | ## ❌ Bad
9 |
10 | ```console
11 | feat(cli):
12 | ```
13 |
14 | ## ✅ Good
15 |
16 | ```console
17 | feat(cli): add new flag
18 | ```
19 |
20 | ## Example
21 |
22 | ### Description must exist
23 |
24 | ```yaml
25 | rules:
26 | description-empty:
27 | level: error
28 | ```
29 |
--------------------------------------------------------------------------------
/web/src/content/docs/rules/description-format.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Description format
3 | description: Check if the description format is valid
4 | ---
5 |
6 | * Default:`ignore`
7 |
8 | In this page, we will use the following commit message as an example.
9 |
10 | ```yaml
11 | rules:
12 | description-format:
13 | level: error
14 | format: ^[A-Z].*$
15 | ```
16 |
17 | ## ❌ Bad
18 |
19 | ```console
20 | feat(cli): added a new flag
21 | ```
22 |
23 | ## ✅ Good
24 |
25 | ```console
26 | feat(cli): Added a new flag
27 | ```
28 |
29 | ## Example
30 |
31 | ### Description must start with a capital letter
32 |
33 | ```yaml
34 | rules:
35 | description-format:
36 | level: error
37 | format: ^[A-Z].*$
38 | ```
39 |
--------------------------------------------------------------------------------
/web/src/content/docs/rules/description-max-length.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Description Max Length
3 | description: Check if the description length is less than or equal to the specified length
4 | ---
5 |
6 | * Default:
7 | * Level: `ignore`
8 |
9 | In this page, we will use the following commit message as an example.
10 |
11 | ```yaml
12 | rules:
13 | description-max-length:
14 | level: error
15 | length: 12
16 | ```
17 |
18 | ## ❌ Bad
19 |
20 | ```console
21 | feat(cli): add new flag for brand new feature
22 | ```
23 |
24 | ## ✅ Good
25 |
26 | ```console
27 | feat(cli): add help flag
28 | ```
29 |
30 | ## Example
31 |
32 | ### Description length should be less than or equal to 72
33 |
34 | ```yaml
35 | rules:
36 | description-max-length:
37 | level: error
38 | length: 72
39 | ```
40 |
--------------------------------------------------------------------------------
/web/src/content/docs/rules/footers-empty.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Footers Empty
3 | description: Check if the footers exists
4 | ---
5 |
6 | * Default: `error`
7 |
8 | ## ❌ Bad
9 |
10 | ```console
11 | feat(cli): user logout handler
12 | ```
13 |
14 | ## ✅ Good
15 |
16 | ```console
17 | feat(cli): add new flag
18 |
19 | Link: https://keisukeyamashita.github.io/commitlint-rs/
20 | ```
21 |
22 | ## Example
23 |
24 | ### Footers must exist
25 |
26 | ```yaml
27 | rules:
28 | footers-empty:
29 | level: error
30 | ```
31 |
--------------------------------------------------------------------------------
/web/src/content/docs/rules/scope-empty.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Scope Empty
3 | description: Check if the scope exists
4 | ---
5 |
6 | * Default: `ignore`
7 |
8 | ## ❌ Bad
9 |
10 | ```console
11 | (cli): fix typo
12 | ```
13 |
14 | ## ✅ Good
15 |
16 | ```console
17 | docs(cli): fix typo
18 | ```
19 |
20 | ## Example
21 |
22 | ### Scope must exist
23 |
24 | ```yaml
25 | rules:
26 | scope-empty:
27 | level: error
28 | ```
29 |
--------------------------------------------------------------------------------
/web/src/content/docs/rules/scope-format.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Scope format
3 | description: Check if the scope format is valid
4 | ---
5 |
6 | * Default:`ignore`
7 |
8 | In this page, we will use the following commit message as an example.
9 |
10 | ```yaml
11 | rules:
12 | scope-format:
13 | level: error
14 | format: ^[a-z]*$
15 | ```
16 |
17 | ## ❌ Bad
18 |
19 | ```console
20 | feat(Cli): added a new flag
21 | ```
22 |
23 | ## ✅ Good
24 |
25 | ```console
26 | feat(cli): added a new flag
27 | ```
28 |
29 | ## Example
30 |
31 | ### Scope must start with a lower letter
32 |
33 | ```yaml
34 | rules:
35 | scope-format:
36 | level: error
37 | format: ^[a-z]*$
38 | ```
39 |
--------------------------------------------------------------------------------
/web/src/content/docs/rules/scope-max-length.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Scope Max Length
3 | description: Check if the scope length is less than or equal to the specified length
4 | ---
5 |
6 | * Default:
7 | * Level: `ignore`
8 |
9 | In this page, we will use the following commit message as an example.
10 |
11 | ```yaml
12 | rules:
13 | scope-max-length:
14 | level: error
15 | length: 5
16 | ```
17 |
18 | ## ❌ Bad
19 |
20 | ```console
21 | feat(super important product): add new flag
22 | ```
23 |
24 | ## ✅ Good
25 |
26 | ```console
27 | feat(cli): add new flag
28 | ```
29 |
30 | ## Example
31 |
32 | ### Description length should be less than or equal to 5
33 |
34 | ```yaml
35 | rules:
36 | scope-max-length:
37 | level: error
38 | length: 5
39 | ```
40 |
--------------------------------------------------------------------------------
/web/src/content/docs/rules/scope.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Scope
3 | description: Allowlist for scopes
4 | ---
5 |
6 | * Default:
7 | * Level: `ignore`
8 | * Optional: `false`
9 |
10 | In this example, we assumed that you have a project with the following scopes:
11 |
12 | ```yaml
13 | rules:
14 | scope:
15 | level: error
16 | options:
17 | - api
18 | - web
19 | ```
20 |
21 | ## ❌ Bad
22 |
23 | ```console
24 | chore(cli): fix typo
25 | => scope cli is not allowed. Only ["api", "web"] are allowed
26 | ```
27 |
28 | ## ✅ Good
29 |
30 | ```console
31 | chore(api): fix typo
32 | ```
33 |
34 | ## Example
35 |
36 | ### Only allow scopes `api` and `web`
37 |
38 | ```yaml
39 | rules:
40 | scope:
41 | level: error
42 | options:
43 | - api
44 | - web
45 | ```
46 |
47 | ### Optional scopes `api` and `web`
48 |
49 | ```yaml
50 | rules:
51 | scope:
52 | level: error
53 | optional: true
54 | options:
55 | - api
56 | ```
57 |
58 | With this configuration, `feat(api): xxx` and `feat: xxx` are valid commits.
59 |
60 | ### Disallow all scopes
61 |
62 | ```yaml
63 | rules:
64 | scope:
65 | level: error
66 | options: [] # or [""]
67 | scope-empty:
68 | level: ignore
69 | ```
70 |
--------------------------------------------------------------------------------
/web/src/content/docs/rules/subject-empty.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Subject Empty
3 | description: Check if the subject exists
4 | ---
5 |
6 | * Default: `error`
7 |
8 | ## ❌ Bad
9 |
10 | ```console
11 |
12 |
13 | Body of the commit
14 | ```
15 |
16 | ## ✅ Good
17 |
18 | ```console
19 | docs(cli): fix typo
20 |
21 | Body of the commit
22 | ```
23 |
24 | ## Example
25 |
26 | ### Subject must exist
27 |
28 | ```yaml
29 | rules:
30 | subject-empty:
31 | level: error
32 | ```
33 |
--------------------------------------------------------------------------------
/web/src/content/docs/rules/type-empty.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Type Empty
3 | description: Check if the type exists
4 | ---
5 |
6 | * Default: `error`
7 |
8 | ## ❌ Bad
9 |
10 | ```console
11 | docs: fix typo
12 | ```
13 |
14 | ```console
15 | (web): fix typo
16 | ```
17 |
18 | ## ✅ Good
19 |
20 | ```console
21 | docs(web): fix typo
22 | ```
23 |
24 | ## Example
25 |
26 | ### Type must exist
27 |
28 | ```yaml
29 | rules:
30 | type-empty:
31 | level: error
32 | ```
33 |
--------------------------------------------------------------------------------
/web/src/content/docs/rules/type-format.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Type format
3 | description: Check if the type format is valid
4 | ---
5 |
6 | * Default:`ignore`
7 |
8 | In this page, we will use the following commit message as an example.
9 |
10 | ```yaml
11 | rules:
12 | type-format:
13 | level: error
14 | format: ^[a-z].*$
15 | ```
16 |
17 | ## ❌ Bad
18 |
19 | ```console
20 | Feat(cli): added a new flag
21 | ```
22 |
23 | ## ✅ Good
24 |
25 | ```console
26 | feat(cli): Added a new flag
27 | ```
28 |
29 | ## Example
30 |
31 | ### Type must start with a capital letter
32 |
33 | ```yaml
34 | rules:
35 | type-format:
36 | level: error
37 | format: ^[a-z].*$
38 | ```
39 |
--------------------------------------------------------------------------------
/web/src/content/docs/rules/type-max-length.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Type Max Length
3 | description: Check if the type length is less than or equal to the specified length
4 | ---
5 |
6 | * Default:
7 | * Level: `ignore`
8 |
9 | In this page, we will use the following commit message as an example.
10 |
11 | ```yaml
12 | rules:
13 | type-max-length:
14 | level: error
15 | length: 6
16 | ```
17 |
18 | ## ❌ Bad
19 |
20 | ```console
21 | feature-for-future(cli): add new flag
22 | ```
23 |
24 | ## ✅ Good
25 |
26 | ```console
27 | feat(cli): add new flag
28 | ```
29 |
30 | ## Example
31 |
32 | ### Type length should be less than or equal to 72
33 |
34 | ```yaml
35 | rules:
36 | type-max-length:
37 | level: error
38 | length: 72
39 | ```
40 |
--------------------------------------------------------------------------------
/web/src/content/docs/rules/type.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Type
3 | description: Allowlist for types
4 | ---
5 |
6 | * Default: `ignore`
7 |
8 | In this example, we assumed that you have a project with the following types:
9 |
10 | ```yaml
11 | rules:
12 | type:
13 | level: error
14 | options:
15 | - feat
16 | - fix
17 | ```
18 |
19 | ## ❌ Bad
20 |
21 | ```console
22 | chore(cli): fix typo
23 | => type chore is not allowed. Only ["feat", "fix"] are allowed
24 | ```
25 |
26 | ## ✅ Good
27 |
28 | ```console
29 | fix(api): fix typo
30 | ```
31 |
32 | ## Example
33 |
34 | ### Only allow types `feat` and `fix`
35 |
36 | ```yaml
37 | rules:
38 | type:
39 | level: error
40 | options:
41 | - api
42 | - web
43 | ```
44 |
45 | ### Disallow all types
46 |
47 | ```yaml
48 | rules:
49 | type:
50 | level: error
51 | options: [] # or [""]
52 | type-empty:
53 | level: ignore
54 | ```
55 |
--------------------------------------------------------------------------------
/web/src/content/docs/setup/debug.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Debug
3 | description: Debug your configurations
4 | ---
5 |
6 | You maybe wondering how you can debug your configurations. This is a common question and we have a few options for you to consider.
7 |
8 | ## Debugging how a rule works
9 |
10 | Debug your configuration using stdin and below:
11 |
12 | ```console
13 | echo "feat(other): debug" | commitlint
14 | ```
15 |
16 | ## Debugging your configuration
17 |
18 | You can debug how your configuration is being loaded and what rules are being used by running the following command:
19 |
20 | ```console
21 | commitlint --print-config
22 | ```
23 |
--------------------------------------------------------------------------------
/web/src/content/docs/setup/install.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Installation
3 | description: Guide how to install commitlint to your project
4 | ---
5 |
6 | ## Using `cargo` CLI
7 |
8 | Commitlint is written in Rust so you can install it using `cargo` CLI:
9 |
10 | ```console
11 | cargo install commitlint-rs
12 | ```
13 |
14 | After that, you will be able to run the `commitlint` command.
15 |
16 | ```console
17 | commitlint --help
18 | ```
19 |
20 | ## Using Cargo Binary Install
21 |
22 | You can also use Binstall ([cargo-bins/cargo-binstall](https://github.com/cargo-bins/cargo-binstall)) to install the CLI
23 |
24 | ```console
25 | cargo binstall commitlint-rs
26 | ```
27 |
28 | ## Using Docker
29 |
30 | Commitlint is also available as a Docker image.
31 | You can pull it from [Docker Hub](https://hub.docker.com/repository/docker/1915keke/commitlint).
32 |
33 | ```console
34 | docker run 1915keke/commitlint
35 | ```
36 |
37 | See all available tags [here](https://hub.docker.com/repository/docker/1915keke/commitlint/tags?page=1&ordering=last_updated).
38 |
--------------------------------------------------------------------------------
/web/src/content/docs/setup/motivation.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: Motivation
3 | description: Why commitlint in Rust?
4 | ---
5 |
6 | Why commitlint in Rust?
7 |
8 | It's very simple, we wanted to complete it in the Rust ecosystem 🚀
9 |
10 | It is hard to prepare a `package.json` for a Rust project to use an existing commitlint, or to prepare a Node.js runtime with CI.
11 | Therefore, we have re-implemented a commitlint that can be easily used with cargo install, etc.
12 |
--------------------------------------------------------------------------------
/web/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "astro/tsconfigs/strict"
3 | }
--------------------------------------------------------------------------------