Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding automation to add boilerplate license/copyrights headers #4077

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions cmd/k-license/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# k-license

## Prepends project files with given template.

* Can be used for adding licence or copyright information on src files of project.
* Skip directory, if template (as provided) already present
* Supports Golang/Python source files, Dockerfile, Makefiles and bash scripts

## Build

```
go build -o k-license k-license.go
```

Example
To Apply header from ./boilerplate
folder default

```
$ go run k-license.go add --help
Add Headers to files

Usage:
k-license add [flags]

Flags:
--confirm
-e, --exclude strings comma-separated list of directories to exclude (default [external/bazel_tools,.git,node_modules,_output,third_party,vendor,verify/boilerplate/test])
-h, --help help for add
--path string Defaults to Current directory (default ".")
--templates string directory containing license templates (default "../../hack/boilerplate")

```

## Help

```
$ go run k-license.go --help
Tool for Adding license Headers

Usage:
k-license [command]

Available Commands:
add Add Headers to files
help Help about any command

Flags:
-h, --help help for k-license

Use "k-license [command] --help" for more information about a command.

```
254 changes: 254 additions & 0 deletions cmd/k-license/k-license.go
mohitsharma-in marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
/*
Copyright 2023 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strconv"
"strings"
"time"

"github.com/spf13/cobra"
)

var excludeDirsLocations = []string{
"external/bazel_tools",
".git",
"node_modules",
"_output",
"third_party",
"vendor",
"verify/boilerplate/test",
}
var GENERATED_GO_MARKERS = [7]string{
"// Code generated by client-gen. DO NOT EDIT.",
"// Code generated by controller-gen. DO NOT EDIT.",
"// Code generated by counterfeiter. DO NOT EDIT.",
"// Code generated by deepcopy-gen. DO NOT EDIT.",
"// Code generated by informer-gen. DO NOT EDIT.",
"// Code generated by lister-gen. DO NOT EDIT.",
"// Code generated by protoc-gen-go. DO NOT EDIT.",
}

var codeFileExts = map[string]bool{
".go": true,
".c": true,
".h": true,
".ipynb": true,
".py": true,
".java": true,
".cpp": true,
".sh": true,
}

var buildFileExts = map[string]bool{
"Makefile": true,
"Dockerfile": true,
}

type Options struct {
templatesDir string
excludeDirs []string
path string
confirm bool
}
type templateFileType struct {
fileExtension string // store file extension strings like ".sh", "Makefile", etc.
templateFileName string // store template file names like "boilerplate.sh.txt", etc
}

var templateFileTypes = []templateFileType{
{".sh", "boilerplate.sh.txt"},
{"Makefile", "boilerplate.Makefile.txt"},
{"Dockerfile", "boilerplate.Dockerfile.txt"},
{".py", "boilerplate.py.txt"},
{".go", "boilerplate.go.txt"},
}

var opts = &Options{}

func main() {
rootCmd := &cobra.Command{
Use: "k-license",
Short: "Tool for Adding license Headers",
}
addCmd := &cobra.Command{
Use: "add",
Short: "Add Headers to files",
RunE: func(cmd *cobra.Command, args []string) error {
return opts.Run()
},
}

addCmd.Flags().StringVar(&opts.templatesDir, "templates", "../../hack/boilerplate", "directory containing license templates")
addCmd.Flags().StringSliceVarP(&opts.excludeDirs, "exclude", "e", excludeDirsLocations, "comma-separated list of directories to exclude")
addCmd.Flags().StringVar(&opts.path, "path", ".", "Defaults to Current directory")
addCmd.Flags().BoolVar(&opts.confirm, "confirm", false, "confirm actually adding license boilerplate to files")
rootCmd.AddCommand(addCmd)
if err := rootCmd.Execute(); err != nil {
os.Exit(1)
}

}
func (opts *Options) Run() error {
files := 0
fileList := make([]string, 0)
err := filepath.WalkDir(opts.path, func(path string, info fs.DirEntry, err error) error {
if info.IsDir() && containsExcluded(opts.excludeDirs, info.Name()) {
return filepath.SkipDir
}
if !info.IsDir() && (isCodeFile(path) || isBuildFile(path)) && !isGenerateFile(path) {
hasLic, err := hasLicense(path)
if !hasLic {
currentYear := strconv.Itoa(time.Now().Year())
if opts.confirm {
err := addLicense(path, opts.templatesDir, currentYear)
if err != nil {
return err
}
fmt.Printf("Modified %s file\n", path)
}
fileList = append(fileList, path)
files++
}
return err

}
if err != nil {
return err
}
return nil
})
if opts.confirm {
fmt.Printf("Modified %v files\n", files)
} else {
fmt.Printf("DRY RUN: No file changes will be made! To make file modifications, rerun the command with \"--confirm\" flag\n")
if files == 0 {
fmt.Printf("All files have appropriate License Headers. No changes required.\n")
}
if files > 0 {
fmt.Printf("%v files will be modified to add License Headers\n", files)
fmt.Printf("Listing files to be modified:\n")
for _, file := range fileList {
fmt.Printf("%s\n", file)
}
}

}
return err
}

// Looks for the Excluded files/directroy
func containsExcluded(list []string, str string) bool {
for _, item := range list {
if item == str {
fmt.Printf("Skipping %s as this is Part of exclude list\n", str)
return true
}
}
return false
}

// Check if the file is code File
func isCodeFile(path string) bool {
return codeFileExts[strings.ToLower(filepath.Ext(path))]
}

// Check if the file is build File
func isBuildFile(path string) bool {
return buildFileExts[filepath.Base(path)]
}

// Checks if the file is auto generated
func isGenerateFile(path string) bool {
data, err := os.ReadFile(path)
if err != nil {
return false
}
for _, ft := range GENERATED_GO_MARKERS {
if strings.Contains(string(data), ft) {
fmt.Printf("Skipping File: %s since this is autogenerated file \n", path)
return true
}
}
return false
}

// Checks for license in the file
func hasLicense(path string) (bool, error) {
data, err := os.ReadFile(path)
if err != nil {
return false, err
}
if strings.Contains(string(data), "Copyright") && strings.Contains(string(data), "Licensed under the Apache License") {
fmt.Printf("Skipping File: %s Already have templates added\n", path)
return true, nil
} else {
return false, nil
}
}

// Reads templates from directory
func getTemplateFile(path string) string {
for _, file := range templateFileTypes {
if strings.HasSuffix(path, file.fileExtension) || filepath.Base(path) == file.fileExtension {
return file.templateFileName
}
}
return "boilerplate.tf.txt"
}

// Adds License Headers
func addLicense(path, templatesDir, year string) error {
tmplData, err := os.ReadFile(filepath.Join(templatesDir, getTemplateFile(path)))
if err != nil {
return err
}
// Replace placeholders with actual values
tmpl := strings.ReplaceAll(string(tmplData), "YEAR", year)

if fileSize(path) {
codeData, err := os.ReadFile(path)
if err != nil {
return err
}
newData := append([]byte(tmpl), []byte("\n")...)
newData = append(newData, codeData...)
return os.WriteFile(path, newData, 0644)
}
return nil
}

// Check for empty file
func fileSize(path string) bool {
fileInfo, err := os.Stat(path)
if err != nil {
fmt.Println(err)
return false
}

if fileInfo.Size() == 0 {
fmt.Println("The file is empty No Modification Required")
return false
} else {
fmt.Println("The file is not empty")
return true
}
}
Loading