|
|
@@ -0,0 +1,346 @@
|
|
|
+// +build none
|
|
|
+
|
|
|
+/*
|
|
|
+This command generates GPL license headers on top of all source files.
|
|
|
+You can run it once per month, before cutting a release or just
|
|
|
+whenever you feel like it.
|
|
|
+
|
|
|
+ go run update-license.go
|
|
|
+
|
|
|
+All authors (people who have contributed code) are listed in the
|
|
|
+AUTHORS file. The author names are mapped and deduplicated using the
|
|
|
+.mailmap file. You can use .mailmap to set the canonical name and
|
|
|
+address for each author. See git-shortlog(1) for an explanation of the
|
|
|
+.mailmap format.
|
|
|
+
|
|
|
+Please review the resulting diff to check whether the correct
|
|
|
+copyright assignments are performed.
|
|
|
+*/
|
|
|
+package main
|
|
|
+
|
|
|
+import (
|
|
|
+ "bufio"
|
|
|
+ "bytes"
|
|
|
+ "fmt"
|
|
|
+ "io/ioutil"
|
|
|
+ "log"
|
|
|
+ "os"
|
|
|
+ "os/exec"
|
|
|
+ "path/filepath"
|
|
|
+ "regexp"
|
|
|
+ "runtime"
|
|
|
+ "sort"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+ "sync"
|
|
|
+ "text/template"
|
|
|
+ "time"
|
|
|
+)
|
|
|
+
|
|
|
+var (
|
|
|
+ // only files with these extensions will be considered
|
|
|
+ extensions = []string{".go", ".js", ".qml"}
|
|
|
+
|
|
|
+ // paths with any of these prefixes will be skipped
|
|
|
+ skipPrefixes = []string{
|
|
|
+ // boring stuff
|
|
|
+ "Godeps/", "tests/files/", "build/",
|
|
|
+ // don't relicense vendored packages
|
|
|
+ "crypto/sha3/", "crypto/ecies/", "logger/glog/",
|
|
|
+ }
|
|
|
+
|
|
|
+ // paths with this prefix are licensed as GPL. all other files are LGPL.
|
|
|
+ gplPrefixes = []string{"cmd/"}
|
|
|
+
|
|
|
+ // this regexp must match the entire license comment at the
|
|
|
+ // beginning of each file.
|
|
|
+ licenseCommentRE = regexp.MustCompile(`(?s)^/\*\s*(Copyright|This file is part of) .*?\*/\n*`)
|
|
|
+
|
|
|
+ // this text appears at the start of AUTHORS
|
|
|
+ authorsFileHeader = "# This is the official list of go-ethereum authors for copyright purposes.\n\n"
|
|
|
+)
|
|
|
+
|
|
|
+// this template generates the license comment.
|
|
|
+// its input is an info structure.
|
|
|
+var licenseT = template.Must(template.New("").Parse(`
|
|
|
+// Copyright {{.Year}} The go-ethereum Authors
|
|
|
+// This file is part of go-ethereum.
|
|
|
+//
|
|
|
+// go-ethereum is free software: you can redistribute it and/or modify
|
|
|
+// it under the terms of the GNU {{.License}} as published by
|
|
|
+// the Free Software Foundation, either version 3 of the License, or
|
|
|
+// (at your option) any later version.
|
|
|
+//
|
|
|
+// go-ethereum is distributed in the hope that it will be useful,
|
|
|
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
+// GNU {{.License}} for more details.
|
|
|
+//
|
|
|
+// You should have received a copy of the GNU {{.License}}
|
|
|
+// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
|
|
|
+
|
|
|
+`[1:]))
|
|
|
+
|
|
|
+type info struct {
|
|
|
+ file string
|
|
|
+ Year int64
|
|
|
+}
|
|
|
+
|
|
|
+func (i info) License() string {
|
|
|
+ if i.gpl() {
|
|
|
+ return "General Public License"
|
|
|
+ } else {
|
|
|
+ return "Lesser General Public License"
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (i info) ShortLicense() string {
|
|
|
+ if i.gpl() {
|
|
|
+ return "GPL"
|
|
|
+ } else {
|
|
|
+ return "LGPL"
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (i info) gpl() bool {
|
|
|
+ for _, p := range gplPrefixes {
|
|
|
+ if strings.HasPrefix(i.file, p) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false
|
|
|
+}
|
|
|
+
|
|
|
+func main() {
|
|
|
+ var (
|
|
|
+ files = getFiles()
|
|
|
+ filec = make(chan string)
|
|
|
+ infoc = make(chan *info, 20)
|
|
|
+ wg sync.WaitGroup
|
|
|
+ )
|
|
|
+
|
|
|
+ writeAuthors(files)
|
|
|
+
|
|
|
+ go func() {
|
|
|
+ for _, f := range files {
|
|
|
+ filec <- f
|
|
|
+ }
|
|
|
+ close(filec)
|
|
|
+ }()
|
|
|
+ for i := runtime.NumCPU(); i >= 0; i-- {
|
|
|
+ // getting file info is slow and needs to be parallel.
|
|
|
+ // it traverses git history for each file.
|
|
|
+ wg.Add(1)
|
|
|
+ go getInfo(filec, infoc, &wg)
|
|
|
+ }
|
|
|
+ go func() {
|
|
|
+ wg.Wait()
|
|
|
+ close(infoc)
|
|
|
+ }()
|
|
|
+ writeLicenses(infoc)
|
|
|
+}
|
|
|
+
|
|
|
+func getFiles() []string {
|
|
|
+ cmd := exec.Command("git", "ls-tree", "-r", "--name-only", "HEAD")
|
|
|
+ var files []string
|
|
|
+ err := doLines(cmd, func(line string) {
|
|
|
+ for _, p := range skipPrefixes {
|
|
|
+ if strings.HasPrefix(line, p) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ext := filepath.Ext(line)
|
|
|
+ for _, wantExt := range extensions {
|
|
|
+ if ext == wantExt {
|
|
|
+ goto keep
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return
|
|
|
+ keep:
|
|
|
+ files = append(files, line)
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ log.Fatalf("error getting files:", err)
|
|
|
+ }
|
|
|
+ return files
|
|
|
+}
|
|
|
+
|
|
|
+var authorRegexp = regexp.MustCompile(`\s*[0-9]+\s*(.*)`)
|
|
|
+
|
|
|
+func gitAuthors(files []string) []string {
|
|
|
+ cmds := []string{"shortlog", "-s", "-n", "-e", "HEAD", "--"}
|
|
|
+ cmds = append(cmds, files...)
|
|
|
+ cmd := exec.Command("git", cmds...)
|
|
|
+ var authors []string
|
|
|
+ err := doLines(cmd, func(line string) {
|
|
|
+ m := authorRegexp.FindStringSubmatch(line)
|
|
|
+ if len(m) > 1 {
|
|
|
+ authors = append(authors, m[1])
|
|
|
+ }
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ log.Fatalln("error getting authors:", err)
|
|
|
+ }
|
|
|
+ return authors
|
|
|
+}
|
|
|
+
|
|
|
+func readAuthors() []string {
|
|
|
+ content, err := ioutil.ReadFile("AUTHORS")
|
|
|
+ if err != nil && !os.IsNotExist(err) {
|
|
|
+ log.Fatalln("error reading AUTHORS:", err)
|
|
|
+ }
|
|
|
+ var authors []string
|
|
|
+ for _, a := range bytes.Split(content, []byte("\n")) {
|
|
|
+ if len(a) > 0 && a[0] != '#' {
|
|
|
+ authors = append(authors, string(a))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Retranslate existing authors through .mailmap.
|
|
|
+ // This should catch email address changes.
|
|
|
+ authors = mailmapLookup(authors)
|
|
|
+ return authors
|
|
|
+}
|
|
|
+
|
|
|
+func mailmapLookup(authors []string) []string {
|
|
|
+ if len(authors) == 0 {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+ cmds := []string{"check-mailmap", "--"}
|
|
|
+ cmds = append(cmds, authors...)
|
|
|
+ cmd := exec.Command("git", cmds...)
|
|
|
+ var translated []string
|
|
|
+ err := doLines(cmd, func(line string) {
|
|
|
+ translated = append(translated, line)
|
|
|
+ })
|
|
|
+ if err != nil {
|
|
|
+ log.Fatalln("error translating authors:", err)
|
|
|
+ }
|
|
|
+ return translated
|
|
|
+}
|
|
|
+
|
|
|
+func writeAuthors(files []string) {
|
|
|
+ merge := make(map[string]bool)
|
|
|
+ // Add authors that Git reports as contributorxs.
|
|
|
+ // This is the primary source of author information.
|
|
|
+ for _, a := range gitAuthors(files) {
|
|
|
+ merge[a] = true
|
|
|
+ }
|
|
|
+ // Add existing authors from the file. This should ensure that we
|
|
|
+ // never lose authors, even if Git stops listing them. We can also
|
|
|
+ // add authors manually this way.
|
|
|
+ for _, a := range readAuthors() {
|
|
|
+ merge[a] = true
|
|
|
+ }
|
|
|
+ // Write sorted list of authors back to the file.
|
|
|
+ var result []string
|
|
|
+ for a := range merge {
|
|
|
+ result = append(result, a)
|
|
|
+ }
|
|
|
+ sort.Strings(result)
|
|
|
+ content := new(bytes.Buffer)
|
|
|
+ content.WriteString(authorsFileHeader)
|
|
|
+ for _, a := range result {
|
|
|
+ content.WriteString(a)
|
|
|
+ content.WriteString("\n")
|
|
|
+ }
|
|
|
+ fmt.Println("writing AUTHORS")
|
|
|
+ if err := ioutil.WriteFile("AUTHORS", content.Bytes(), 0644); err != nil {
|
|
|
+ log.Fatalln(err)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func getInfo(files <-chan string, out chan<- *info, wg *sync.WaitGroup) {
|
|
|
+ for file := range files {
|
|
|
+ stat, err := os.Lstat(file)
|
|
|
+ if err != nil {
|
|
|
+ fmt.Printf("ERROR %s: %v\n", file, err)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ if !stat.Mode().IsRegular() {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ info, err := fileInfo(file)
|
|
|
+ if err != nil {
|
|
|
+ fmt.Printf("ERROR %s: %v\n", file, err)
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ out <- info
|
|
|
+ }
|
|
|
+ wg.Done()
|
|
|
+}
|
|
|
+
|
|
|
+// fileInfo finds the lowest year in which the given file was commited.
|
|
|
+func fileInfo(file string) (*info, error) {
|
|
|
+ info := &info{file: file, Year: int64(time.Now().Year())}
|
|
|
+ cmd := exec.Command("git", "log", "--follow", "--find-copies", "--pretty=format:%ai", "--", file)
|
|
|
+ err := doLines(cmd, func(line string) {
|
|
|
+ y, err := strconv.ParseInt(line[:4], 10, 64)
|
|
|
+ if err != nil {
|
|
|
+ fmt.Printf("cannot parse year: %q", line[:4])
|
|
|
+ }
|
|
|
+ if y < info.Year {
|
|
|
+ info.Year = y
|
|
|
+ }
|
|
|
+ })
|
|
|
+ return info, err
|
|
|
+}
|
|
|
+
|
|
|
+func writeLicenses(infos <-chan *info) {
|
|
|
+ for i := range infos {
|
|
|
+ writeLicense(i)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func writeLicense(info *info) {
|
|
|
+ fi, err := os.Stat(info.file)
|
|
|
+ if os.IsNotExist(err) {
|
|
|
+ fmt.Println("skipping (does not exist)", info.file)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ if err != nil {
|
|
|
+ log.Fatalf("error stat'ing %s: %v\n", info.file, err)
|
|
|
+ }
|
|
|
+ content, err := ioutil.ReadFile(info.file)
|
|
|
+ if err != nil {
|
|
|
+ log.Fatalf("error reading %s: %v\n", info.file, err)
|
|
|
+ }
|
|
|
+ // Construct new file content.
|
|
|
+ buf := new(bytes.Buffer)
|
|
|
+ licenseT.Execute(buf, info)
|
|
|
+ if m := licenseCommentRE.FindIndex(content); m != nil && m[0] == 0 {
|
|
|
+ buf.Write(content[:m[0]])
|
|
|
+ buf.Write(content[m[1]:])
|
|
|
+ } else {
|
|
|
+ buf.Write(content)
|
|
|
+ }
|
|
|
+ // Write it to the file.
|
|
|
+ if bytes.Equal(content, buf.Bytes()) {
|
|
|
+ fmt.Println("skipping (no changes)", info.file)
|
|
|
+ return
|
|
|
+ }
|
|
|
+ fmt.Println("writing", info.ShortLicense(), info.file)
|
|
|
+ if err := ioutil.WriteFile(info.file, buf.Bytes(), fi.Mode()); err != nil {
|
|
|
+ log.Fatalf("error writing %s: %v", info.file, err)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func doLines(cmd *exec.Cmd, f func(string)) error {
|
|
|
+ stdout, err := cmd.StdoutPipe()
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ if err := cmd.Start(); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ s := bufio.NewScanner(stdout)
|
|
|
+ for s.Scan() {
|
|
|
+ f(s.Text())
|
|
|
+ }
|
|
|
+ if s.Err() != nil {
|
|
|
+ return s.Err()
|
|
|
+ }
|
|
|
+ if err := cmd.Wait(); err != nil {
|
|
|
+ return fmt.Errorf("%v (for %s)", err, strings.Join(cmd.Args, " "))
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|