├── .dependency_license ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── .pdd ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── check_coverage.sh ├── dependencies.sh ├── download-gitlint.sh ├── go.mod ├── go.sum ├── internal ├── commits │ ├── commits.go │ └── commits_test.go ├── issues │ ├── filters.go │ ├── filters_test.go │ ├── issues.go │ └── issues_test.go └── repo │ ├── repo.go │ └── repo_test.go ├── main.go └── release.sh /.dependency_license: -------------------------------------------------------------------------------- 1 | # weasel config 2 | 3 | README\.md, Docs 4 | 5 | # Remove false positives. 6 | \.gitignore, Ignore 7 | \.dependency_license, Ignore 8 | go\.mod, Ignore 9 | go\.sum, Ignore 10 | \.golangci\.yml, Ignore 11 | \.goreleaser\.yml, Ignore 12 | \.pdd, Ignore 13 | \.travis\.yml, Ignore 14 | \.idea, Ignore 15 | \.vscode, Ignore 16 | download-gitlint.sh, Ignore 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | 4 | " goreleaser 5 | dist/ 6 | 7 | " golangci-lint 8 | bin/ 9 | 10 | " binary created by "build" recipe 11 | gitlint 12 | 13 | " test coverage 14 | coverage.txt 15 | cov_check.txt 16 | 17 | " PDD output file 18 | puzzles.xml 19 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # options for analysis running 2 | run: 3 | # default concurrency is a available CPU number 4 | concurrency: 4 5 | 6 | # timeout for analysis, e.g. 30s, 5m, default is 1m 7 | deadline: 1m 8 | 9 | # exit code when at least one issue was found, default is 1 10 | issues-exit-code: 1 11 | 12 | # include test files or not, default is true 13 | tests: true 14 | 15 | # list of build tags, all linters use it. Default is empty list. 16 | build-tags: 17 | # - mytag 18 | 19 | # which dirs to skip: they won't be analyzed; 20 | # can use regexp here: generated.*, regexp is applied on full path; 21 | # default value is empty list, but next dirs are always skipped independently 22 | # from this option's value: 23 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 24 | skip-dirs: 25 | # - src/external_libs 26 | # - autogenerated_by_my_lib 27 | 28 | # which files to skip: they will be analyzed, but issues from them 29 | # won't be reported. Default value is empty list, but there is 30 | # no need to include all autogenerated files, we confidently recognize 31 | # autogenerated files. If it's not please let us know. 32 | skip-files: 33 | # - ".*\\.my\\.go$" 34 | # - lib/bad.go 35 | 36 | # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": 37 | # If invoked with -mod=readonly, the go command is disallowed from the implicit 38 | # automatic updating of go.mod described above. Instead, it fails when any changes 39 | # to go.mod are needed. This setting is most useful to check that go.mod does 40 | # not need updates, such as in a continuous integration and testing system. 41 | # If invoked with -mod=vendor, the go command assumes that the vendor 42 | # directory holds the correct copies of dependencies and ignores 43 | # the dependency descriptions in go.mod. 44 | # modules-download-mode: readonly|release|vendor 45 | # 46 | # @todo #1 Enable this configuration after this golangci-lint issue is 47 | # fixed: https://github.com/golangci/golangci-lint/issues/397. According 48 | # to the golangci-lint docs, this config is desirable since the CI build 49 | # should not be required to update go.mod. 50 | #modules-download-mode: readonly 51 | 52 | 53 | # output configuration options 54 | output: 55 | # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" 56 | format: colored-line-number 57 | 58 | # print lines of code with issue, default is true 59 | print-issued-lines: true 60 | 61 | # print linter name in the end of issue text, default is true 62 | print-linter-name: true 63 | 64 | 65 | # all available settings of specific linters 66 | linters-settings: 67 | errcheck: 68 | # report about not checking of errors in type assetions: `a := b.(MyStruct)`; 69 | # default is false: such cases aren't reported by default. 70 | check-type-assertions: true 71 | 72 | # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; 73 | # default is false: such cases aren't reported by default. 74 | check-blank: true 75 | 76 | # [deprecated] comma-separated list of pairs of the form pkg:regex 77 | # the regex is used to ignore names within pkg. (default "fmt:.*"). 78 | # see https://github.com/kisielk/errcheck#the-deprecated-method for details 79 | ignore: fmt:.*,io/ioutil:^Read.* 80 | 81 | # path to a file containing a list of functions to exclude from checking 82 | # see https://github.com/kisielk/errcheck#excluding-functions for details 83 | #exclude: /path/to/file.txt 84 | govet: 85 | # report about shadowed variables 86 | check-shadowing: true 87 | golint: 88 | # minimal confidence for issues, default is 0.8 89 | min-confidence: 0.8 90 | gofmt: 91 | # simplify code: gofmt with `-s` option, true by default 92 | simplify: true 93 | goimports: 94 | # put imports beginning with prefix after 3rd-party packages; 95 | # it's a comma-separated list of prefixes 96 | #local-prefixes: github.com/org/project 97 | gocyclo: 98 | # minimal code complexity to report, 30 by default (but we recommend 10-20) 99 | min-complexity: 5 100 | maligned: 101 | # print struct with more effective memory layout or not, false by default 102 | suggest-new: true 103 | dupl: 104 | # tokens count to trigger issue, 150 by default 105 | threshold: 100 106 | goconst: 107 | # minimal length of string constant, 3 by default 108 | min-len: 3 109 | # minimal occurrences count to trigger, 3 by default 110 | min-occurrences: 3 111 | depguard: 112 | list-type: blacklist 113 | include-go-root: false 114 | packages: 115 | - github.com/davecgh/go-spew/spew 116 | misspell: 117 | # Correct spellings using locale preferences for US or UK. 118 | # Default is to use a neutral variety of English. 119 | # Setting locale to US will correct the British spelling of 'colour' to 'color'. 120 | locale: US 121 | lll: 122 | # max line length, lines longer will be reported. Default is 120. 123 | # '\t' is counted as 1 character by default, and can be changed with the tab-width option 124 | line-length: 100 125 | # tab width in spaces. Default to 1. 126 | tab-width: 2 127 | unused: 128 | # treat code as a program (not a library) and report unused exported identifiers; default is false. 129 | # XXX: if you enable this setting, unused will report a lot of false-positives in text editors: 130 | # if it's called for subdir of a project it can't find funcs usages. All text editor integrations 131 | # with golangci-lint call it on a directory with the changed file. 132 | check-exported: true 133 | unparam: 134 | # Inspect exported functions, default is false. Set to true if no external program/library imports your code. 135 | # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: 136 | # if it's called for subdir of a project it can't find external interfaces. All text editor integrations 137 | # with golangci-lint call it on a directory with the changed file. 138 | check-exported: false 139 | nakedret: 140 | # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 141 | max-func-lines: 0 142 | prealloc: 143 | # XXX: we don't recommend using this linter before doing performance profiling. 144 | # For most programs usage of prealloc will be a premature optimization. 145 | 146 | # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. 147 | # True by default. 148 | simple: true 149 | range-loops: true # Report preallocation suggestions on range loops, true by default 150 | for-loops: true # Report preallocation suggestions on for loops, false by default 151 | gocritic: 152 | # Which checks should be enabled; can't be combined with 'disabled-checks'; 153 | # See https://go-critic.github.io/overview#checks-overview 154 | # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` 155 | # By default list of stable checks is used. 156 | #enabled-checks: 157 | # - rangeValCopy 158 | 159 | # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty 160 | #disabled-checks: 161 | # - regexpMust 162 | 163 | # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint` run to see all tags and checks. 164 | # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". 165 | enabled-tags: 166 | - diagnostic 167 | - style 168 | - performance 169 | - experimental 170 | - opinionated 171 | 172 | settings: # settings passed to gocritic 173 | captLocal: # must be valid enabled check name 174 | paramsOnly: true 175 | rangeValCopy: 176 | sizeThreshold: 32 177 | 178 | linters: 179 | enable-all: true 180 | # presets: 181 | # - bugs 182 | # - unused 183 | fast: true 184 | 185 | 186 | issues: 187 | # List of regexps of issue texts to exclude, empty list by default. 188 | # But independently from this option we use default exclude patterns, 189 | # it can be disabled by `exclude-use-default: false`. To list all 190 | # excluded by default patterns execute `golangci-lint run --help` 191 | exclude: 192 | # - abcdef 193 | 194 | # Independently from option `exclude` we use default exclude patterns, 195 | # it can be disabled by this option. To list all 196 | # excluded by default patterns execute `golangci-lint run --help`. 197 | # Default value for this option is true. 198 | exclude-use-default: false 199 | 200 | # Maximum issues count per one linter. Set to 0 to disable. Default is 50. 201 | max-per-linter: 0 202 | 203 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 204 | max-same-issues: 0 205 | 206 | # Show only new issues: if there are unstaged changes or untracked files, 207 | # only those changes are analyzed, else only changes in HEAD~ are analyzed. 208 | # It's a super-useful option for integration of golangci-lint into existing 209 | # large codebase. It's not practical to fix all existing issues at the moment 210 | # of integration: much better don't allow issues in new code. 211 | # Default is false. 212 | new: false 213 | 214 | # Show only new issues created after git revision `REV` 215 | # new-from-rev: REV 216 | 217 | # Show only new issues created in git patch with set file path. 218 | # new-from-patch: path/to/patch/file 219 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | #before: 4 | # hooks: 5 | # # you may remove this if you don't use vgo 6 | # - go mod download 7 | # # you may remove this if you don't need go generate 8 | # - go generate ./... 9 | 10 | project_name: go-gitlint 11 | before: 12 | hooks: 13 | - go mod download 14 | builds: 15 | - 16 | env: 17 | - CGO_ENABLED=0 18 | main: ./main.go 19 | binary: gitlint 20 | goos: 21 | - linux 22 | - windows 23 | - darwin 24 | goarch: 25 | - 386 26 | - amd64 27 | archive: 28 | replacements: 29 | darwin: osx 30 | 386: x86_32 31 | amd64: x86_64 32 | release: 33 | name_template: "{{.Tag}}" 34 | prerelease: auto 35 | changelog: 36 | filters: 37 | exclude: 38 | - '^docs:' 39 | - '^test:' 40 | -------------------------------------------------------------------------------- /.pdd: -------------------------------------------------------------------------------- 1 | --source=. 2 | --verbose 3 | --exclude dist/**/* 4 | --exclude bin/**/* 5 | --exclude gitlint 6 | --exclude coverage.txt 7 | --exclude .gitignore 8 | --exclude go.sum 9 | --exclude cov_check.txt 10 | --rule min-words:20 11 | --rule min-estimate:0 12 | --rule max-estimate:0 13 | 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: focal 2 | 3 | language: go 4 | go: 5 | - "1.16.x" 6 | 7 | # PDD needs ruby version 2.3 or above 8 | before_install: 9 | - rvm install 2.7.1 10 | 11 | jobs: 12 | include: 13 | - if: (type = pull_request) OR (type = push AND branch = master) 14 | script: make checks && 15 | bash <(curl -s https://codecov.io/bash) 16 | - if: tag IS present 17 | script: make checks && 18 | bash <(curl -s https://codecov.io/bash) && 19 | make release 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 [yyyy] [name of copyright owner] 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 George Aristy 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # 18 | # Supported Targets: 19 | # 20 | # build: builds the binary 21 | # test: runs tests 22 | # coverage: verifies test coverage 23 | # dependencies: ensures build dependencies 24 | # lint: runs linters 25 | # pdd: runs pdd (see https://github.com/yegor256/pdd) 26 | # checks: runs build+test+pdd+lint 27 | # release: releases to GitHub (requires GitHub token) 28 | # 29 | 30 | build: 31 | @echo "Building..." 32 | @go build -o gitlint main.go 33 | 34 | test: 35 | @echo "Running unit tests..." 36 | @go test -count=1 -race -cover -coverprofile=coverage.txt -covermode=atomic ./... | tee cov_check.txt 37 | 38 | coverage: 39 | @echo "Verifying test coverage..." 40 | @./check_coverage.sh 41 | 42 | dependencies: 43 | @echo "Ensuring dependencies..." 44 | @./dependencies.sh 45 | 46 | lint: dependencies 47 | @echo "Running linter..." 48 | @golangci-lint run 49 | 50 | pdd: dependencies 51 | @echo "Scanning for puzzles..." 52 | @pdd -q --file=puzzles.xml 53 | 54 | license: dependencies 55 | @echo "Verifying license headers..." 56 | @weasel 57 | 58 | checks: build lint pdd license test coverage 59 | 60 | release: 61 | @echo "Releasing..." 62 | @./release.sh 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Release](https://img.shields.io/github/release/llorllale/go-gitlint.svg?style=flat-square)](https://github.com/llorllale/go-gitlint/releases/latest) 2 | [![Build Status](https://travis-ci.com/llorllale/go-gitlint.svg?branch=master)](https://travis-ci.com/llorllale/go-gitlint) 3 | [![codecov](https://codecov.io/gh/llorllale/go-gitlint/branch/master/graph/badge.svg)](https://codecov.io/gh/llorllale/go-gitlint) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/llorllale/go-gitlint?style=flat-square)](https://goreportcard.com/report/github.com/llorllale/go-gitlint) 5 | [![codebeat badge](https://codebeat.co/badges/5d0a7cfd-3dfb-4cc9-bd4f-f310c02068eb)](https://codebeat.co/projects/github-com-llorllale-go-gitlint-master) 6 | [![Powered By: GoReleaser](https://img.shields.io/badge/powered%20by-goreleaser-green.svg)](https://github.com/goreleaser) 7 | [![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](http://godoc.org/github.com/llorllale/go-gitlint) 8 | [![PDD status](http://www.0pdd.com/svg?name=llorllale/go-gitlint)](http://www.0pdd.com/p?name=llorllale/go-gitlint) 9 | [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://raw.githubusercontent.com/llorllale/go-gitlint/master/LICENSE) 10 | 11 | # go-gitlint 12 | Lint your git! 13 | 14 | ## Usage 15 | ``` 16 | $ ./gitlint --help 17 | usage: gitlint [] 18 | 19 | Flags: 20 | --help Show context-sensitive help (also try --help-long and --help-man). 21 | --path="." Path to the git repo (default: "."). 22 | --subject-regex=".*" Commit subject line must conform to this regular expression (default: ".*"). 23 | --subject-maxlen=2147483646 Max length for commit subject line (default: math.MaxInt32 - 1). 24 | --subject-minlen=0 Min length for commit subject line (default: 0). 25 | --body-regex=".*" Commit message body must conform to this regular expression (default: ".*"). 26 | --body-maxlen=2147483646 Max length for commit body (default: math.MaxInt32 - 1) 27 | --since="1970-01-01" A date in "yyyy-MM-dd" format starting from which commits will be analyzed (default: "1970-01-01"). 28 | --msg-file="" Only analyze the commit message found in this file (default: ""). 29 | --max-parents=1 Max number of parents a commit can have in order to be analyzed (default: 1). Useful for excluding merge commits. 30 | --excl-author-names="$a" Don't lint commits with authors whose names match these comma-separated regular expressions (default: '$a'). 31 | --excl-author-emails="$a" Don't lint commits with authors whose emails match these comma-separated regular expressions (default: '$a'). 32 | ``` 33 | Additionally, it will look for configurations in a file `.gitlint` in the current directory if it exists. This file's format is just the same command line flags but each on a separate line. *Flags passed through the command line take precedence.* 34 | 35 | ### Integration 36 | 37 | #### With Git 38 | Lint your commit message when committing to your local branch. 39 | 40 | Add a `commit-msg` hook to your Git repo: 41 | 42 | 1. Create hook: `echo 'gitlint --msg-file=$1' > .git/hooks/commit-msg` 43 | 2. Make it executable: `chmod +x .git/hooks/commit-msg` 44 | 45 | Now your commits will be validated after saving and closing the commit message in your text editor. 46 | 47 | #### With GitHub Actions 48 | To integrate it as GitHub action into CI pipeline use [gitlint-action](https://github.com/g4s8/gitlint-action). 49 | 50 | #### With any CI pipeline 51 | 52 | Use [`download-gitlint.sh`](https://raw.githubusercontent.com/llorllale/go-gitlint/master/download-gitlint.sh): 53 | 54 | 1. `curl https://raw.githubusercontent.com/llorllale/go-gitlint/master/download-gitlint.sh > download-gitlint.sh` 55 | 2. `chmod +x download-gitlint.sh` 56 | 57 | Usage: 58 | ``` 59 | $ ./download-gitlint.sh -h 60 | ./download-gitlint.sh: download go binaries for llorllale/go-gitlint 61 | 62 | Usage: ./download-gitlint.sh [-b] bindir [-d] [tag] 63 | -b sets bindir or installation directory, Defaults to ./bin 64 | -d turns on debug logging 65 | [tag] is a tag from 66 | https://github.com/llorllale/go-gitlint/releases 67 | If tag is missing, then the latest will be used. 68 | 69 | Generated by godownloader 70 | https://github.com/goreleaser/godownloader 71 | ``` 72 | 73 | ### Exit codes 74 | 75 | `gitlint`'s exit code will equal the number of issues found with your commit(s). 76 | 77 | ## Motivation 78 | 79 | - [X] Validate format of commit message subject and body 80 | - [X] Lint commit msgs on varios development platforms (Windows, Linux, Mac) 81 | - [X] Configuration from file with cli args taking precedence 82 | - [X] [`commit-msg`](https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks) hook to validate my commit's msg 83 | - [X] Performance (because a slow pre-commit hook would render the git workflow unmanageable) 84 | - [X] My first Go project :) 85 | 86 | ## Contributing 87 | Fork this repo, make sure `make checks` works, **and then** open a PR. 88 | 89 | ## Build dependencies 90 | To run `make checks` you will need Go `1.16.x` and Ruby `2.x` (for `pdd` - see below). 91 | [Ruby Version Manager](https://rvm.io/) is highly recommended. 92 | 93 | The `make dependencies` recipe attempts to download and install the following: 94 | 95 | * [pdd](https://github.com/yegor256/pdd) 96 | * [golangci-lint](https://github.com/golangci/golangci-lint) v1.29.0 97 | * [weasel](https://github.com/comcast/weasel) 98 | 99 | -------------------------------------------------------------------------------- /check_coverage.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2019 George Aristy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | let THRESHOLD=80 19 | IGNORE=github.com/llorllale/go-gitlint 20 | 21 | let exit_code=0 22 | 23 | while read line; do 24 | pkg=$(echo $line | sed 's/\s\+/ /g' | sed 's/%//' | cut -d ' ' -f 2) 25 | if [[ "$(echo $line | grep 'no test files')" != "" && "$pkg" != "$IGNORE" ]]; then 26 | echo "No coverage for package [$pkg]" 27 | let exit_code++ 28 | elif [[ "$(echo $line | grep coverage)" != "" ]]; then 29 | cov=$(echo $line | sed 's/\s\+/ /g' | sed 's/%//' | cut -d ' ' -f 5) 30 | if [ 1 -eq $(echo "$THRESHOLD > $cov" | bc) ]; then 31 | echo "Coverage [$cov] for package [$pkg] is below threshold [$THRESHOLD]" 32 | let exit_code++ 33 | fi 34 | fi 35 | done < ./cov_check.txt 36 | 37 | exit $exit_code 38 | -------------------------------------------------------------------------------- /dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright 2019 George Aristy 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | ensureRuby2xInstalled() { 19 | result=$(ruby --version) 20 | 21 | if [ -z "$result" ]; then 22 | echo "Please install Ruby 2.x!" 23 | exit 1 24 | fi 25 | 26 | version=$(echo $result | cut -d " " -f 2) 27 | matched=$([[ $version =~ 2\..* ]] && echo matched) 28 | 29 | if [ -z "$matched" ]; then 30 | echo "You have Ruby $version installed - please install a 2.x version." 31 | exit 1 32 | fi 33 | } 34 | 35 | installPDD() { 36 | installed=$(pdd -h && echo yes) 37 | 38 | if [ -z "$installed" ]; then 39 | echo "PDD not found. Installing..." 40 | gem install pdd 41 | fi 42 | } 43 | 44 | installGolangCILint() { 45 | VERSION=1.29.0 46 | installed=$(golangci-lint --version) 47 | 48 | if [ -z "$installed" ]; then 49 | echo "golangci-lint not found. Installing..." 50 | curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v$VERSION 51 | 52 | return 0 53 | fi 54 | 55 | version=$(echo $installed | cut -d " " -f 4) 56 | matched=$([[ $version =~ $VERSION ]] && echo matched) 57 | 58 | if [ -z "$matched" ]; then 59 | echo "You have golangci-lint $version installed. Please install version v$VERSION." 60 | exit 1 61 | fi 62 | } 63 | 64 | installWeasel() { 65 | installed=$(weasel -v && echo yes) 66 | 67 | if [ -z "$installed" ]; then 68 | echo "weasel not found. Installing..." 69 | (cd $(mktemp -d) && go get github.com/comcast/weasel) 70 | fi 71 | } 72 | 73 | main() { 74 | ensureRuby2xInstalled 75 | installPDD 76 | installGolangCILint 77 | installWeasel 78 | } 79 | 80 | main 81 | -------------------------------------------------------------------------------- /download-gitlint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | # Code generated by godownloader on 2019-08-18T15:25:39Z. DO NOT EDIT. 4 | # 5 | 6 | usage() { 7 | this=$1 8 | cat </dev/null 125 | } 126 | echoerr() { 127 | echo "$@" 1>&2 128 | } 129 | log_prefix() { 130 | echo "$0" 131 | } 132 | _logp=6 133 | log_set_priority() { 134 | _logp="$1" 135 | } 136 | log_priority() { 137 | if test -z "$1"; then 138 | echo "$_logp" 139 | return 140 | fi 141 | [ "$1" -le "$_logp" ] 142 | } 143 | log_tag() { 144 | case $1 in 145 | 0) echo "emerg" ;; 146 | 1) echo "alert" ;; 147 | 2) echo "crit" ;; 148 | 3) echo "err" ;; 149 | 4) echo "warning" ;; 150 | 5) echo "notice" ;; 151 | 6) echo "info" ;; 152 | 7) echo "debug" ;; 153 | *) echo "$1" ;; 154 | esac 155 | } 156 | log_debug() { 157 | log_priority 7 || return 0 158 | echoerr "$(log_prefix)" "$(log_tag 7)" "$@" 159 | } 160 | log_info() { 161 | log_priority 6 || return 0 162 | echoerr "$(log_prefix)" "$(log_tag 6)" "$@" 163 | } 164 | log_err() { 165 | log_priority 3 || return 0 166 | echoerr "$(log_prefix)" "$(log_tag 3)" "$@" 167 | } 168 | log_crit() { 169 | log_priority 2 || return 0 170 | echoerr "$(log_prefix)" "$(log_tag 2)" "$@" 171 | } 172 | uname_os() { 173 | os=$(uname -s | tr '[:upper:]' '[:lower:]') 174 | case "$os" in 175 | cygwin_nt*) os="windows" ;; 176 | mingw*) os="windows" ;; 177 | msys_nt*) os="windows" ;; 178 | esac 179 | echo "$os" 180 | } 181 | uname_arch() { 182 | arch=$(uname -m) 183 | case $arch in 184 | x86_64) arch="amd64" ;; 185 | x86) arch="386" ;; 186 | i686) arch="386" ;; 187 | i386) arch="386" ;; 188 | aarch64) arch="arm64" ;; 189 | armv5*) arch="armv5" ;; 190 | armv6*) arch="armv6" ;; 191 | armv7*) arch="armv7" ;; 192 | esac 193 | echo ${arch} 194 | } 195 | uname_os_check() { 196 | os=$(uname_os) 197 | case "$os" in 198 | darwin) return 0 ;; 199 | dragonfly) return 0 ;; 200 | freebsd) return 0 ;; 201 | linux) return 0 ;; 202 | android) return 0 ;; 203 | nacl) return 0 ;; 204 | netbsd) return 0 ;; 205 | openbsd) return 0 ;; 206 | plan9) return 0 ;; 207 | solaris) return 0 ;; 208 | windows) return 0 ;; 209 | esac 210 | log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" 211 | return 1 212 | } 213 | uname_arch_check() { 214 | arch=$(uname_arch) 215 | case "$arch" in 216 | 386) return 0 ;; 217 | amd64) return 0 ;; 218 | arm64) return 0 ;; 219 | armv5) return 0 ;; 220 | armv6) return 0 ;; 221 | armv7) return 0 ;; 222 | ppc64) return 0 ;; 223 | ppc64le) return 0 ;; 224 | mips) return 0 ;; 225 | mipsle) return 0 ;; 226 | mips64) return 0 ;; 227 | mips64le) return 0 ;; 228 | s390x) return 0 ;; 229 | amd64p32) return 0 ;; 230 | esac 231 | log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" 232 | return 1 233 | } 234 | untar() { 235 | tarball=$1 236 | case "${tarball}" in 237 | *.tar.gz | *.tgz) tar -xzf "${tarball}" ;; 238 | *.tar) tar -xf "${tarball}" ;; 239 | *.zip) unzip "${tarball}" ;; 240 | *) 241 | log_err "untar unknown archive format for ${tarball}" 242 | return 1 243 | ;; 244 | esac 245 | } 246 | http_download_curl() { 247 | local_file=$1 248 | source_url=$2 249 | header=$3 250 | if [ -z "$header" ]; then 251 | code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") 252 | else 253 | code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") 254 | fi 255 | if [ "$code" != "200" ]; then 256 | log_debug "http_download_curl received HTTP status $code" 257 | return 1 258 | fi 259 | return 0 260 | } 261 | http_download_wget() { 262 | local_file=$1 263 | source_url=$2 264 | header=$3 265 | if [ -z "$header" ]; then 266 | wget -q -O "$local_file" "$source_url" 267 | else 268 | wget -q --header "$header" -O "$local_file" "$source_url" 269 | fi 270 | } 271 | http_download() { 272 | log_debug "http_download $2" 273 | if is_command curl; then 274 | http_download_curl "$@" 275 | return 276 | elif is_command wget; then 277 | http_download_wget "$@" 278 | return 279 | fi 280 | log_crit "http_download unable to find wget or curl" 281 | return 1 282 | } 283 | http_copy() { 284 | tmp=$(mktemp) 285 | http_download "${tmp}" "$1" "$2" || return 1 286 | body=$(cat "$tmp") 287 | rm -f "${tmp}" 288 | echo "$body" 289 | } 290 | github_release() { 291 | owner_repo=$1 292 | version=$2 293 | test -z "$version" && version="latest" 294 | giturl="https://github.com/${owner_repo}/releases/${version}" 295 | json=$(http_copy "$giturl" "Accept:application/json") 296 | test -z "$json" && return 1 297 | version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') 298 | test -z "$version" && return 1 299 | echo "$version" 300 | } 301 | hash_sha256() { 302 | TARGET=${1:-/dev/stdin} 303 | if is_command gsha256sum; then 304 | hash=$(gsha256sum "$TARGET") || return 1 305 | echo "$hash" | cut -d ' ' -f 1 306 | elif is_command sha256sum; then 307 | hash=$(sha256sum "$TARGET") || return 1 308 | echo "$hash" | cut -d ' ' -f 1 309 | elif is_command shasum; then 310 | hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 311 | echo "$hash" | cut -d ' ' -f 1 312 | elif is_command openssl; then 313 | hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 314 | echo "$hash" | cut -d ' ' -f a 315 | else 316 | log_crit "hash_sha256 unable to find command to compute sha-256 hash" 317 | return 1 318 | fi 319 | } 320 | hash_sha256_verify() { 321 | TARGET=$1 322 | checksums=$2 323 | if [ -z "$checksums" ]; then 324 | log_err "hash_sha256_verify checksum file not specified in arg2" 325 | return 1 326 | fi 327 | BASENAME=${TARGET##*/} 328 | want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) 329 | if [ -z "$want" ]; then 330 | log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" 331 | return 1 332 | fi 333 | got=$(hash_sha256 "$TARGET") 334 | if [ "$want" != "$got" ]; then 335 | log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" 336 | return 1 337 | fi 338 | } 339 | cat /dev/null < 1 { 74 | body = strings.Join(parts[1:], "") 75 | } 76 | 77 | return body 78 | } 79 | 80 | // In returns the commits in the repo. 81 | // @todo #4 These err checks are extremely annoying. Figure out 82 | // how to handle them elegantly and reduce the cyclo complexity 83 | // of this function (currently at 4). 84 | func In(repository repo.Repo) Commits { 85 | return func() []*Commit { 86 | r := repository() 87 | 88 | ref, err := r.Head() 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | iter, err := r.Log(&git.LogOptions{From: ref.Hash()}) 94 | if err != nil { 95 | panic(err) 96 | } 97 | 98 | commits := make([]*Commit, 0) 99 | 100 | err = iter.ForEach(func(c *object.Commit) error { 101 | commits = append( 102 | commits, 103 | &Commit{ 104 | Hash: c.Hash.String(), 105 | Message: c.Message, 106 | Date: c.Author.When, 107 | NumParents: len(c.ParentHashes), 108 | Author: &Author{ 109 | Name: c.Author.Name, 110 | Email: c.Author.Email, 111 | }, 112 | }, 113 | ) 114 | 115 | return nil 116 | }) 117 | if err != nil { 118 | panic(err) 119 | } 120 | 121 | return commits 122 | } 123 | } 124 | 125 | // Since returns commits authored since time t (format: yyyy-MM-dd). 126 | func Since(t string, cmts Commits) Commits { 127 | return filtered( 128 | func(c *Commit) bool { 129 | start, err := time.Parse("2006-01-02", t) 130 | if err != nil { 131 | panic(err) 132 | } 133 | return !c.Date.Before(start) 134 | }, 135 | cmts, 136 | ) 137 | } 138 | 139 | // NotAuthoredByNames filters out commits with authors whose names match any of the given patterns. 140 | func NotAuthoredByNames(patterns []string, cmts Commits) Commits { 141 | return filtered( 142 | func(c *Commit) bool { 143 | for _, p := range patterns { 144 | match, err := regexp.MatchString(p, c.Author.Name) 145 | if err != nil { 146 | panic(err) 147 | } 148 | if match { 149 | return false 150 | } 151 | } 152 | return true 153 | }, 154 | cmts, 155 | ) 156 | } 157 | 158 | // NotAuthoredByEmails filters out commits with authors whose emails match any 159 | // of the given patterns. 160 | func NotAuthoredByEmails(patterns []string, cmts Commits) Commits { 161 | return filtered( 162 | func(c *Commit) bool { 163 | for _, p := range patterns { 164 | match, err := regexp.MatchString(p, c.Author.Email) 165 | if err != nil { 166 | panic(err) 167 | } 168 | if match { 169 | return false 170 | } 171 | } 172 | return true 173 | }, 174 | cmts, 175 | ) 176 | } 177 | 178 | // WithMaxParents returns commits that have at most n number of parents. 179 | // Useful for excluding merge commits. 180 | func WithMaxParents(n int, cmts Commits) Commits { 181 | return filtered( 182 | func(c *Commit) bool { 183 | return c.NumParents <= n 184 | }, 185 | cmts, 186 | ) 187 | } 188 | 189 | // MsgIn returns a single fake commit with the message read from this reader. 190 | // This fake commit will have a fake hash and its timestamp will be time.Now(). 191 | func MsgIn(reader io.Reader) Commits { 192 | return func() []*Commit { 193 | b, err := ioutil.ReadAll(reader) 194 | if err != nil { 195 | panic(err) 196 | } 197 | 198 | return []*Commit{{ 199 | Hash: "fakehsh", 200 | Message: string(b), 201 | Date: time.Now(), 202 | }} 203 | } 204 | } 205 | 206 | func filtered(filter func(*Commit) bool, in Commits) (out Commits) { 207 | return func() []*Commit { 208 | f := make([]*Commit, 0) 209 | 210 | for _, c := range in() { 211 | if filter(c) { 212 | f = append(f, c) 213 | } 214 | } 215 | 216 | return f 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /internal/commits/commits_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 George Aristy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package commits_test 16 | 17 | import ( 18 | "fmt" 19 | "io/ioutil" 20 | "os" 21 | "path/filepath" 22 | "strings" 23 | "testing" 24 | "time" 25 | 26 | "github.com/google/uuid" 27 | "github.com/stretchr/testify/assert" 28 | "github.com/stretchr/testify/require" 29 | "gopkg.in/src-d/go-git.v4" 30 | "gopkg.in/src-d/go-git.v4/plumbing/object" 31 | 32 | "github.com/llorllale/go-gitlint/internal/commits" 33 | "github.com/llorllale/go-gitlint/internal/repo" 34 | ) 35 | 36 | func TestCommitID(t *testing.T) { 37 | const ID = "test ID" 38 | 39 | assert.Equal(t, 40 | (&commits.Commit{Hash: ID}).ID(), ID, 41 | "Commit.ID() must return the commit's hash") 42 | } 43 | 44 | func TestCommitShortID(t *testing.T) { 45 | const ID = "c26cf8af130955c5c67cfea96f9532680b963628" 46 | 47 | assert.Equal(t, 48 | (&commits.Commit{Hash: ID}).ShortID(), 49 | ID[:7], 50 | "Commit.ShortID() must equal the first 7 characters of the commit's hash") 51 | } 52 | 53 | func TestCommitSubject(t *testing.T) { 54 | const subject = "test subject" 55 | 56 | assert.Equal(t, 57 | (&commits.Commit{Message: subject + "\n\ntest body"}).Subject(), 58 | subject, 59 | `Commit.Subject() must return the substring before the first \n`) 60 | } 61 | 62 | func TestCommitBody(t *testing.T) { 63 | const body = "test body" 64 | 65 | assert.Equal(t, 66 | (&commits.Commit{Message: "test subject\n\n" + body}).Body(), 67 | body, 68 | `Commit.Body() must return the substring after the first \n\n`) 69 | } 70 | 71 | func TestIn(t *testing.T) { 72 | msgs := []string{"subject1\n\nbody1", "subject2\n\nbody2", "subject3\n\nbody3"} 73 | r := tmpRepo(t, msgs...) 74 | cmits := commits.In(r)() 75 | 76 | assert.Len(t, cmits, len(msgs), 77 | "commits.In() did not return the correct number of commits") 78 | 79 | for i, msg := range msgs { 80 | commit := cmits[len(cmits)-i-1] 81 | assert.Equal(t, msg, commit.Subject()+"\n\n"+commit.Body(), 82 | "commits.In() returned commits with incorrect message subjects or bodies") 83 | } 84 | } 85 | 86 | func TestSince(t *testing.T) { 87 | before, err := time.Parse("2006-01-02", "2017-10-25") 88 | require.NoError(t, err) 89 | 90 | since, err := time.Parse("2006-01-02", "2019-01-01") 91 | require.NoError(t, err) 92 | 93 | after, err := time.Parse("2006-01-02", "2019-03-03") 94 | require.NoError(t, err) 95 | 96 | cmits := commits.Since( 97 | "2019-01-01", 98 | func() []*commits.Commit { 99 | return []*commits.Commit{ 100 | {Date: before}, 101 | {Date: since}, 102 | {Date: after}, 103 | } 104 | }, 105 | )() 106 | 107 | assert.Len(t, cmits, 2) 108 | assert.Contains(t, cmits, &commits.Commit{Date: since}) 109 | assert.Contains(t, cmits, &commits.Commit{Date: after}) 110 | } 111 | 112 | func TestMsgIn(t *testing.T) { 113 | const message = "test subject\n\ntest body" 114 | 115 | cmits := commits.MsgIn(strings.NewReader(message))() 116 | 117 | assert.Len(t, cmits, 1) 118 | assert.Equal(t, "test subject", cmits[0].Subject()) 119 | assert.Equal(t, "test body", cmits[0].Body()) 120 | } 121 | 122 | func TestWithMaxParents(t *testing.T) { 123 | const max = 1 124 | 125 | cmits := commits.WithMaxParents(max, func() []*commits.Commit { 126 | return []*commits.Commit{ 127 | {NumParents: max}, 128 | {NumParents: 2}, 129 | {NumParents: 3}, 130 | } 131 | })() 132 | 133 | assert.Len(t, cmits, 1) 134 | assert.Equal(t, cmits[0].NumParents, max) 135 | } 136 | 137 | func TestNotAuthored(t *testing.T) { 138 | filtered := &commits.Commit{Author: randomAuthor()} 139 | expected := []*commits.Commit{ 140 | {Author: randomAuthor()}, 141 | {Author: randomAuthor()}, 142 | {Author: randomAuthor()}, 143 | {Author: randomAuthor()}, 144 | } 145 | 146 | actual := commits.NotAuthoredByNames( 147 | []string{filtered.Author.Name}, 148 | func() []*commits.Commit { return append(expected, filtered) }, 149 | )() 150 | 151 | assert.Equal(t, expected, actual) 152 | 153 | actual = commits.NotAuthoredByEmails( 154 | []string{filtered.Author.Email}, 155 | func() []*commits.Commit { return append(expected, filtered) }, 156 | )() 157 | 158 | assert.Equal(t, expected, actual) 159 | } 160 | 161 | func randomAuthor() *commits.Author { 162 | return &commits.Author{ 163 | Name: uuid.New().String(), 164 | Email: uuid.New().String() + "@test.com", 165 | } 166 | } 167 | 168 | // A git repo initialized and with one commit per each of the messages provided. 169 | // This repo is created in a temporary directory; use the cleanup function 170 | // to delete it afterwards. 171 | func tmpRepo(t *testing.T, msgs ...string) repo.Repo { 172 | folder, err := ioutil.TempDir( 173 | "", 174 | strings.ReplaceAll(uuid.New().String(), "-", ""), 175 | ) 176 | require.NoError(t, err) 177 | 178 | t.Cleanup(func() { 179 | require.NoError(t, os.RemoveAll(folder)) 180 | }) 181 | 182 | return func() *git.Repository { 183 | r, err := git.PlainInit(folder, false) 184 | require.NoError(t, err) 185 | 186 | wt, err := r.Worktree() 187 | require.NoError(t, err) 188 | 189 | for i, msg := range msgs { 190 | file := fmt.Sprintf("msg%d.txt", i) 191 | 192 | err = ioutil.WriteFile(filepath.Join(folder, file), []byte(msg), 0600) 193 | require.NoError(t, err) 194 | 195 | _, err = wt.Add(file) 196 | require.NoError(t, err) 197 | 198 | _, err = wt.Commit(msg, &git.CommitOptions{ 199 | Author: &object.Signature{ 200 | Name: "John Doe", 201 | Email: "john@doe.org", 202 | When: time.Now(), 203 | }, 204 | }) 205 | require.NoError(t, err) 206 | } 207 | 208 | return r 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /internal/issues/filters.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 George Aristy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package issues 16 | 17 | import ( 18 | "fmt" 19 | "regexp" 20 | 21 | "github.com/llorllale/go-gitlint/internal/commits" 22 | ) 23 | 24 | // Filter identifies an issue with a commit. 25 | // A filter returning a zero-valued Issue signals that it found no issue 26 | // with the commit. 27 | type Filter func(*commits.Commit) Issue 28 | 29 | // OfSubjectRegex tests a commit's subject with the regex. 30 | func OfSubjectRegex(regex string) Filter { 31 | return func(c *commits.Commit) Issue { 32 | var issue Issue 33 | 34 | matched, err := regexp.MatchString(regex, c.Subject()) 35 | if err != nil { 36 | panic(err) 37 | } 38 | 39 | if !matched { 40 | issue = Issue{ 41 | Desc: fmt.Sprintf("subject does not match regex [%s]", regex), 42 | Commit: *c, 43 | } 44 | } 45 | 46 | return issue 47 | } 48 | } 49 | 50 | // OfBodyRegex tests a commit's body with the regex. 51 | func OfBodyRegex(regex string) Filter { 52 | return func(c *commits.Commit) Issue { 53 | var issue Issue 54 | 55 | matched, err := regexp.MatchString(regex, c.Body()) 56 | if err != nil { 57 | panic(err) 58 | } 59 | 60 | if !matched { 61 | issue = Issue{ 62 | Desc: fmt.Sprintf("body does not conform to regex [%s]", regex), 63 | Commit: *c, 64 | } 65 | } 66 | 67 | return issue 68 | } 69 | } 70 | 71 | // OfSubjectMaxLength checks that a commit's subject does not exceed this length. 72 | func OfSubjectMaxLength(length int) Filter { 73 | return func(c *commits.Commit) Issue { 74 | var issue Issue 75 | 76 | if len(c.Subject()) > length { 77 | issue = Issue{ 78 | Desc: fmt.Sprintf("subject length exceeds max [%d]", length), 79 | Commit: *c, 80 | } 81 | } 82 | 83 | return issue 84 | } 85 | } 86 | 87 | // OfSubjectMinLength checks that a commit's subject's length is at least 88 | // of length min. 89 | func OfSubjectMinLength(min int) Filter { 90 | return func(c *commits.Commit) Issue { 91 | var issue Issue 92 | 93 | if len(c.Subject()) < min { 94 | issue = Issue{ 95 | Desc: fmt.Sprintf("subject length less than min [%d]", min), 96 | Commit: *c, 97 | } 98 | } 99 | 100 | return issue 101 | } 102 | } 103 | 104 | // OfBodyMaxLength checks that a commit's body's length doesn't exceed this 105 | // max number of characters. 106 | func OfBodyMaxLength(max int) Filter { 107 | return func(c *commits.Commit) Issue { 108 | var issue Issue 109 | 110 | if len(c.Body()) > max { 111 | issue = Issue{ 112 | Desc: fmt.Sprintf("body length exceeds max [%d]", max), 113 | Commit: *c, 114 | } 115 | } 116 | 117 | return issue 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /internal/issues/filters_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 George Aristy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package issues_test 16 | 17 | import ( 18 | "math" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/assert" 22 | 23 | "github.com/llorllale/go-gitlint/internal/commits" 24 | "github.com/llorllale/go-gitlint/internal/issues" 25 | ) 26 | 27 | func TestOfSubjectRegexMatch(t *testing.T) { 28 | assert.Zero(t, 29 | issues.OfSubjectRegex(`\(#\d+\) [\w ]{10,50}`)( 30 | &commits.Commit{ 31 | Message: "(#123) This is a good subject", 32 | }, 33 | ), 34 | "filter.OfSubjectRegex() must match if the commit's subject matches the regex", 35 | ) 36 | } 37 | 38 | func TestOfSubjectRegexNonMatch(t *testing.T) { 39 | assert.NotZero(t, 40 | issues.OfSubjectRegex(`\(#\d+\) [\w ]{,50}`)( 41 | &commits.Commit{ 42 | Message: "I break all the rules!", 43 | }, 44 | ), 45 | "filter.OfSubjectRegex() must not match if the commit's subject does not match the regex", 46 | ) 47 | } 48 | 49 | func TestOfBodyRegexMatch(t *testing.T) { 50 | assert.Zero(t, 51 | issues.OfBodyRegex(`^.{10,20}$`)( 52 | &commits.Commit{ 53 | Message: "subject\n\nBetween 10 and 20", 54 | }, 55 | ), 56 | "filter.OfBodyRegex() must match if the commit's subject matches the regex", 57 | ) 58 | } 59 | 60 | func TestOfBodyRegexNonMatch(t *testing.T) { 61 | assert.NotZero(t, 62 | issues.OfBodyRegex(`^.{10,20}$`)( 63 | &commits.Commit{ 64 | Message: "subject\n\nMore than twenty characters!", 65 | }, 66 | ), 67 | "filter.OfBodyRegex() must not match if the commit's subject does not match the regex", 68 | ) 69 | } 70 | 71 | func TestOfSubjectMaxLengthMatch(t *testing.T) { 72 | assert.NotZero(t, 73 | issues.OfSubjectMaxLength(5)( 74 | &commits.Commit{ 75 | Message: "very very very VERY long subject\n\nand body", 76 | }, 77 | ), 78 | "filter.OfSubjectMaxLength() must match if the commit's subject is too long", 79 | ) 80 | } 81 | 82 | func TestOfSubjectMaxLengthNonMatch(t *testing.T) { 83 | assert.Zero(t, 84 | issues.OfSubjectMaxLength(10)( 85 | &commits.Commit{ 86 | Message: "short\n\nmessage", 87 | }, 88 | ), 89 | "filter.OfSubjectMaxLength() must not match if the commit's subject is not too long", 90 | ) 91 | } 92 | 93 | func TestOfSubjectMinLengthMatch(t *testing.T) { 94 | assert.NotZero(t, 95 | issues.OfSubjectMinLength(10)( 96 | &commits.Commit{ 97 | Message: "short\n\nand body", 98 | }, 99 | ), 100 | "filter.OfSubjectMinLength() must match if the commit's subject is too short", 101 | ) 102 | } 103 | 104 | func TestOfSubjectMinLengthNonMatch(t *testing.T) { 105 | assert.Zero(t, 106 | issues.OfSubjectMinLength(10)( 107 | &commits.Commit{ 108 | Message: "not too short subject\n\nmessage", 109 | }, 110 | ), 111 | "filter.OfSubjectMinLength() must not match if the commit's subject is not too short", 112 | ) 113 | } 114 | 115 | func TestOfBodyMaxLengthMatch(t *testing.T) { 116 | assert.NotZero(t, 117 | issues.OfBodyMaxLength(1)( 118 | &commits.Commit{ 119 | Message: "subject\n\nclearly, this commit has a long body", 120 | }, 121 | ), 122 | ) 123 | } 124 | 125 | func TestOfBodyMaxLengthNonMatch(t *testing.T) { 126 | assert.Zero(t, 127 | issues.OfBodyMaxLength(math.MaxInt32)( 128 | &commits.Commit{ 129 | Message: "subject\n\nclearly, this commit cannot exceed this max", 130 | }, 131 | ), 132 | ) 133 | } 134 | -------------------------------------------------------------------------------- /internal/issues/issues.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 George Aristy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package issues provides filters for problems found in commit messages. 16 | package issues 17 | 18 | import ( 19 | "io" 20 | 21 | "github.com/fatih/color" 22 | "github.com/llorllale/go-gitlint/internal/commits" 23 | ) 24 | 25 | // Issue is a problem found with a commit. 26 | type Issue struct { 27 | Desc string 28 | Commit commits.Commit 29 | } 30 | 31 | // Issues is a collection of Issues. 32 | type Issues func() []Issue 33 | 34 | // Collected returns a collection of issues identified. 35 | func Collected(filters []Filter, cmts commits.Commits) Issues { 36 | return func() []Issue { 37 | issues := make([]Issue, 0) 38 | 39 | for _, c := range cmts() { 40 | for _, f := range filters { 41 | if issue := f(c); issue != (Issue{}) { 42 | issues = append(issues, issue) 43 | } 44 | } 45 | } 46 | 47 | return issues 48 | } 49 | } 50 | 51 | // Printed prints the issues to the writer. 52 | func Printed(w io.Writer, sep string, issues Issues) Issues { 53 | return func() []Issue { 54 | iss := issues() 55 | 56 | for idx := range iss { 57 | i := iss[idx] 58 | 59 | _, err := color.New(color.Bold).Fprintf(w, "%s: ", i.Commit.ShortID()) 60 | if err != nil { 61 | panic(err) 62 | } 63 | 64 | _, err = color.New(color.FgRed).Fprintf(w, "%s%s", i.Desc, sep) 65 | if err != nil { 66 | panic(err) 67 | } 68 | } 69 | 70 | return iss 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /internal/issues/issues_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 George Aristy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package issues_test 16 | 17 | import ( 18 | "fmt" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/assert" 22 | 23 | "github.com/llorllale/go-gitlint/internal/commits" 24 | "github.com/llorllale/go-gitlint/internal/issues" 25 | ) 26 | 27 | func TestCollected(t *testing.T) { 28 | expected := []*commits.Commit{ 29 | {Hash: "123"}, 30 | {Hash: "456"}, 31 | } 32 | isus := issues.Collected( 33 | []issues.Filter{ 34 | func(c *commits.Commit) issues.Issue { 35 | var issue issues.Issue 36 | 37 | if c.Hash == "123" || c.Hash == "456" { 38 | issue = issues.Issue{ 39 | Desc: "test", 40 | Commit: *c, 41 | } 42 | } 43 | 44 | return issue 45 | }, 46 | }, 47 | func() []*commits.Commit { 48 | return append(expected, &commits.Commit{Hash: "789"}) 49 | }, 50 | )() 51 | 52 | assert.Len(t, 53 | isus, 54 | 2, 55 | "issues.Collected() must return the filtered commits") 56 | 57 | for _, i := range isus { 58 | assert.Contains(t, 59 | expected, &i.Commit, 60 | "issues.Collected() must return the filtered commits") 61 | } 62 | } 63 | 64 | func TestPrinted(t *testing.T) { 65 | const sep = "-" 66 | 67 | isus := []issues.Issue{ 68 | { 69 | Desc: "issueA", 70 | Commit: commits.Commit{ 71 | Hash: "18045269d8d2fd8f53d01883c6c7b548d0b9e3ae", 72 | Message: "first commit", 73 | }, 74 | }, 75 | { 76 | Desc: "issueB", 77 | Commit: commits.Commit{ 78 | Hash: "4be918ff8bfc91de77a1462707a8d2eb30956f93", 79 | Message: "second commit", 80 | }, 81 | }, 82 | } 83 | 84 | var expected string 85 | 86 | for _, i := range isus { 87 | expected += fmt.Sprintf("%s: %s%s", i.Commit.ShortID(), i.Desc, sep) 88 | } 89 | 90 | writer := &mockWriter{} 91 | 92 | issues.Printed( 93 | writer, sep, 94 | func() []issues.Issue { 95 | return isus 96 | }, 97 | )() 98 | 99 | assert.Equal(t, 100 | expected, writer.msg, 101 | "issues.Printed() must join Commit.ShortID() and the Issue.Desc with the separator") 102 | } 103 | 104 | type mockWriter struct { 105 | msg string 106 | } 107 | 108 | func (m *mockWriter) Write(b []byte) (int, error) { 109 | m.msg += string(b) 110 | return len(b), nil 111 | } 112 | -------------------------------------------------------------------------------- /internal/repo/repo.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 George Aristy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package repo is the API for fetching repos. 16 | package repo 17 | 18 | import ( 19 | git "gopkg.in/src-d/go-git.v4" 20 | ) 21 | 22 | // Repo is an initialized git repository. 23 | type Repo func() *git.Repository 24 | 25 | // Filesystem is a pre-existing git repository on the filesystem 26 | // with directory as root. 27 | func Filesystem(directory string) Repo { 28 | return func() *git.Repository { 29 | repo, err := git.PlainOpen(directory) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | return repo 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /internal/repo/repo_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 George Aristy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package repo_test 16 | 17 | import ( 18 | "fmt" 19 | "io/ioutil" 20 | "os" 21 | "path/filepath" 22 | "strings" 23 | "testing" 24 | "time" 25 | 26 | "github.com/google/uuid" 27 | "github.com/stretchr/testify/assert" 28 | "github.com/stretchr/testify/require" 29 | "gopkg.in/src-d/go-git.v4" 30 | "gopkg.in/src-d/go-git.v4/plumbing/object" 31 | 32 | "github.com/llorllale/go-gitlint/internal/repo" 33 | ) 34 | 35 | func TestFilesystem(t *testing.T) { 36 | msgs := []string{"commit1", "commit2", "commit3"} 37 | r, path := tmpGitRepo(t, msgs...) 38 | test := repo.Filesystem(path)() 39 | 40 | head, err := test.Head() 41 | require.NoError(t, err) 42 | 43 | iter, err := r.Log(&git.LogOptions{From: head.Hash()}) 44 | require.NoError(t, err) 45 | 46 | err = iter.ForEach(func(c *object.Commit) error { 47 | assert.Contains(t, msgs, c.Message, 48 | "repo.Filesystem() did not return all commits") 49 | 50 | return nil 51 | }) 52 | require.NoError(t, err) 53 | } 54 | 55 | func tmpGitRepo(t *testing.T, msgs ...string) (r *git.Repository, folder string) { 56 | var err error 57 | 58 | folder, err = ioutil.TempDir( 59 | "", 60 | strings.ReplaceAll(uuid.New().String(), "-", ""), 61 | ) 62 | require.NoError(t, err) 63 | 64 | t.Cleanup(func() { 65 | require.NoError(t, os.RemoveAll(folder)) 66 | }) 67 | 68 | r, err = git.PlainInit(folder, false) 69 | require.NoError(t, err) 70 | 71 | wt, err := r.Worktree() 72 | require.NoError(t, err) 73 | 74 | for i, msg := range msgs { 75 | file := fmt.Sprintf("msg%d.txt", i) 76 | 77 | err = ioutil.WriteFile(filepath.Join(folder, file), []byte(msg), 0600) 78 | require.NoError(t, err) 79 | 80 | _, err = wt.Add(file) 81 | require.NoError(t, err) 82 | 83 | _, err = wt.Commit(msg, &git.CommitOptions{ 84 | Author: &object.Signature{ 85 | Name: "John Doe", 86 | Email: "john@doe.org", 87 | When: time.Now(), 88 | }, 89 | }) 90 | require.NoError(t, err) 91 | } 92 | 93 | return r, folder 94 | } 95 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 George Aristy 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "math" 19 | "os" 20 | "strconv" 21 | "strings" 22 | 23 | "github.com/llorllale/go-gitlint/internal/commits" 24 | "github.com/llorllale/go-gitlint/internal/issues" 25 | "github.com/llorllale/go-gitlint/internal/repo" 26 | kingpin "gopkg.in/alecthomas/kingpin.v2" 27 | ) 28 | 29 | // @todo #9 Global variables are a code smell (especially those in filterse.go). 30 | // They promote coupling across different components inside the same package. 31 | // Figure out a way to remove these global variables. Whatever command line 32 | // parser we choose should be able to auto-generate usage. 33 | var ( 34 | path = kingpin.Flag("path", `Path to the git repo (default: ".").`).Default(".").String() //nolint:lll,gochecknoglobals // https://github.com/llorllale/go-gitlint/issues/23 35 | subjectRegex = kingpin.Flag("subject-regex", `Commit subject line must conform to this regular expression (default: ".*").`).Default(".*").String() //nolint:lll,gochecknoglobals // https://github.com/llorllale/go-gitlint/issues/23 36 | subjectMaxLength = kingpin.Flag("subject-maxlen", "Max length for commit subject line (default: math.MaxInt32 - 1).").Default(strconv.Itoa(math.MaxInt32 - 1)).Int() //nolint:lll,gochecknoglobals // https://github.com/llorllale/go-gitlint/issues/23 37 | subjectMinLength = kingpin.Flag("subject-minlen", "Min length for commit subject line (default: 0).").Default("0").Int() //nolint:lll,gochecknoglobals // https://github.com/llorllale/go-gitlint/issues/23 38 | bodyRegex = kingpin.Flag("body-regex", `Commit message body must conform to this regular expression (default: ".*").`).Default(".*").String() //nolint:lll,gochecknoglobals // https://github.com/llorllale/go-gitlint/issues/23 39 | bodyMaxLength = kingpin.Flag("body-maxlen", `Max length for commit body (default: math.MaxInt32 - 1)`).Default(strconv.Itoa(math.MaxInt32 - 1)).Int() //nolint:lll,gochecknoglobals // https://github.com/llorllale/go-gitlint/issues/23 40 | since = kingpin.Flag("since", `A date in "yyyy-MM-dd" format starting from which commits will be analyzed (default: "1970-01-01").`).Default("1970-01-01").String() //nolint:lll,gochecknoglobals // https://github.com/llorllale/go-gitlint/issues/23 41 | msgFile = kingpin.Flag("msg-file", `Only analyze the commit message found in this file (default: "").`).Default("").String() //nolint:lll,gochecknoglobals // https://github.com/llorllale/go-gitlint/issues/23 42 | maxParents = kingpin.Flag("max-parents", `Max number of parents a commit can have in order to be analyzed (default: 1). Useful for excluding merge commits.`).Default("1").Int() //nolint:lll,gochecknoglobals // https://github.com/llorllale/go-gitlint/issues/23 43 | authorNames = kingpin.Flag("excl-author-names", "Don't lint commits with authors whose names match these comma-separated regular expressions (default: '$a').").Default("$a").String() //nolint:lll,gochecknoglobals // https://github.com/llorllale/go-gitlint/issues/23 44 | authorEmails = kingpin.Flag("excl-author-emails", "Don't lint commits with authors whose emails match these comma-separated regular expressions (default: '$a').").Default("$a").String() //nolint:lll,gochecknoglobals // https://github.com/llorllale/go-gitlint/issues/23 45 | ) 46 | 47 | func main() { 48 | configure() 49 | os.Exit( 50 | len( 51 | issues.Printed( 52 | os.Stdout, "\n", 53 | issues.Collected( 54 | []issues.Filter{ 55 | issues.OfSubjectRegex(*subjectRegex), 56 | issues.OfSubjectMaxLength(*subjectMaxLength), 57 | issues.OfSubjectMinLength(*subjectMinLength), 58 | issues.OfBodyRegex(*bodyRegex), 59 | issues.OfBodyMaxLength(*bodyMaxLength), 60 | }, 61 | try( 62 | len(*msgFile) > 0, 63 | func() commits.Commits { 64 | file, err := os.Open(*msgFile) 65 | if err != nil { 66 | panic(err) 67 | } 68 | return commits.MsgIn(file) 69 | }, 70 | func() commits.Commits { 71 | return commits.NotAuthoredByNames( 72 | strings.Split(*authorNames, ","), 73 | commits.NotAuthoredByEmails( 74 | strings.Split(*authorEmails, ","), 75 | commits.WithMaxParents( 76 | *maxParents, 77 | commits.Since( 78 | *since, 79 | commits.In( 80 | repo.Filesystem(*path), 81 | ), 82 | ), 83 | ), 84 | ), 85 | ) 86 | }, 87 | ), 88 | ), 89 | )(), 90 | ), 91 | ) 92 | } 93 | 94 | func configure() { 95 | const file = ".gitlint" 96 | 97 | args := os.Args[1:] 98 | 99 | if _, err := os.Stat(file); err == nil { 100 | config, err := kingpin.ExpandArgsFromFile(file) 101 | if err != nil { 102 | panic(err) 103 | } 104 | 105 | args = append(args, config...) 106 | } 107 | 108 | if _, err := kingpin.CommandLine.Parse(unique(args)); err != nil { 109 | panic(err) 110 | } 111 | } 112 | 113 | func unique(args []string) []string { 114 | u := make([]string, 0) 115 | flags := make([]string, 0) 116 | 117 | for _, a := range args { 118 | name := strings.Split(a, "=")[0] 119 | 120 | if !contains(name, flags) { 121 | u = append(u, a) 122 | flags = append(flags, name) 123 | } 124 | } 125 | 126 | return u 127 | } 128 | 129 | func contains(s string, strs []string) bool { 130 | for _, str := range strs { 131 | if s == str { 132 | return true 133 | } 134 | } 135 | 136 | return false 137 | } 138 | 139 | func try(cond bool, actual, dflt func() commits.Commits) commits.Commits { 140 | if cond { 141 | return actual() 142 | } 143 | 144 | return dflt() 145 | } 146 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 George Aristy 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | docker run --rm --privileged \ 18 | -v $PWD:/go/src/github.com/user/repo \ 19 | -v /var/run/docker.sock:/var/run/docker.sock \ 20 | -w /go/src/github.com/user/repo \ 21 | -e GITHUB_TOKEN \ 22 | -e GO111MODULE=on \ 23 | goreleaser/goreleaser:v0.101 release --rm-dist 24 | --------------------------------------------------------------------------------