mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
331 lines
8.3 KiB
Go
331 lines
8.3 KiB
Go
// Copyright 2018 Google LLC
|
|
//
|
|
// 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.
|
|
|
|
// This program ensures source code files have copyright license headers.
|
|
// See usage with "addlicense -h".
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"html/template"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
)
|
|
|
|
const helpText = `Usage: addlicense [flags] pattern [pattern ...]
|
|
|
|
The program ensures source code files have copyright license headers
|
|
by scanning directory patterns recursively.
|
|
|
|
It modifies all source files in place and avoids adding a license header
|
|
to any file that already has one.
|
|
|
|
The pattern argument can be provided multiple times, and may also refer
|
|
to single files.
|
|
|
|
Flags:
|
|
`
|
|
|
|
var (
|
|
holder = flag.String("c", "Gitpod GmbH", "copyright holder")
|
|
license = flag.String("l", "agpl", "license type: agpl")
|
|
licensef = flag.String("f", "", "license file")
|
|
year = flag.String("y", fmt.Sprint(time.Now().Year()), "copyright year(s)")
|
|
verbose = flag.Bool("v", false, "verbose mode: print the name of the files that are modified")
|
|
stdin = flag.Bool("s", false, "read paths to file that are modified from stdin")
|
|
checkonly = flag.Bool("check", false, "check only mode: verify presence of license headers and exit with non-zero code if missing")
|
|
remove = flag.Bool("remove", false, "remove header mode: if a license header is present, we'll remove it")
|
|
)
|
|
|
|
func main() {
|
|
flag.Usage = func() {
|
|
fmt.Fprintln(os.Stderr, helpText)
|
|
flag.PrintDefaults()
|
|
}
|
|
flag.Parse()
|
|
if flag.NArg() == 0 {
|
|
flag.Usage()
|
|
os.Exit(1)
|
|
}
|
|
|
|
data := ©rightData{
|
|
Year: *year,
|
|
Holder: *holder,
|
|
}
|
|
|
|
var t *template.Template
|
|
if *licensef != "" {
|
|
d, err := os.ReadFile(*licensef)
|
|
if err != nil {
|
|
log.Printf("license file: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
t, err = template.New("").Parse(string(d))
|
|
if err != nil {
|
|
log.Printf("license file: %v", err)
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
t = licenseTemplate[*license]
|
|
if t == nil {
|
|
log.Printf("unknown license: %s", *license)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// process at most 1000 files in parallel
|
|
ch := make(chan *file, 1000)
|
|
done := make(chan struct{})
|
|
go func() {
|
|
var wg errgroup.Group
|
|
for f := range ch {
|
|
f := f // https://golang.org/doc/faq#closures_and_goroutines
|
|
wg.Go(func() error {
|
|
if *checkonly {
|
|
// Check if file extension is known
|
|
lic, err := licenseHeader(f.path, t, data)
|
|
if err != nil {
|
|
log.Printf("%s: %v", f.path, err)
|
|
return err
|
|
}
|
|
if lic == nil { // Unknown fileExtension
|
|
return nil
|
|
}
|
|
// Check if file has a license
|
|
isMissingLicenseHeader, err := fileHasLicense(f.path)
|
|
if err != nil {
|
|
log.Printf("%s: %v", f.path, err)
|
|
return err
|
|
}
|
|
if isMissingLicenseHeader {
|
|
fmt.Printf("%s\n", f.path)
|
|
return errors.New("missing license header")
|
|
}
|
|
} else if *remove {
|
|
modified, haslic, err := removeLicense(f.path, f.mode, t, data)
|
|
if err != nil {
|
|
log.Printf("%s: %v", f.path, err)
|
|
return err
|
|
}
|
|
if haslic && !modified {
|
|
log.Printf("%s should have been modified but wasn't", f.path)
|
|
}
|
|
} else {
|
|
modified, err := addLicense(f.path, f.mode, t, data)
|
|
if err != nil {
|
|
log.Printf("%s: %v", f.path, err)
|
|
return err
|
|
}
|
|
if *verbose && modified {
|
|
log.Printf("%s modified", f.path)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
err := wg.Wait()
|
|
close(done)
|
|
if err != nil {
|
|
os.Exit(1)
|
|
}
|
|
}()
|
|
|
|
defer func() {
|
|
close(ch)
|
|
<-done
|
|
}()
|
|
|
|
if *stdin {
|
|
scanner := bufio.NewScanner(os.Stdin)
|
|
for scanner.Scan() {
|
|
path := scanner.Text()
|
|
stat, err := os.Stat(path)
|
|
if err != nil {
|
|
log.Printf("file %s does not exist", path)
|
|
}
|
|
ch <- &file{path, stat.Mode()}
|
|
}
|
|
return
|
|
}
|
|
|
|
for _, d := range flag.Args() {
|
|
walk(ch, d)
|
|
}
|
|
}
|
|
|
|
type file struct {
|
|
path string
|
|
mode os.FileMode
|
|
}
|
|
|
|
func walk(ch chan<- *file, start string) {
|
|
filepath.Walk(start, func(path string, fi os.FileInfo, err error) error {
|
|
if err != nil {
|
|
log.Printf("%s error: %v", path, err)
|
|
return nil
|
|
}
|
|
if fi.IsDir() {
|
|
return nil
|
|
}
|
|
ch <- &file{path, fi.Mode()}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func addLicense(path string, fmode os.FileMode, tmpl *template.Template, data *copyrightData) (bool, error) {
|
|
var lic []byte
|
|
var err error
|
|
lic, err = licenseHeader(path, tmpl, data)
|
|
if err != nil || lic == nil {
|
|
return false, err
|
|
}
|
|
|
|
b, err := os.ReadFile(path)
|
|
if err != nil || hasLicense(b) {
|
|
return false, err
|
|
}
|
|
|
|
line := hashBang(b)
|
|
if len(line) > 0 {
|
|
b = b[len(line):]
|
|
if line[len(line)-1] != '\n' {
|
|
line = append(line, '\n')
|
|
}
|
|
lic = append(line, lic...)
|
|
}
|
|
b = append(lic, b...)
|
|
return true, os.WriteFile(path, b, fmode)
|
|
}
|
|
|
|
func removeLicense(path string, fmode os.FileMode, tmpl *template.Template, data *copyrightData) (haslic bool, modified bool, err error) {
|
|
var lic []byte
|
|
lic, err = licenseHeader(path, tmpl, data)
|
|
if err != nil || lic == nil {
|
|
return false, false, err
|
|
}
|
|
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return false, false, err
|
|
}
|
|
olen := len(b)
|
|
if !hasLicense(b) {
|
|
return false, false, nil
|
|
}
|
|
|
|
lic = bytes.TrimSpace(lic)
|
|
|
|
b = bytes.ReplaceAll(b, []byte("Copyright (c) 2021 Gitpod GmbH."), []byte("Copyright (c) 2022 Gitpod GmbH."))
|
|
b = bytes.ReplaceAll(b, []byte("Copyright (c) 2020 Gitpod GmbH."), []byte("Copyright (c) 2022 Gitpod GmbH."))
|
|
b = bytes.ReplaceAll(b, bytes.TrimSpace(lic), nil)
|
|
if len(b) >= olen {
|
|
fmt.Println(string(lic))
|
|
fmt.Println("---")
|
|
fmt.Println(string(b[:len(lic)]))
|
|
fmt.Println("===")
|
|
}
|
|
|
|
return len(b) < olen, true, os.WriteFile(path, b, fmode)
|
|
}
|
|
|
|
// fileHasLicense reports whether the file at path contains a license header.
|
|
func fileHasLicense(path string) (bool, error) {
|
|
b, err := os.ReadFile(path)
|
|
if err != nil || hasLicense(b) {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func licenseHeader(path string, tmpl *template.Template, data *copyrightData) ([]byte, error) {
|
|
var lic []byte
|
|
var err error
|
|
switch fileExtension(path) {
|
|
default:
|
|
return nil, nil
|
|
case ".c", ".h":
|
|
lic, err = prefix(tmpl, data, "/*", " * ", " */")
|
|
case ".js", ".mjs", ".cjs", ".jsx", ".tsx", ".css", ".tf", ".ts", ".jsonnet", ".libsonnet":
|
|
lic, err = prefix(tmpl, data, "/**", " * ", " */")
|
|
case ".cc", ".cpp", ".cs", ".go", ".hh", ".hpp", ".java", ".m", ".mm", ".proto", ".rs", ".scala", ".swift", ".dart", ".groovy", ".kt", ".kts":
|
|
lic, err = prefix(tmpl, data, "", "// ", "")
|
|
case ".py", ".sh", ".yaml", ".yml", ".dockerfile", "dockerfile", ".rb", "gemfile":
|
|
lic, err = prefix(tmpl, data, "", "# ", "")
|
|
case ".el", ".lisp":
|
|
lic, err = prefix(tmpl, data, "", ";; ", "")
|
|
case ".erl":
|
|
lic, err = prefix(tmpl, data, "", "% ", "")
|
|
case ".hs", ".sql":
|
|
lic, err = prefix(tmpl, data, "", "-- ", "")
|
|
case ".html", ".xml", ".vue":
|
|
lic, err = prefix(tmpl, data, "<!--", " ", "-->")
|
|
case ".php":
|
|
lic, err = prefix(tmpl, data, "", "// ", "")
|
|
case ".ml", ".mli", ".mll", ".mly":
|
|
lic, err = prefix(tmpl, data, "(**", " ", "*)")
|
|
}
|
|
return lic, err
|
|
}
|
|
|
|
func fileExtension(name string) string {
|
|
if v := filepath.Ext(name); v != "" {
|
|
return strings.ToLower(v)
|
|
}
|
|
return strings.ToLower(filepath.Base(name))
|
|
}
|
|
|
|
var head = []string{
|
|
"#!", // shell script
|
|
"<?xml", // XML declaratioon
|
|
"<!doctype", // HTML doctype
|
|
"# encoding:", // Ruby encoding
|
|
"# frozen_string_literal:", // Ruby interpreter instruction
|
|
"<?php", // PHP opening tag
|
|
}
|
|
|
|
func hashBang(b []byte) []byte {
|
|
var line []byte
|
|
for _, c := range b {
|
|
line = append(line, c)
|
|
if c == '\n' {
|
|
break
|
|
}
|
|
}
|
|
first := strings.ToLower(string(line))
|
|
for _, h := range head {
|
|
if strings.HasPrefix(first, h) {
|
|
return line
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func hasLicense(b []byte) bool {
|
|
n := 1000
|
|
if len(b) < 1000 {
|
|
n = len(b)
|
|
}
|
|
return bytes.Contains(bytes.ToLower(b[:n]), []byte("copyright")) ||
|
|
bytes.Contains(bytes.ToLower(b[:n]), []byte("mozilla public"))
|
|
}
|