Skip to content

Commit 59c5a2f

Browse files
committed
Merge pull request #244 from fjl/license-tool
Generated License Headers
2 parents 032ab66 + 1d05027 commit 59c5a2f

File tree

2 files changed

+259
-0
lines changed

2 files changed

+259
-0
lines changed

.mailmap

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Jeffrey Wilcke <[email protected]>
2+
3+
4+
5+
6+
Viktor Trón <[email protected]>
7+
8+
Joseph Goulden <[email protected]>
9+
10+
Nick Savers <[email protected]>
11+
12+
Maran Hidskes <[email protected]>

update-license.go

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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 <[email protected]>"
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+
}

0 commit comments

Comments
 (0)