diff --git a/.github/workflows/build-and-deploy.yml b/.github/workflows/build-and-deploy.yml index 25f582a..d3b802d 100644 --- a/.github/workflows/build-and-deploy.yml +++ b/.github/workflows/build-and-deploy.yml @@ -31,7 +31,7 @@ jobs: name: Clone git repository - id: docker-login - uses: Seravo/actions/docker-login@v1.15.0 + uses: seravo/actions/docker-login@v1.16.0 name: Login to ghcr.io # To speed up builds, try to use previously built image as cache source. @@ -40,12 +40,12 @@ jobs: - if: ${{ github.event_name != 'schedule' }} name: Pull previously built image id: docker-pull - uses: Seravo/actions/docker-pull-previous@v1.15.0 + uses: seravo/actions/docker-pull-previous@v1.16.0 with: image: "${{ env.image }}" - id: docker-build - uses: Seravo/actions/docker-build@v1.15.0 + uses: seravo/actions/docker-build@v1.16.0 name: Build image with: image: "${{ env.image }}" @@ -53,20 +53,20 @@ jobs: - if: ${{ github.ref == 'refs/heads/main' }} name: Push new image to production id: docker-push-master - uses: Seravo/actions/docker-push@v1.15.0 + uses: seravo/actions/docker-push@v1.16.0 with: image: "${{ env.image }}" - id: docker-tag-push-commit name: Tag image with commit id - uses: Seravo/actions/docker-tag-and-push@v1.15.0 + uses: seravo/actions/docker-tag-and-push@v1.16.0 with: source: "${{ env.image }}" target: "${{ env.image }}:${{ github.sha }}" - id: docker-tag-push-refname name: Tag image with refname - uses: Seravo/actions/docker-tag-and-push@v1.15.0 + uses: seravo/actions/docker-tag-and-push@v1.16.0 with: source: "${{ env.image }}" target: "${{ env.image }}:${{ steps.refname.outputs.refname }}" diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000..5cd3d20 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,22 @@ +--- +name: Check code formatting with Ruff + +on: + push: + branches: + - main + - feature/** + - bugfix/** + +jobs: + format: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Ruff + id: format + uses: seravo/actions/pyruff-format@v1.16.0 + with: + dev_requirements_path: dev-requirements.txt diff --git a/.github/workflows/gitlint.yml b/.github/workflows/gitlint.yml index a702d80..206972d 100644 --- a/.github/workflows/gitlint.yml +++ b/.github/workflows/gitlint.yml @@ -13,4 +13,4 @@ jobs: ref: ${{ github.head_ref }} - name: Execute git linting - uses: seravo/gitlint@v1.0.1 + uses: seravo/gitlint@v1 diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml new file mode 100644 index 0000000..87d6115 --- /dev/null +++ b/.github/workflows/pytest.yml @@ -0,0 +1,27 @@ +--- +name: Run unittests + +on: + push: + branches: + - main + - feature/** + - bugfix/** + +jobs: + testing: + runs-on: ubuntu-latest + strategy: + matrix: + PYTHON_VERSION: ["3.12"] + steps: + - name: Checkout repository with submodules + uses: actions/checkout@v4 + + - name: Run tests + id: tests + uses: seravo/actions/pytest@v1.16.0 + with: + python_version: ${{ matrix.PYTHON_VERSION }} + requirements_path: requirements.txt + dev_requirements_path: dev-requirements.txt diff --git a/.github/workflows/python-lint.yml b/.github/workflows/python-lint.yml new file mode 100644 index 0000000..d88abb6 --- /dev/null +++ b/.github/workflows/python-lint.yml @@ -0,0 +1,22 @@ +--- +name: Lint code with ruff + +on: + push: + branches: + - main + - feature/** + - bugfix/** + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run ruff + id: format + uses: seravo/actions/pyruff@v1.16.0 + with: + dev_requirements_path: dev-requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c3ba83f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*~ +*.swp +*.py[co] diff --git a/.gitlint b/.gitlint index f57534e..5aa9959 100644 --- a/.gitlint +++ b/.gitlint @@ -1,5 +1,6 @@ [general] regex-style-search = True +extra-path=/gitlint-rules.d # [title-max-length] # line-length=50 diff --git a/Dockerfile b/Dockerfile index c22599d..1f5488f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,7 @@ RUN useradd user && \ mkdir -p /workdir && \ mkdir -p /config +COPY rules /gitlint-rules.d COPY .gitlint /config/gitlint COPY --chmod=0755 entrypoint.sh /entrypoint.sh diff --git a/dev-requirements.in b/dev-requirements.in new file mode 100644 index 0000000..91ae08a --- /dev/null +++ b/dev-requirements.in @@ -0,0 +1,4 @@ +coverage>=7.13 +pytest>=9.0 +ruff>=0.14 +gitlint diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..4edeb5b --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,164 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile --generate-hashes dev-requirements.in -o dev-requirements.txt +arrow==1.2.3 \ + --hash=sha256:3934b30ca1b9f292376d9db15b19446088d12ec58629bc3f0da28fd55fb633a1 \ + --hash=sha256:5a49ab92e3b7b71d96cd6bfcc4df14efefc9dfa96ea19045815914a6ab6b1fe2 + # via gitlint-core +click==8.1.3 \ + --hash=sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e \ + --hash=sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48 + # via gitlint-core +coverage==7.13.0 \ + --hash=sha256:0018f73dfb4301a89292c73be6ba5f58722ff79f51593352759c1790ded1cabe \ + --hash=sha256:00c3d22cf6fb1cf3bf662aaaa4e563be8243a5ed2630339069799835a9cc7f9b \ + --hash=sha256:02d9fb9eccd48f6843c98a37bd6817462f130b86da8660461e8f5e54d4c06070 \ + --hash=sha256:0602f701057c6823e5db1b74530ce85f17c3c5be5c85fc042ac939cbd909426e \ + --hash=sha256:06cac81bf10f74034e055e903f5f946e3e26fc51c09fc9f584e4a1605d977053 \ + --hash=sha256:086cede306d96202e15a4b77ace8472e39d9f4e5f9fd92dd4fecdfb2313b2080 \ + --hash=sha256:0900872f2fdb3ee5646b557918d02279dc3af3dfb39029ac4e945458b13f73bc \ + --hash=sha256:0a3a30f0e257df382f5f9534d4ce3d4cf06eafaf5192beb1a7bd066cb10e78fb \ + --hash=sha256:0b3d67d31383c4c68e19a88e28fc4c2e29517580f1b0ebec4a069d502ce1e0bf \ + --hash=sha256:0dfa3855031070058add1a59fdfda0192fd3e8f97e7c81de0596c145dea51820 \ + --hash=sha256:0f4872f5d6c54419c94c25dd6ae1d015deeb337d06e448cd890a1e89a8ee7f3b \ + --hash=sha256:11c21557d0e0a5a38632cbbaca5f008723b26a89d70db6315523df6df77d6232 \ + --hash=sha256:166ad2a22ee770f5656e1257703139d3533b4a0b6909af67c6b4a3adc1c98657 \ + --hash=sha256:193c3887285eec1dbdb3f2bd7fbc351d570ca9c02ca756c3afbc71b3c98af6ef \ + --hash=sha256:1d84e91521c5e4cb6602fe11ece3e1de03b2760e14ae4fcf1a4b56fa3c801fcd \ + --hash=sha256:1ed5630d946859de835a85e9a43b721123a8a44ec26e2830b296d478c7fd4259 \ + --hash=sha256:22486cdafba4f9e471c816a2a5745337742a617fef68e890d8baf9f3036d7833 \ + --hash=sha256:22ccfe8d9bb0d6134892cbe1262493a8c70d736b9df930f3f3afae0fe3ac924d \ + --hash=sha256:24e4e56304fdb56f96f80eabf840eab043b3afea9348b88be680ec5986780a0f \ + --hash=sha256:25dc33618d45456ccb1d37bce44bc78cf269909aa14c4db2e03d63146a8a1493 \ + --hash=sha256:263c3dbccc78e2e331e59e90115941b5f53e85cfcc6b3b2fbff1fd4e3d2c6ea8 \ + --hash=sha256:28ee1c96109974af104028a8ef57cec21447d42d0e937c0275329272e370ebcf \ + --hash=sha256:30a3a201a127ea57f7e14ba43c93c9c4be8b7d17a26e03bb49e6966d019eede9 \ + --hash=sha256:3188936845cd0cb114fa6a51842a304cdbac2958145d03be2377ec41eb285d19 \ + --hash=sha256:367449cf07d33dc216c083f2036bb7d976c6e4903ab31be400ad74ad9f85ce98 \ + --hash=sha256:37eee4e552a65866f15dedd917d5e5f3d59805994260720821e2c1b51ac3248f \ + --hash=sha256:3a10260e6a152e5f03f26db4a407c4c62d3830b9af9b7c0450b183615f05d43b \ + --hash=sha256:3a7b1cd820e1b6116f92c6128f1188e7afe421c7e1b35fa9836b11444e53ebd9 \ + --hash=sha256:3ab483ea0e251b5790c2aac03acde31bff0c736bf8a86829b89382b407cd1c3b \ + --hash=sha256:3ad968d1e3aa6ce5be295ab5fe3ae1bf5bb4769d0f98a80a0252d543a2ef2e9e \ + --hash=sha256:445badb539005283825959ac9fa4a28f712c214b65af3a2c464f1adc90f5fcbc \ + --hash=sha256:453b7ec753cf5e4356e14fe858064e5520c460d3bbbcb9c35e55c0d21155c256 \ + --hash=sha256:494f5459ffa1bd45e18558cd98710c36c0b8fbfa82a5eabcbe671d80ecffbfe8 \ + --hash=sha256:4b5de7d4583e60d5fd246dd57fcd3a8aa23c6e118a8c72b38adf666ba8e7e927 \ + --hash=sha256:4f3e223b2b2db5e0db0c2b97286aba0036ca000f06aca9b12112eaa9af3d92ae \ + --hash=sha256:4fdb6f54f38e334db97f72fa0c701e66d8479af0bc3f9bfb5b90f1c30f54500f \ + --hash=sha256:51a202e0f80f241ccb68e3e26e19ab5b3bf0f813314f2c967642f13ebcf1ddfe \ + --hash=sha256:581f086833d24a22c89ae0fe2142cfaa1c92c930adf637ddf122d55083fb5a0f \ + --hash=sha256:583221913fbc8f53b88c42e8dbb8fca1d0f2e597cb190ce45916662b8b9d9621 \ + --hash=sha256:58632b187be6f0be500f553be41e277712baa278147ecb7559983c6d9faf7ae1 \ + --hash=sha256:5c67dace46f361125e6b9cace8fe0b729ed8479f47e70c89b838d319375c8137 \ + --hash=sha256:5e70f92ef89bac1ac8a99b3324923b4749f008fdbd7aa9cb35e01d7a284a04f9 \ + --hash=sha256:5f5d9bd30756fff3e7216491a0d6d520c448d5124d3d8e8f56446d6412499e74 \ + --hash=sha256:5f8a0297355e652001015e93be345ee54393e45dc3050af4a0475c5a2b767d46 \ + --hash=sha256:62d7c4f13102148c78d7353c6052af6d899a7f6df66a32bddcc0c0eb7c5326f8 \ + --hash=sha256:69ac2c492918c2461bc6ace42d0479638e60719f2a4ef3f0815fa2df88e9f940 \ + --hash=sha256:6abb3a4c52f05e08460bd9acf04fec027f8718ecaa0d09c40ffbc3fbd70ecc39 \ + --hash=sha256:6e63ccc6e0ad8986386461c3c4b737540f20426e7ec932f42e030320896c311a \ + --hash=sha256:6e9e451dee940a86789134b6b0ffbe31c454ade3b849bb8a9d2cca2541a8e91d \ + --hash=sha256:6fb2d5d272341565f08e962cce14cdf843a08ac43bd621783527adb06b089c4b \ + --hash=sha256:71936a8b3b977ddd0b694c28c6a34f4fff2e9dd201969a4ff5d5fc7742d614b0 \ + --hash=sha256:73419b89f812f498aca53f757dd834919b48ce4799f9d5cad33ca0ae442bdb1a \ + --hash=sha256:739c6c051a7540608d097b8e13c76cfa85263ced467168dc6b477bae3df7d0e2 \ + --hash=sha256:7464663eaca6adba4175f6c19354feea61ebbdd735563a03d1e472c7072d27bb \ + --hash=sha256:74c136e4093627cf04b26a35dab8cbfc9b37c647f0502fc313376e11726ba303 \ + --hash=sha256:76541dc8d53715fb4f7a3a06b34b0dc6846e3c69bc6204c55653a85dd6220971 \ + --hash=sha256:7a485ff48fbd231efa32d58f479befce52dcb6bfb2a88bb7bf9a0b89b1bc8030 \ + --hash=sha256:7e442c013447d1d8d195be62852270b78b6e255b79b8675bad8479641e21fd96 \ + --hash=sha256:7f15a931a668e58087bc39d05d2b4bf4b14ff2875b49c994bbdb1c2217a8daeb \ + --hash=sha256:7f88ae3e69df2ab62fb0bc5219a597cb890ba5c438190ffa87490b315190bb33 \ + --hash=sha256:8069e831f205d2ff1f3d355e82f511eb7c5522d7d413f5db5756b772ec8697f8 \ + --hash=sha256:850d2998f380b1e266459ca5b47bc9e7daf9af1d070f66317972f382d46f1904 \ + --hash=sha256:898cce66d0836973f48dda4e3514d863d70142bdf6dfab932b9b6a90ea5b222d \ + --hash=sha256:9097818b6cc1cfb5f174e3263eba4a62a17683bcfe5c4b5d07f4c97fa51fbf28 \ + --hash=sha256:936bc20503ce24770c71938d1369461f0c5320830800933bc3956e2a4ded930e \ + --hash=sha256:9372dff5ea15930fea0445eaf37bbbafbc771a49e70c0aeed8b4e2c2614cc00e \ + --hash=sha256:9987a9e4f8197a1000280f7cc089e3ea2c8b3c0a64d750537809879a7b4ceaf9 \ + --hash=sha256:99acd4dfdfeb58e1937629eb1ab6ab0899b131f183ee5f23e0b5da5cba2fec74 \ + --hash=sha256:9b01c22bc74a7fb44066aaf765224c0d933ddf1f5047d6cdfe4795504a4493f8 \ + --hash=sha256:a00d3a393207ae12f7c49bb1c113190883b500f48979abb118d8b72b8c95c032 \ + --hash=sha256:a23e5a1f8b982d56fa64f8e442e037f6ce29322f1f9e6c2344cd9e9f4407ee57 \ + --hash=sha256:a2bdb3babb74079f021696cb46b8bb5f5661165c385d3a238712b031a12355be \ + --hash=sha256:a394aa27f2d7ff9bc04cf703817773a59ad6dfbd577032e690f961d2460ee936 \ + --hash=sha256:a6c6e16b663be828a8f0b6c5027d36471d4a9f90d28444aa4ced4d48d7d6ae8f \ + --hash=sha256:af0a583efaacc52ae2521f8d7910aff65cdb093091d76291ac5820d5e947fc1c \ + --hash=sha256:af827b7cbb303e1befa6c4f94fd2bf72f108089cfa0f8abab8f4ca553cf5ca5a \ + --hash=sha256:c4be718e51e86f553bcf515305a158a1cd180d23b72f07ae76d6017c3cc5d791 \ + --hash=sha256:cdb3c9f8fef0a954c632f64328a3935988d33a6604ce4bf67ec3e39670f12ae5 \ + --hash=sha256:d10fd186aac2316f9bbb46ef91977f9d394ded67050ad6d84d94ed6ea2e8e54e \ + --hash=sha256:d1e97353dcc5587b85986cda4ff3ec98081d7e84dd95e8b2a6d59820f0545f8a \ + --hash=sha256:d2a9d7f1c11487b1c69367ab3ac2d81b9b3721f097aa409a3191c3e90f8f3dd7 \ + --hash=sha256:de7f6748b890708578fc4b7bb967d810aeb6fcc9bff4bb77dbca77dab2f9df6a \ + --hash=sha256:e5330fa0cc1f5c3c4c3bb8e101b742025933e7848989370a1d4c8c5e401ea753 \ + --hash=sha256:e999e2dcc094002d6e2c7bbc1fb85b58ba4f465a760a8014d97619330cdbbbf3 \ + --hash=sha256:eb76670874fdd6091eedcc856128ee48c41a9bbbb9c3f1c7c3cf169290e3ffd6 \ + --hash=sha256:f1c23e24a7000da892a312fb17e33c5f94f8b001de44b7cf8ba2e36fbd15859e \ + --hash=sha256:f2ffc92b46ed6e6760f1d47a71e56b5664781bc68986dbd1836b2b70c0ce2071 \ + --hash=sha256:f4f72a85316d8e13234cafe0a9f81b40418ad7a082792fa4165bd7d45d96066b \ + --hash=sha256:f59883c643cb19630500f57016f76cfdcd6845ca8c5b5ea1f6e17f74c8e5f511 \ + --hash=sha256:f6aaef16d65d1787280943f1c8718dc32e9cf141014e4634d64446702d26e0ff \ + --hash=sha256:fe81055d8c6c9de76d60c94ddea73c290b416e061d40d542b24a5871bad498b7 \ + --hash=sha256:ff45e0cd8451e293b63ced93161e189780baf444119391b3e7d25315060368a6 + # via -r dev-requirements.in +gitlint==0.19.1 \ + --hash=sha256:26bb085959148d99fbbc178b4e56fda6c3edd7646b7c2a24d8ee1f8e036ed85d \ + --hash=sha256:b5b70fb894e80849b69abbb65ee7dbb3520fc3511f202a6e6b6ddf1a71ee8f61 + # via -r dev-requirements.in +gitlint-core==0.19.1 \ + --hash=sha256:7bf977b03ff581624a9e03f65ebb8502cc12dfaa3e92d23e8b2b54bbdaa29992 \ + --hash=sha256:f41effd1dcbc06ffbfc56b6888cce72241796f517b46bd9fd4ab1b145056988c + # via gitlint +iniconfig==2.0.0 \ + --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \ + --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374 + # via pytest +packaging==23.2 \ + --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \ + --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7 + # via pytest +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via pytest +pygments==2.19.2 \ + --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ + --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b + # via pytest +pytest==9.0.2 \ + --hash=sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b \ + --hash=sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11 + # via -r dev-requirements.in +python-dateutil==2.9.0.post0 \ + --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ + --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 + # via arrow +ruff==0.14.9 \ + --hash=sha256:1e5cb521e5ccf0008bd74d5595a4580313844a42b9103b7388eca5a12c970743 \ + --hash=sha256:347e3bf16197e8a2de17940cd75fd6491e25c0aa7edf7d61aa03f146a1aa885a \ + --hash=sha256:35f85b25dd586381c0cc053f48826109384c81c00ad7ef1bd977bfcc28119d5b \ + --hash=sha256:6a1cfb04eda979b20c8c19550c8b5f498df64ff8da151283311ce3199e8b3648 \ + --hash=sha256:712ff04f44663f1b90a1195f51525836e3413c8a773574a7b7775554269c30ed \ + --hash=sha256:72034534e5b11e8a593f517b2f2f2b273eb68a30978c6a2d40473ad0aaa4cb4a \ + --hash=sha256:7715d14e5bccf5b660f54516558aa94781d3eb0838f8e706fb60e3ff6eff03a8 \ + --hash=sha256:84bf7c698fc8f3cb8278830fb6b5a47f9bcc1ed8cb4f689b9dd02698fa840697 \ + --hash=sha256:8769efc71558fecc25eb295ddec7d1030d41a51e9dcf127cbd63ec517f22d567 \ + --hash=sha256:8e821c366517a074046d92f0e9213ed1c13dbc5b37a7fc20b07f79b64d62cc84 \ + --hash=sha256:a111fee1db6f1d5d5810245295527cda1d367c5aa8f42e0fca9a78ede9b4498b \ + --hash=sha256:aa733093d1f9d88a5d98988d8834ef5d6f9828d03743bf5e338bf980a19fce27 \ + --hash=sha256:ab208c1b7a492e37caeaf290b1378148f75e13c2225af5d44628b95fd7834273 \ + --hash=sha256:c0b53a10e61df15a42ed711ec0bda0c582039cf6c754c49c020084c55b5b0bc2 \ + --hash=sha256:cd429a8926be6bba4befa8cdcf3f4dd2591c413ea5066b1e99155ed245ae42bb \ + --hash=sha256:d5dc3473c3f0e4a1008d0ef1d75cee24a48e254c8bed3a7afdd2b4392657ed2c \ + --hash=sha256:df0937f30aaabe83da172adaf8937003ff28172f59ca9f17883b4213783df197 \ + --hash=sha256:ed9d7417a299fc6030b4f26333bf1117ed82a61ea91238558c0268c14e00d0c2 \ + --hash=sha256:f1ec5de1ce150ca6e43691f4a9ef5c04574ad9ca35c8b3b0e18877314aba7e75 + # via -r dev-requirements.in +sh==1.14.3 \ + --hash=sha256:e4045b6c732d9ce75d571c79f5ac2234edd9ae4f5fa9d59b09705082bdca18c7 + # via gitlint-core +six==1.17.0 \ + --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ + --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 + # via python-dateutil diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0947969 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,89 @@ +[project] +name = "seravo_gitlint" +description = "" +requires-python = ">=3.12" + + +[tool.pyright] +exclude = ["python-seravo"] +typeCheckingMode = "strict" +reportUnknownMemberType = "warning" +reportIncompatibleMethodOverride = "warning" +reportGeneralTypeIssues = "information" +reportUnknownVariableType = "information" +reportIncompatibleVariableOverride = "information" +reportPrivateImportUsage = false +reportMissingModuleSource = false +reportSelfClsParameterName = false +reportMissingTypeStubs = false +reportUnusedFunction = false +reportUntypedFunctionDecorator = false +reportUnnecessaryTypeIgnoreComment = "error" + + +[tool.ruff] +line-length = 79 +exclude = ["python-seravo", "migrations", "external"] +lint.select = [ + "C90", # mccabe + "E", # pycodestyle + "F", # pyflakes + "UP", # pyupgrade + "W", # pycodestyle + "I", # isort + "ANN001", # flake8-annotations name + "ANN002", # flake8-annotations args + "ANN003", # flake8-annotations kwargs + "ASYNC", # flake8-async + "ARG", # flake8-unused-arguments + "S", # flake8-bandit + "FBT", # flake8-boolean-trap + "B", # flake8-bugbear + "C4", # flake8-comprehensions + "TID252", # flake8-tidy-imports + "RET", # flake8-return + "SIM", # flake8-simplify + "PTH", # flake8-use-pathlib + "PLR1714", # pylint + "PERF", # Perflint + "RUF", # Ruff + "D201", # no-blank-line-before-function + "D202", # no-blank-line-after-function + "D204", # one-blank-line-after-class + "D419" # empty-docstring +] +lint.ignore = [ + "S101", # Use of assert detected + "S311", # Use of standard pseudo-random generators + "S603", # Subprocess without shell + "PERF203", # Use of try except in loop +] +target-version = "py312" + +[tool.ruff.lint.per-file-ignores] +# For parametrize and fixture, ignore boolean trap and unused args in tests +"tests/**.py" = ["FBT001", "ARG002", "ARG001"] +"tests/base.py" = ["ANN", "E", "RET", "PTH"] + +[tool.ruff.lint.mccabe] +max-complexity = 15 + +[tool.ruff.format] +preview = true +quote-style = "preserve" + +[tool.coverage.run] +branch = true +data_file = ".coverage" +omit = [ + "external/*", + "python-seravo/*", + "tests/*", + "ve/*" +] + +[tool.coverage.report] +fail_under = 95 +show_missing = true +skip_covered = true +skip_empty = true diff --git a/requirements-seravo.txt b/requirements-seravo.txt new file mode 100644 index 0000000..e69de29 diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/requirements.in @@ -0,0 +1 @@ + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ + diff --git a/rules/define_impact.py b/rules/define_impact.py new file mode 100644 index 0000000..57d94f4 --- /dev/null +++ b/rules/define_impact.py @@ -0,0 +1,24 @@ +import re + +from gitlint.git import GitCommit +from gitlint.rules import CommitRule, RuleViolation + + +class ImpactDefined(CommitRule): + """Enforce that `Impact: ` has been defined for every + commit""" + + name = "body-requires-impact" + + id = "SERAVO0001" + + regexp = r'^Impact: (major|minor|patch)$' + + def validate(self, commit: GitCommit) -> list[RuleViolation] | None: + self.log.debug("Check that proper Impact: has been defined") + + for line in commit.message.body: + if re.match(self.regexp, line): + return None + msg = "Body does not contain a valid 'Impact' line" + return [RuleViolation(self.id, msg, line_nr=1)] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/base.py b/tests/base.py new file mode 100644 index 0000000..db4cdf2 --- /dev/null +++ b/tests/base.py @@ -0,0 +1,243 @@ +import contextlib +import copy +import logging +import os +import re +import shutil +import tempfile +import unittest +from pathlib import Path +from typing import Any +from unittest.mock import patch + +from gitlint.config import LintConfig +from gitlint.deprecation import LOG as DEPRECATION_LOG +from gitlint.deprecation import Deprecation +from gitlint.git import GitChangedFileStats, GitContext +from gitlint.utils import FILE_ENCODING, LOG_FORMAT + +EXPECTED_REGEX_STYLE_SEARCH_DEPRECATION_WARNING = ( + "WARNING: gitlint.deprecated.regex_style_search {0} - {1}: gitlint will be switching from using " + "Python regex 'match' (match beginning) to 'search' (match anywhere) semantics. " + "Please review your {1}.regex option accordingly. " + "To remove this warning, set general.regex-style-search=True. More details: " + "https://jorisroovers.github.io/gitlint/configuration/general_options/#regex-style-search" +) + + +class BaseTestCase(unittest.TestCase): + """Base class of which all gitlint unit test classes are derived. Provides a number of convenience methods.""" + + # In case of assert failures, print the full error message + maxDiff = None + + # Working directory in which tests in this class are executed + working_dir = None + # Originally working dir when the test was started + original_working_dir = None + + SAMPLES_DIR = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "samples" + ) + EXPECTED_DIR = os.path.join( + os.path.dirname(os.path.realpath(__file__)), "expected" + ) + GITLINT_USE_SH_LIB = os.environ.get("GITLINT_USE_SH_LIB", "[NOT SET]") + + @classmethod + def setUpClass(cls): + # Run tests a temporary directory to shield them from any local git config + cls.original_working_dir = os.getcwd() + cls.working_dir = tempfile.mkdtemp() + os.chdir(cls.working_dir) + + @classmethod + def tearDownClass(cls): + # Go back to original working dir and remove our temp working dir + os.chdir(cls.original_working_dir) + shutil.rmtree(cls.working_dir) + + def setUp(self): + self.logcapture = LogCapture() + self.logcapture.setFormatter(logging.Formatter(LOG_FORMAT)) + logging.getLogger("gitlint").setLevel(logging.DEBUG) + logging.getLogger("gitlint").handlers = [self.logcapture] + DEPRECATION_LOG.handlers = [self.logcapture] + + # Make sure we don't propagate anything to child loggers, we need to do this explicitly here + # because if you run a specific test file like test_lint.py, we won't be calling the setupLogging() method + # in gitlint.cli that normally takes care of this + # Example test where this matters (for DEPRECATION_LOG): + # gitlint-core/gitlint/tests/rules/test_configuration_rules.py::ConfigurationRuleTests::test_ignore_by_title + logging.getLogger("gitlint").propagate = False + DEPRECATION_LOG.propagate = False + + # Make sure Deprecation has a clean config set at the start of each test. + # Tests that want to specifically test deprecation should override this. + Deprecation.config = LintConfig() + # Normally Deprecation only logs messages once per process. + # For tests we want to log every time, so we reset the warning_msgs set per test. + Deprecation.warning_msgs = set() + + @staticmethod + @contextlib.contextmanager + def tempdir(): + tmpdir = tempfile.mkdtemp() + try: + yield tmpdir + finally: + shutil.rmtree(tmpdir) + + @staticmethod + def get_sample_path(filename: str = "") -> str: + # Don't join up empty files names because this will add a trailing slash + if filename == "": + return BaseTestCase.SAMPLES_DIR + + return os.path.join(BaseTestCase.SAMPLES_DIR, filename) + + @staticmethod + def get_sample(filename: str = "") -> str: + """Read and return the contents of a file in gitlint/tests/samples""" + sample_path = BaseTestCase.get_sample_path(filename) + return Path(sample_path).read_text(encoding=FILE_ENCODING) + + @staticmethod + def patch_input(side_effect): + """Patches the built-in input() with a provided side-effect""" + module_path = "builtins.input" + patched_module = patch(module_path, side_effect=side_effect) + return patched_module + + @staticmethod + def get_expected( + filename: str = "", variable_dict: dict[str, Any] | None = None + ) -> str: + """Utility method to read an expected file from gitlint/tests/expected and return it as a string. + Optionally replace template variables specified by variable_dict.""" + expected_path = os.path.join(BaseTestCase.EXPECTED_DIR, filename) + expected = Path(expected_path).read_text(encoding=FILE_ENCODING) + + if variable_dict: + expected = expected.format(**variable_dict) + return expected + + @staticmethod + def get_user_rules_path(): + return os.path.join(BaseTestCase.SAMPLES_DIR, "user_rules") + + @staticmethod + def gitcontext(commit_msg_str, changed_files=None): + """Utility method to easily create gitcontext objects based on a given commit msg string and an optional set of + changed files""" + with patch("gitlint.git.git_commentchar") as comment_char: + comment_char.return_value = "#" + gitcontext = GitContext.from_commit_msg(commit_msg_str) + commit = gitcontext.commits[-1] + if changed_files: + changed_file_stats = { + filename: GitChangedFileStats(filename, 8, 3) + for filename in changed_files + } + commit.changed_files_stats = changed_file_stats + return gitcontext + + @staticmethod + def gitcommit(commit_msg_str, changed_files=None, **kwargs): + """Utility method to easily create git commit given a commit msg string and an optional set of changed files""" + gitcontext = BaseTestCase.gitcontext(commit_msg_str, changed_files) + commit = gitcontext.commits[-1] + for attr, value in kwargs.items(): + setattr(commit, attr, value) + return commit + + def assert_logged(self, expected): + """Asserts that the logs match an expected string or list. + This method knows how to compare a passed list of log lines as well as a newline concatenated string + of all loglines.""" + if isinstance(expected, list): + self.assertListEqual(self.logcapture.messages, expected) + else: + self.assertEqual("\n".join(self.logcapture.messages), expected) + + def assert_log_contains(self, line): + """Asserts that a certain line is in the logs""" + self.assertIn(line, self.logcapture.messages) + + def assertRaisesRegex( + self, expected_exception, expected_regex, *args, **kwargs + ): + """Pass-through method to unittest.TestCase.assertRaisesRegex that applies re.escape() to the passed + `expected_regex`. This is useful to automatically escape all file paths that might be present in the regex. + """ + return super().assertRaisesRegex( + expected_exception, re.escape(expected_regex), *args, **kwargs + ) + + def clearlog(self): + """Clears the log capture""" + self.logcapture.clear() + + @contextlib.contextmanager + def assertRaisesMessage(self, expected_exception, expected_msg): + """Asserts an exception has occurred with a given error message""" + try: + yield + except expected_exception as exc: + exception_msg = str(exc) + if exception_msg != expected_msg: # pragma: nocover + error = f"Right exception, wrong message:\n got: {exception_msg}\n expected: {expected_msg}" + raise self.fail(error) from exc + # else: everything is fine, just return + return + except Exception as exc: # pragma: nocover + raise self.fail( + f"Expected '{expected_exception.__name__}' got '{exc.__class__.__name__}'" + ) from exc + + # No exception raised while we expected one + raise self.fail( + f"Expected to raise {expected_exception.__name__}, didn't get an exception at all" + ) # pragma: nocover + + def object_equality_test(self, obj, attr_list, ctor_kwargs=None): + """Helper function to easily implement object equality tests. + Creates an object clone for every passed attribute and checks for (in)equality + of the original object with the clone based on those attributes' values. + This function assumes all attributes in `attr_list` can be passed to the ctor of `obj.__class__`. + """ + if not ctor_kwargs: + ctor_kwargs = {} + + attr_kwargs = {} + for attr in attr_list: + attr_kwargs[attr] = getattr(obj, attr) + + # For every attr, clone the object and assert the clone and the original object are equal + # Then, change the current attr and assert objects are unequal + for attr in attr_list: + attr_kwargs_copy = copy.deepcopy(attr_kwargs) + attr_kwargs_copy.update(ctor_kwargs) + clone = obj.__class__(**attr_kwargs_copy) + self.assertEqual(obj, clone) + + # Change attribute and assert objects are different (via both attribute set and ctor) + setattr(clone, attr, "föo") + self.assertNotEqual(obj, clone) + attr_kwargs_copy[attr] = "föo" + + self.assertNotEqual(obj, obj.__class__(**attr_kwargs_copy)) + + +class LogCapture(logging.Handler): + """Mock logging handler used to capture any log messages during tests.""" + + def __init__(self, *args, **kwargs): + logging.Handler.__init__(self, *args, **kwargs) + self.messages = [] + + def emit(self, record): + self.messages.append(self.format(record)) + + def clear(self): + self.messages = [] diff --git a/tests/test_define_impact.py b/tests/test_define_impact.py new file mode 100644 index 0000000..a28d248 --- /dev/null +++ b/tests/test_define_impact.py @@ -0,0 +1,76 @@ +from pathlib import Path + +from gitlint.config import LintConfig +from gitlint.lint import GitLinter + +from .base import BaseTestCase + +MESSAGE_HEAD = """Create cool feature + +This is the coolest feature of them all. +""" + +MESSAGE_TEMPLATE = """{head} + +Impact: {impact} +""" + + +class TestImpact(BaseTestCase): + # Based on + + def _get_linter(self) -> GitLinter: + config = LintConfig() + config.extra_path = Path(__file__).parent.parent / 'rules' + return GitLinter(config) + + def _test_impact(self, impact: str): + linter = self._get_linter() + gitcontext = self.gitcontext( + MESSAGE_TEMPLATE.format(head=MESSAGE_HEAD, impact=impact) + ) + results = linter.lint(gitcontext.commits[-1]) + # FIXME: Should we ensure that our custom rule was loaded? + assert len(results) == 0 + + def test_impact_major(self): + """Test that valid impact has been set""" + self._test_impact('major') + + def test_impact_minor(self): + """Test that valid impact has been set""" + self._test_impact('major') + + def test_impact_patch(self): + """Test that valid impact has been set""" + self._test_impact('patch') + + def _test_invalid_impact(self, impact: str): + """Test that invalid impact is not valid""" + linter = self._get_linter() + gitcontext = self.gitcontext( + MESSAGE_TEMPLATE.format(head=MESSAGE_HEAD, impact=impact) + ) + results = linter.lint(gitcontext.commits[-1]) + assert len(results) > 0 + assert 'SERAVO0001' in [x.rule_id for x in results] + + def test_invalid_impact_mainor(self): + self._test_invalid_impact('mainor') + + def test_invalid_impact_empty(self): + self._test_invalid_impact('') + + def test_invalid_impact_aku(self): + self._test_invalid_impact('aku') + + def test_invalid_impact_patchx(self): + self._test_invalid_impact('patchx') + + def test_missing_impact(self): + """Test that missing impact is not valid""" + linter = self._get_linter() + gitcontext = self.gitcontext(MESSAGE_HEAD) + results = linter.lint(gitcontext.commits[-1]) + assert len(results) > 0 + assert 'SERAVO0001' in [x.rule_id for x in results]