github.com/jonasnick/go-ethereum@v0.7.12-0.20150216215225-22176f05d387/update-license.go (about)

     1  // +build none
     2  /*
     3  This command generates GPL license headers on top of all source files.
     4  You can run it once per month, before cutting a release or just
     5  whenever you feel like it.
     6  
     7  	go run update-license.go
     8  
     9  The copyright in each file is assigned to any authors for which git
    10  can find commits in the file's history. It will try to follow renames
    11  throughout history. The author names are mapped and deduplicated using
    12  the .mailmap file. You can use .mailmap to set the canonical name and
    13  address for each author. See git-shortlog(1) for an explanation
    14  of the .mailmap format.
    15  
    16  Please review the resulting diff to check whether the correct
    17  copyright assignments are performed.
    18  */
    19  package main
    20  
    21  import (
    22  	"bufio"
    23  	"bytes"
    24  	"fmt"
    25  	"io/ioutil"
    26  	"os"
    27  	"os/exec"
    28  	"path"
    29  	"regexp"
    30  	"runtime"
    31  	"sort"
    32  	"strings"
    33  	"sync"
    34  	"text/template"
    35  )
    36  
    37  var (
    38  	// only files with these extensions will be considered
    39  	extensions = []string{".go", ".js", ".qml"}
    40  
    41  	// paths with any of these prefixes will be skipped
    42  	skipPrefixes = []string{"tests/files/", "cmd/mist/assets/ext/", "cmd/mist/assets/muted/"}
    43  
    44  	// paths with this prefix are licensed as GPL. all other files are LGPL.
    45  	gplPrefixes = []string{"cmd/"}
    46  
    47  	// this regexp must match the entire license comment at the
    48  	// beginning of each file.
    49  	licenseCommentRE = regexp.MustCompile(`(?s)^/\*\s*(Copyright|This file is part of) .*?\*/\n*`)
    50  
    51  	// this line is used when git doesn't find any authors for a file
    52  	defaultCopyright = "Copyright (C) 2014 Jeffrey Wilcke <jeffrey@ethereum.org>"
    53  )
    54  
    55  // this template generates the license comment.
    56  // its input is an info structure.
    57  var licenseT = template.Must(template.New("").Parse(`/*
    58  	{{.Copyrights}}
    59  
    60  	This file is part of go-ethereum
    61  
    62  	go-ethereum is free software: you can redistribute it and/or modify
    63  	it under the terms of the GNU {{.License}} as published by
    64  	the Free Software Foundation, either version 3 of the License, or
    65  	(at your option) any later version.
    66  
    67  	go-ethereum is distributed in the hope that it will be useful,
    68  	but WITHOUT ANY WARRANTY; without even the implied warranty of
    69  	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    70  	GNU {{.License}} for more details.
    71  
    72  	You should have received a copy of the GNU {{.License}}
    73  	along with go-ethereum.  If not, see <http://www.gnu.org/licenses/>.
    74  */
    75  
    76  `))
    77  
    78  type info struct {
    79  	file    string
    80  	mode    os.FileMode
    81  	authors map[string][]string // map keys are authors, values are years
    82  	gpl     bool
    83  }
    84  
    85  func (i info) Copyrights() string {
    86  	var lines []string
    87  	for name, years := range i.authors {
    88  		lines = append(lines, "Copyright (C) "+strings.Join(years, ", ")+" "+name)
    89  	}
    90  	if len(lines) == 0 {
    91  		lines = []string{defaultCopyright}
    92  	}
    93  	sort.Strings(lines)
    94  	return strings.Join(lines, "\n\t")
    95  }
    96  
    97  func (i info) License() string {
    98  	if i.gpl {
    99  		return "General Public License"
   100  	} else {
   101  		return "Lesser General Public License"
   102  	}
   103  }
   104  
   105  func (i info) ShortLicense() string {
   106  	if i.gpl {
   107  		return "GPL"
   108  	} else {
   109  		return "LGPL"
   110  	}
   111  }
   112  
   113  func (i *info) addAuthorYear(name, year string) {
   114  	for _, y := range i.authors[name] {
   115  		if y == year {
   116  			return
   117  		}
   118  	}
   119  	i.authors[name] = append(i.authors[name], year)
   120  	sort.Strings(i.authors[name])
   121  }
   122  
   123  func main() {
   124  	files := make(chan string)
   125  	infos := make(chan *info)
   126  	wg := new(sync.WaitGroup)
   127  
   128  	go getFiles(files)
   129  	for i := runtime.NumCPU(); i >= 0; i-- {
   130  		// getting file info is slow and needs to be parallel
   131  		wg.Add(1)
   132  		go getInfo(files, infos, wg)
   133  	}
   134  	go func() { wg.Wait(); close(infos) }()
   135  	writeLicenses(infos)
   136  }
   137  
   138  func getFiles(out chan<- string) {
   139  	cmd := exec.Command("git", "ls-tree", "-r", "--name-only", "HEAD")
   140  	err := doLines(cmd, func(line string) {
   141  		for _, p := range skipPrefixes {
   142  			if strings.HasPrefix(line, p) {
   143  				return
   144  			}
   145  		}
   146  		ext := path.Ext(line)
   147  		for _, wantExt := range extensions {
   148  			if ext == wantExt {
   149  				goto send
   150  			}
   151  		}
   152  		return
   153  
   154  	send:
   155  		out <- line
   156  	})
   157  	if err != nil {
   158  		fmt.Println("error getting files:", err)
   159  	}
   160  	close(out)
   161  }
   162  
   163  func getInfo(files <-chan string, out chan<- *info, wg *sync.WaitGroup) {
   164  	for file := range files {
   165  		stat, err := os.Lstat(file)
   166  		if err != nil {
   167  			fmt.Printf("ERROR %s: %v\n", file, err)
   168  			continue
   169  		}
   170  		if !stat.Mode().IsRegular() {
   171  			continue
   172  		}
   173  		info, err := fileInfo(file)
   174  		if err != nil {
   175  			fmt.Printf("ERROR %s: %v\n", file, err)
   176  			continue
   177  		}
   178  		info.mode = stat.Mode()
   179  		out <- info
   180  	}
   181  	wg.Done()
   182  }
   183  
   184  func fileInfo(file string) (*info, error) {
   185  	info := &info{file: file, authors: make(map[string][]string)}
   186  	for _, p := range gplPrefixes {
   187  		if strings.HasPrefix(file, p) {
   188  			info.gpl = true
   189  			break
   190  		}
   191  	}
   192  	cmd := exec.Command("git", "log", "--follow", "--find-copies", "--pretty=format:%aI | %aN <%aE>", "--", file)
   193  	err := doLines(cmd, func(line string) {
   194  		sep := strings.IndexByte(line, '|')
   195  		year, name := line[:4], line[sep+2:]
   196  		info.addAuthorYear(name, year)
   197  	})
   198  	return info, err
   199  }
   200  
   201  func writeLicenses(infos <-chan *info) {
   202  	buf := new(bytes.Buffer)
   203  	for info := range infos {
   204  		content, err := ioutil.ReadFile(info.file)
   205  		if err != nil {
   206  			fmt.Printf("ERROR: couldn't read %s: %v\n", info.file, err)
   207  			continue
   208  		}
   209  
   210  		// construct new file content
   211  		buf.Reset()
   212  		licenseT.Execute(buf, info)
   213  		if m := licenseCommentRE.FindIndex(content); m != nil && m[0] == 0 {
   214  			buf.Write(content[m[1]:])
   215  		} else {
   216  			buf.Write(content)
   217  		}
   218  
   219  		if !bytes.Equal(content, buf.Bytes()) {
   220  			fmt.Println("writing", info.ShortLicense(), info.file)
   221  			if err := ioutil.WriteFile(info.file, buf.Bytes(), info.mode); err != nil {
   222  				fmt.Printf("ERROR: couldn't write %s: %v", info.file, err)
   223  			}
   224  		}
   225  	}
   226  }
   227  
   228  func doLines(cmd *exec.Cmd, f func(string)) error {
   229  	stdout, err := cmd.StdoutPipe()
   230  	if err != nil {
   231  		return err
   232  	}
   233  	if err := cmd.Start(); err != nil {
   234  		return err
   235  	}
   236  	s := bufio.NewScanner(stdout)
   237  	for s.Scan() {
   238  		f(s.Text())
   239  	}
   240  	if s.Err() != nil {
   241  		return s.Err()
   242  	}
   243  	if err := cmd.Wait(); err != nil {
   244  		return fmt.Errorf("%v (for %s)", err, strings.Join(cmd.Args, " "))
   245  	}
   246  	return nil
   247  }