update-license.go 5.9 KB

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