From d403b71dc766795437eaf01a002da7a75f109b76 Mon Sep 17 00:00:00 2001 From: Mohamed Elmoslemany <117270519+mo-c4t@users.noreply.github.com> Date: Wed, 10 Jul 2024 09:06:31 +0200 Subject: [PATCH] camino-license initial push (#1) * Initial Commit --- .github/workflows/build.yml | 24 +++ .github/workflows/lint.yml | 27 ++++ .github/workflows/test.yml | 23 +++ .gitignore | 5 + .golangci.yml | 181 ++++++++++++++++++++++ README.md | 63 +++++++- cmd/check.go | 48 ++++++ cmd/root.go | 25 +++ config.yaml.example | 32 ++++ go.mod | 19 +++ go.sum | 24 +++ main.go | 12 ++ pkg/camino-license/camino-license.go | 144 +++++++++++++++++ pkg/camino-license/camino-license_test.go | 98 ++++++++++++ pkg/config/config.go | 100 ++++++++++++ pkg/config/config_test.go | 45 ++++++ pkg/config_test.yaml | 22 +++ 17 files changed, 891 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/test.yml create mode 100644 .golangci.yml create mode 100644 cmd/check.go create mode 100644 cmd/root.go create mode 100644 config.yaml.example create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 pkg/camino-license/camino-license.go create mode 100644 pkg/camino-license/camino-license_test.go create mode 100644 pkg/config/config.go create mode 100644 pkg/config/config_test.go create mode 100644 pkg/config_test.yaml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..6658442 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,24 @@ +name: Build + +on: + pull_request: + branches: + - c4t + - dev + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.22 + cache: false + - name: run build + run: go build + diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..6f54807 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,27 @@ +name: Lint + +on: + pull_request: + branches: + - c4t + - dev + workflow_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.22 + cache: false + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v1.55 + - name: run lint + run: golangci-lint run --config .golangci.yml \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..a0d6578 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: Test + +on: + pull_request: + branches: + - c4t + - dev + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: 1.22 + cache: false + - name: run test + run: go test ./... \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6f6f5e6..9c3b6fb 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,8 @@ # Go workspace file go.work go.work.sum + +_test-files +.vscode + +camino-license diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..dbac84a --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,181 @@ +# https://golangci-lint.run/usage/configuration/ +run: + timeout: 10m + + # Enables skipping of directories: + # - vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + # Default: true + #deprecated + #skip-dirs-use-default: false + + # If set we pass it to "go list -mod={option}". From "go help modules": + # If invoked with -mod=readonly, the go command is disallowed from the implicit + # automatic updating of go.mod described above. Instead, it fails when any changes + # to go.mod are needed. This setting is most useful to check that go.mod does + # not need updates, such as in a continuous integration and testing system. + # If invoked with -mod=vendor, the go command assumes that the vendor + # directory holds the correct copies of dependencies and ignores + # the dependency descriptions in go.mod. + # + # Allowed values: readonly|vendor|mod + # By default, it isn't set. + modules-download-mode: readonly + +output: + # Make issues output unique by line. + # Default: true + uniq-by-line: false + +issues: + # Maximum issues count per one linter. + # Set to 0 to disable. + # Default: 50 + max-issues-per-linter: 0 + + # Enables skipping of directories: + # - vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + # Default: true + exclude-dirs-use-default: false + + # Maximum count of issues with the same text. + # Set to 0 to disable. + # Default: 3 + max-same-issues: 0 + +linters: + disable-all: true + enable: + - asciicheck + - bodyclose + - depguard + - dupword + - errcheck + - errorlint + - exportloopref + - forbidigo + - goconst + - gocritic + # - goerr113 + - gofmt + - gofumpt + - goimports + # - gomnd + - goprintffuncname + - gosec + - gosimple + - govet + - importas + - ineffassign + # - lll + - misspell + - nakedret + - noctx + - nolintlint + - perfsprint + - prealloc + - predeclared + - revive + - staticcheck + - stylecheck + - tagalign + - typecheck + - unconvert + - unparam + - unused + - usestdlibvars + - whitespace + +linters-settings: + depguard: + rules: + packages: + deny: + - pkg: "io/ioutil" + desc: io/ioutil is deprecated. Use package io or os instead. + - pkg: "github.com/stretchr/testify/assert" + desc: github.com/stretchr/testify/require should be used instead. + - pkg: "github.com/golang/mock/gomock" + desc: go.uber.org/mock/gomock should be used instead. + errorlint: + # Check for plain type assertions and type switches. + asserts: false + # Check for plain error comparisons. + comparison: false + forbidigo: + # Forbid the following identifiers (list of regexp). + forbid: + - 'require\.Error$(# ErrorIs should be used instead)?' + - 'require\.ErrorContains$(# ErrorIs should be used instead)?' + - 'require\.EqualValues$(# Equal should be used instead)?' + - 'require\.NotEqualValues$(# NotEqual should be used instead)?' + - '^(t|b|tb|f)\.(Fatal|Fatalf|Error|Errorf)$(# the require library should be used instead)?' + # Exclude godoc examples from forbidigo checks. + exclude_godoc_examples: false + goimports: + local-prefixes: github.com/ava-labs/avalanchego + gosec: + excludes: + - G107 # Url provided to HTTP request as taint input https://securego.io/docs/rules/g107 + importas: + # Do not allow unaliased imports of aliased packages. + no-unaliased: false + # Do not allow non-required aliases. + no-extra-aliases: false + # List of aliases + alias: + - pkg: github.com/ava-labs/avalanchego/utils/math + alias: safemath + revive: + rules: + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bool-literal-in-expr + - name: bool-literal-in-expr + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#early-return + - name: early-return + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines + - name: empty-lines + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-format + - name: string-format + disabled: false + arguments: + - ["fmt.Errorf[0]", "/.*%.*/", "no format directive, use errors.New instead"] + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#struct-tag + - name: struct-tag + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-naming + - name: unexported-naming + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unhandled-error + - name: unhandled-error + disabled: false + arguments: + - "fmt\\.Fprint" + - "fmt\\.Fprintf" + - "fmt\\.Print" + - "fmt\\.Printf" + - "fmt\\.Println" + - "math/rand\\.Read" + - "strings\\.Builder\\.WriteString" + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter + - name: unused-parameter + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver + - name: unused-receiver + disabled: false + # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#useless-break + - name: useless-break + disabled: false + staticcheck: + # https://staticcheck.io/docs/options#checks + checks: + - "all" + - "-SA6002" # Storing non-pointer values in sync.Pool allocates memory + - "-SA1019" # Using a deprecated function, variable, constant or field + tagalign: + align: true + sort: true + strict: true + order: + - serialize diff --git a/README.md b/README.md index 456f8c5..eaff192 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,63 @@ # camino-license -A go package to check license headers and update current year in licence header. +A go package to check license headers. + + +# Usage +`camino-license check --config=config.yaml FILES/DIRS` +It checks license headers in the specified files or directories according to the given configuration file. `FILES/DIRS` are space-separated strings of the path (absolute or relative). +Example: +`camino-license check --config=config.yaml camino-license/cmd camino-license/pkg/camino-license/config.go ` + + +# Configuration +This is an example of a configuration file: + +```yml +default-headers: + - name: avax + header: | + // Copyright (C) 2019-{YEAR}, Ava Labs, Inc. All rights reserved. + // See the file LICENSE for licensing terms. + + - name: avax-c4t + header: | + // Copyright (C) 2022-{YEAR}, Chain4Travel AG. All rights reserved. + // + // This file is a derived work, based on ava-labs code whose + // original notices appear below. + // + // It is distributed under the same license conditions as the + // original code from which it is derived. + // + // Much love to the original authors for their work. + // ********************************************************** + // Copyright (C) 2019-{YEAR}, Ava Labs, Inc. All rights reserved. + // See the file LICENSE for licensing terms. + +custom-headers: + - name: c4t + header: | + // Copyright (C) 2022-{YEAR}, Chain4Travel AG. All rights reserved. + // See the file LICENSE for licensing terms. + + include-paths: + - "./**/camino*.go" + - "./test" + + exclude-paths: + - "./**/camino_visitor2.go" + +``` +`default-headers`: If the file is not specified in the custom-headers paths, then it should contain one of the default headers + +`name`: Name to the header object. It must be unique. **(Required)** + +`header`: License header to be used. **(Required)** + +`custom-headers`: Headers to be used in certain files according to the `include-paths` and `exclude-paths`. + +`include-paths`: list of path pattern to identify files which should contain that custom license header. Use `**`to include all sub directories. Use `*` to include all matching pattern in the same directory. **(Required if custom-headers is specified)** + +`exclude-paths`: list of path pattern to identify files which will be excluded from `include-paths`. Use `**`to include all sub directories. Use `*` to include all matching pattern in the same directory. + +`{YEAR}`: It will be automatically replaced with current year when the check runs. diff --git a/cmd/check.go b/cmd/check.go new file mode 100644 index 0000000..149ecb2 --- /dev/null +++ b/cmd/check.go @@ -0,0 +1,48 @@ +// Copyright (C) 2022-2024, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package cmd + +import ( + "fmt" + + caminolicense "github.com/chain4travel/camino-license/pkg/camino-license" + config "github.com/chain4travel/camino-license/pkg/config" + "github.com/spf13/cobra" +) + +// checkCmd represents the check command +var checkCmd = &cobra.Command{ + Use: "check [FLAGS] FILES/DIRS", + Short: "camino-license to check license headers", + Long: `camino-license to check license headers if they are compatible with the templates definded in the configuration file`, + RunE: func(cmd *cobra.Command, args []string) error { + configFile, _ := cmd.Flags().GetString("config") + headersConfig, err := config.GetHeadersConfig(configFile) + if err != nil { + return err + } + h := caminolicense.CaminoLicenseHeader{Config: headersConfig} + wrongFiles, err := h.CheckLicense(args) + if err != nil { + filesNumber := len(wrongFiles) + if filesNumber == 1 { + fmt.Println("1 file has wrong License Headers:") + } else { + fmt.Println(filesNumber, "files have wrong License Headers:") + } + for _, f := range wrongFiles { + fmt.Println(f.File, " - Reason:", f.Reason) + } + return err + } + fmt.Println("Check has finished successfully. All files have correct License Headers.") + return nil + }, +} + +// adding flags and check to camino-license command +func init() { + checkCmd.Flags().StringP("config", "c", "config.yaml", "configuration yaml file path") + rootCmd.AddCommand(checkCmd) +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..7722e7a --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,25 @@ +// Copyright (C) 2022-2024, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +// camino-license command +var rootCmd = &cobra.Command{ + Use: "camino-license COMMAND [FLAGS] FILES/DIRS", + Short: "camino-license pkg to check license headers", + Long: `camino-license pkg to check license headers according to a given yaml configuration`, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/config.yaml.example b/config.yaml.example new file mode 100644 index 0000000..72687d4 --- /dev/null +++ b/config.yaml.example @@ -0,0 +1,32 @@ +default-headers: + - name: avax + header: | + // Copyright (C) 2019-{YEAR}, Ava Labs, Inc. All rights reserved. + // See the file LICENSE for licensing terms. + + - name: avax-c4t + header: | + // Copyright (C) 2022-{YEAR}, Chain4Travel AG. All rights reserved. + // + // This file is a derived work, based on ava-labs code whose + // original notices appear below. + // + // It is distributed under the same license conditions as the + // original code from which it is derived. + // + // Much love to the original authors for their work. + // ********************************************************** + // Copyright (C) 2019-{YEAR}, Ava Labs, Inc. All rights reserved. + // See the file LICENSE for licensing terms. + +custom-headers: + - name: c4t + header: | + // Copyright (C) 2022-{YEAR}, Chain4Travel AG. All rights reserved. + // See the file LICENSE for licensing terms. + + include-paths: + - "./**/camino*.go" + + exclude-paths: + - "./**/camino_visitor2.go" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3e729e5 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module github.com/chain4travel/camino-license + +go 1.22 + +require ( + github.com/pkg/errors v0.9.1 + github.com/spf13/cobra v1.8.0 + github.com/stretchr/testify v1.9.0 + github.com/yargevad/filepathx v1.0.0 + gopkg.in/yaml.v2 v2.4.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8ee2447 --- /dev/null +++ b/go.sum @@ -0,0 +1,24 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= +github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..465e33b --- /dev/null +++ b/main.go @@ -0,0 +1,12 @@ +// Copyright (C) 2022-2024, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package main + +import ( + "github.com/chain4travel/camino-license/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/pkg/camino-license/camino-license.go b/pkg/camino-license/camino-license.go new file mode 100644 index 0000000..333bdce --- /dev/null +++ b/pkg/camino-license/camino-license.go @@ -0,0 +1,144 @@ +// Copyright (C) 2022-2024, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package caminolicense + +import ( + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "time" + + config "github.com/chain4travel/camino-license/pkg/config" + "github.com/pkg/errors" + "github.com/yargevad/filepathx" +) + +type WrongLicenseHeader struct { + File string + Reason string +} + +type CaminoLicenseHeader struct { + Config config.HeadersConfig +} + +var CheckErr = errors.New("Some files has wrong License Header") + +const ( + defaultHeaderError = "File doesn't have the same License Header as any of the default headers defined in the configuration file" + customHeaderError = "File doesn't have the same License Header as Custom Header: " +) + +// public function to start checking for license headers in a list of files or directories +func (h CaminoLicenseHeader) CheckLicense(files []string) ([]WrongLicenseHeader, error) { + var wrongFiles []WrongLicenseHeader + for _, f := range files { + info, err := os.Stat(f) + if err != nil { + wrongFiles = append(wrongFiles, WrongLicenseHeader{f, "File doesn't exist"}) + continue + } + + if info.IsDir() { + pathFiles, filePathErr := filepathx.Glob(f + "/**/*.go") + if filePathErr != nil { + wrongFiles = append(wrongFiles, WrongLicenseHeader{f, "Cannot find .go files under this directory"}) + continue + } + for _, path := range pathFiles { + // TODO: set license exclusions to be configured in the configuration file + match, matchErr := filepath.Match("mock_*.go", filepath.Base(path)) + if strings.HasSuffix(path, ".pb.go") || matchErr != nil || match { + continue + } + isWrong, wrongFile := h.checkFileLicense(path) + if isWrong { + wrongFiles = append(wrongFiles, wrongFile) + } + } + } else { + isWrong, wrongFile := h.checkFileLicense(f) + if isWrong { + wrongFiles = append(wrongFiles, wrongFile) + } + } + } + + if len(wrongFiles) > 0 { + return wrongFiles, CheckErr + } + + return wrongFiles, nil +} + +// To check if a file should have a custom license header or one of the default ones +func (h CaminoLicenseHeader) checkFileLicense(f string) (bool, WrongLicenseHeader) { + isCustomHeader, headerName, header := h.checkCustomHeader(f) + if isCustomHeader { + correctLicense, reason := verifyCustomLicenseHeader(f, headerName, header) + if !correctLicense { + return true, WrongLicenseHeader{f, reason} + } + } else { + correctLicense, reason := h.verifyDefaultLicenseHeader(f) + if !correctLicense { + return true, WrongLicenseHeader{f, reason} + } + } + return false, WrongLicenseHeader{} +} + +// to check if the file is included in a custom header path +func (h CaminoLicenseHeader) checkCustomHeader(file string) (bool, string, string) { + for _, customHeader := range h.Config.CustomHeaders { + absFile, fileErr := filepath.Abs(file) + if fileErr != nil { + absFile = file + } + if slices.Contains(customHeader.AllFiles, absFile) && !slices.Contains(customHeader.ExcludedFiles, absFile) { + return true, customHeader.Name, customHeader.Header + } + } + return false, "", "" +} + +// to verify if a custom license header from the configuration is similar to the one in the file. +func verifyCustomLicenseHeader(file string, headerName string, header string) (bool, string) { + bytes, err := os.ReadFile(file) + if err != nil { + return false, fmt.Sprintf("Cannot read file: %s", err) + } + content := string(bytes) + currentYear := time.Now().Format("2006") + + header = strings.ReplaceAll(header, "{YEAR}", currentYear) + + if strings.HasPrefix(content, header) { + return true, "" + } + return false, customHeaderError + headerName +} + +// to verify if any of the default license headers from the configuration is similar to the one in the file. +func (h CaminoLicenseHeader) verifyDefaultLicenseHeader(file string) (bool, string) { + bytes, err := os.ReadFile(file) + if err != nil { + return false, fmt.Sprintf("Cannot read file: %s", err) + } + content := string(bytes) + + for _, defaultHeader := range h.Config.DefaultHeaders { + header := defaultHeader.Header + currentYear := time.Now().Format("2006") + header = strings.ReplaceAll(header, "{YEAR}", currentYear) + + if strings.HasPrefix(content, header) { + return true, "" + } + } + + return false, defaultHeaderError +} diff --git a/pkg/camino-license/camino-license_test.go b/pkg/camino-license/camino-license_test.go new file mode 100644 index 0000000..b112666 --- /dev/null +++ b/pkg/camino-license/camino-license_test.go @@ -0,0 +1,98 @@ +// Copyright (C) 2022-2024, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package caminolicense + +import ( + "fmt" + "os" + "testing" + "time" + + "github.com/stretchr/testify/require" + + config "github.com/chain4travel/camino-license/pkg/config" +) + +var headersConfig = config.HeadersConfig{ + DefaultHeaders: []config.DefaultHeader{ + { + Name: "l1", + Header: "// Copyright (C) 2022-{YEAR}, Chain4Travel AG. All rights reserved.\n// L1\n", + }, + { + Name: "l2", + Header: "// Copyright (C) 2022-{YEAR}, Chain4Travel AG. All rights reserved.\n// L2\n", + }, + }, CustomHeaders: []config.CustomHeader{ + { + Name: "l3", + Header: "// Copyright (C) 2022-{YEAR}, Chain4Travel AG. All rights reserved.\n// L3\n", + IncludePaths: []string{"./**/camino*.go"}, + ExcludePaths: []string{"./**/camino_test_exclude.go"}, + }, + }, +} + +func TestCorrectDefaultLicense(t *testing.T) { + require.NoError(t, os.WriteFile("./test_correct_default_1.go", []byte(fmt.Sprintf("// Copyright (C) 2022-%d, Chain4Travel AG. All rights reserved.\n// L1\n\n package caminolicense", time.Now().Year())), 0o600)) + h := CaminoLicenseHeader{Config: headersConfig} + wrongFiles, err := h.CheckLicense([]string{"./test_correct_default_1.go"}) + require.NoError(t, err) + require.Empty(t, wrongFiles) + require.NoError(t, os.Remove("./test_correct_default_1.go")) + require.NoError(t, os.WriteFile("./test_correct_default_2.go", []byte(fmt.Sprintf("// Copyright (C) 2022-%d, Chain4Travel AG. All rights reserved.\n// L2\n\n package caminolicense", time.Now().Year())), 0o600)) + wrongFiles2, err2 := h.CheckLicense([]string{"./test_correct_default_2.go"}) + require.NoError(t, err2) + require.Empty(t, wrongFiles2) + require.NoError(t, os.Remove("./test_correct_default_2.go")) +} + +func TestWrongDefaultLicense(t *testing.T) { + require.NoError(t, os.WriteFile("./test_wrong_default.go", []byte(fmt.Sprintf("// Copyright (C) 2022-%d, Chain4Travel AG. All rights reserved.\n// Wrong License\n\n package caminolicense", time.Now().Year())), 0o600)) + h := CaminoLicenseHeader{Config: headersConfig} + wrongFiles, err := h.CheckLicense([]string{"./test_wrong_default.go"}) + require.ErrorIs(t, err, CheckErr) + expectedWrongFiles := []WrongLicenseHeader{ + { + File: "./test_wrong_default.go", + Reason: defaultHeaderError, + }, + } + require.Equal(t, expectedWrongFiles, wrongFiles) + require.NoError(t, os.Remove("./test_wrong_default.go")) +} + +func TestCorrectCustomLicense(t *testing.T) { + require.NoError(t, os.WriteFile("./camino_test_correct_custom.go", []byte(fmt.Sprintf("// Copyright (C) 2022-%d, Chain4Travel AG. All rights reserved.\n// L3\n\n package caminolicense", time.Now().Year())), 0o600)) + require.NoError(t, os.WriteFile("./camino_test_exclude.go", []byte(fmt.Sprintf("// Copyright (C) 2022-%d, Chain4Travel AG. All rights reserved.\n// L1\n\n package caminolicense", time.Now().Year())), 0o600)) + headersConfig2, _ := config.GetHeadersConfig("../config_test.yaml") + h := CaminoLicenseHeader{Config: headersConfig2} + wrongFiles, err := h.CheckLicense([]string{"./camino_test_correct_custom.go", "./camino_test_exclude.go"}) + require.NoError(t, err) + require.Empty(t, wrongFiles) + require.NoError(t, os.Remove("./camino_test_correct_custom.go")) + require.NoError(t, os.Remove("./camino_test_exclude.go")) +} + +func TestWrongCustomLicense(t *testing.T) { + require.NoError(t, os.WriteFile("./camino_test_exclude.go", []byte(fmt.Sprintf("// Copyright (C) 2022-%d, Chain4Travel AG. All rights reserved.\n// L3\n\n package caminolicense", time.Now().Year())), 0o600)) + require.NoError(t, os.WriteFile("./camino_test_wrong_custom.go", []byte(fmt.Sprintf("// Copyright (C) 2022-%d, Chain4Travel AG. All rights reserved.\n// L1\n\n package caminolicense", time.Now().Year())), 0o600)) + headersConfig2, _ := config.GetHeadersConfig("../config_test.yaml") + h := CaminoLicenseHeader{Config: headersConfig2} + wrongFiles, err := h.CheckLicense([]string{"./camino_test_wrong_custom.go", "./camino_test_exclude.go"}) + require.ErrorIs(t, err, CheckErr) + expectedWrongFiles := []WrongLicenseHeader{ + { + File: "./camino_test_wrong_custom.go", + Reason: customHeaderError + headersConfig2.CustomHeaders[0].Name, + }, + { + File: "./camino_test_exclude.go", + Reason: defaultHeaderError, + }, + } + require.Equal(t, expectedWrongFiles, wrongFiles) + require.NoError(t, os.Remove("./camino_test_wrong_custom.go")) + require.NoError(t, os.Remove("./camino_test_exclude.go")) +} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..3e4778c --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,100 @@ +// Copyright (C) 2022-2024, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package config + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/yargevad/filepathx" + "gopkg.in/yaml.v2" +) + +type DefaultHeader struct { + Name string `yaml:"name"` + Header string `yaml:"header"` +} + +type CustomHeader struct { + Name string `yaml:"name"` + Header string `yaml:"header"` + IncludePaths []string `yaml:"include-paths"` + ExcludePaths []string `yaml:"exclude-paths"` + AllFiles []string + ExcludedFiles []string +} + +type HeadersConfig struct { + DefaultHeaders []DefaultHeader `yaml:"default-headers"` + CustomHeaders []CustomHeader `yaml:"custom-headers"` +} + +// read configuration file +func GetHeadersConfig(configPath string) (HeadersConfig, error) { + yamlFile, err := os.ReadFile(configPath) + if err != nil { + return HeadersConfig{}, fmt.Errorf("failed to read config file %s: %w", configPath, err) + } + headersConfig := &HeadersConfig{} + err = yaml.Unmarshal(yamlFile, headersConfig) + if err != nil { + return HeadersConfig{}, fmt.Errorf("failed to read config file %s: %w", configPath, err) + } + configAbsPath, err := filepath.Abs(configPath) + if err != nil { + fmt.Println("Error: Couldn't get the absolute path for the config file:", configPath) + configAbsPath = configPath + } + + for i, customHeader := range headersConfig.CustomHeaders { + includedFiles, err := getCustomHeaderIncludedFiles(customHeader, filepath.Dir(configAbsPath)) + if err != nil { + return HeadersConfig{}, fmt.Errorf("failed to read config file %s: %w", configPath, err) + } + headersConfig.CustomHeaders[i].AllFiles = includedFiles + + excludedFiles, err := getCustomHeaderExcludedFiles(customHeader, filepath.Dir(configAbsPath)) + if err != nil { + return HeadersConfig{}, fmt.Errorf("failed to read config file %s: %w", configPath, err) + } + headersConfig.CustomHeaders[i].ExcludedFiles = excludedFiles + } + return *headersConfig, nil +} + +// walk through directories of include-paths to get all possible files that matches the pattern +func getCustomHeaderIncludedFiles(customHeader CustomHeader, dir string) ([]string, error) { + var files []string + for _, path := range customHeader.IncludePaths { + absPath := path + if !filepath.IsAbs(path) { + absPath = filepath.Join(dir, path) + } + pathFiles, err := filepathx.Glob(absPath) + if err != nil { + return files, errors.New("Cannot get file matches of the custom header included path: " + path) + } + files = append(files, pathFiles...) + } + return files, nil +} + +// walk through directories of exclude-paths to get all possible files that matches the pattern +func getCustomHeaderExcludedFiles(customHeader CustomHeader, dir string) ([]string, error) { + var files []string + for _, path := range customHeader.ExcludePaths { + absPath := path + if !filepath.IsAbs(path) { + absPath = filepath.Join(dir, path) + } + pathFiles, err := filepathx.Glob(absPath) + if err != nil { + return files, errors.New("Cannot get file matches of the custom header excluded path: " + path) + } + files = append(files, pathFiles...) + } + return files, nil +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..75ceccc --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,45 @@ +// Copyright (C) 2022-2024, Chain4Travel AG. All rights reserved. +// See the file LICENSE for licensing terms. + +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestReadConfig(t *testing.T) { + headersConfig, err := GetHeadersConfig("../config_test.yaml") + require.NoError(t, err) + currentPath, _ := filepath.Abs("..") + expectedHeadersConfig := HeadersConfig{ + []DefaultHeader{ + { + Name: "l1", + Header: "// Copyright (C) 2022-{YEAR}, Chain4Travel AG. All rights reserved.\n// L1\n", + }, + { + Name: "l2", + Header: "// Copyright (C) 2022-{YEAR}, Chain4Travel AG. All rights reserved.\n// L2\n", + }, + }, + []CustomHeader{ + { + Name: "l3", + Header: "// Copyright (C) 2022-{YEAR}, Chain4Travel AG. All rights reserved.\n// L3\n", + IncludePaths: []string{"./**/camino*.go"}, + ExcludePaths: []string{"./**/camino_*exclude.go"}, + AllFiles: []string{currentPath + "/camino-license/camino-license.go", currentPath + "/camino-license/camino-license_test.go"}, + }, + }, + } + require.Equal(t, expectedHeadersConfig, headersConfig) +} + +func TestNoConfig(t *testing.T) { + _, err := GetHeadersConfig("../config2_test.yaml") + require.ErrorIs(t, err, os.ErrNotExist) +} diff --git a/pkg/config_test.yaml b/pkg/config_test.yaml new file mode 100644 index 0000000..e0878af --- /dev/null +++ b/pkg/config_test.yaml @@ -0,0 +1,22 @@ +default-headers: + - name: l1 + header: | + // Copyright (C) 2022-{YEAR}, Chain4Travel AG. All rights reserved. + // L1 + + - name: l2 + header: | + // Copyright (C) 2022-{YEAR}, Chain4Travel AG. All rights reserved. + // L2 + +custom-headers: + - name: l3 + header: | + // Copyright (C) 2022-{YEAR}, Chain4Travel AG. All rights reserved. + // L3 + + include-paths: + - "./**/camino*.go" + + exclude-paths: + - "./**/camino_*exclude.go"