123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- // Copyright (C) 2015 The Syncthing Authors.
- //
- // This Source Code Form is subject to the terms of the Mozilla Public
- // License, v. 2.0. If a copy of the MPL was not distributed with this file,
- // You can obtain one at https://mozilla.org/MPL/2.0/.
- //go:build ignore
- // +build ignore
- // Generates the list of contributors in gui/index.html based on contents of
- // AUTHORS.
- package main
- import (
- "bytes"
- "cmp"
- "fmt"
- "log"
- "math"
- "os"
- "os/exec"
- "regexp"
- "slices"
- "strings"
- )
- const htmlFile = "gui/default/syncthing/core/aboutModalView.html"
- var (
- nicknameRe = regexp.MustCompile(`\(([^\s]*)\)`)
- emailRe = regexp.MustCompile(`<([^\s]*)>`)
- authorBotsRegexps = []string{
- `\[bot\]`,
- `Syncthing.*Automation`,
- }
- )
- var authorBotsRe = regexp.MustCompile(strings.Join(authorBotsRegexps, "|"))
- const authorsHeader = `# This is the official list of Syncthing authors for copyright purposes.
- #
- # THIS FILE IS MOSTLY AUTO GENERATED. IF YOU'VE MADE A COMMIT TO THE
- # REPOSITORY YOU WILL BE ADDED HERE AUTOMATICALLY WITHOUT THE NEED FOR
- # ANY MANUAL ACTION.
- #
- # That said, you are welcome to correct your name or add a nickname / GitHub
- # user name as appropriate. The format is:
- #
- # Name Name Name (nickname) <[email protected]> <[email protected]>
- #
- # The in-GUI authors list is periodically automatically updated from the
- # contents of this file.
- #
- `
- type author struct {
- name string
- nickname string
- emails []string
- commits int
- log10commits int
- }
- func main() {
- // Read authors from the AUTHORS file
- authorSet := getAuthors()
- // Grab the set of all known authors based on the git log, and add any
- // missing ones to the authors list.
- addAuthors(authorSet)
- authors := authorSet.filteredAuthors()
- // Write authors to the about dialog
- slices.SortFunc(authors, func(a, b author) int {
- return cmp.Or(
- -cmp.Compare(a.log10commits, b.log10commits),
- cmp.Compare(strings.ToLower(a.name), strings.ToLower(b.name)))
- })
- var lines []string
- for _, author := range authors {
- lines = append(lines, author.name)
- }
- replacement := strings.Join(lines, ", ")
- authorsRe := regexp.MustCompile(`(?s)id="contributor-list">.*?</div>`)
- bs, err := os.ReadFile(htmlFile)
- if err != nil {
- log.Fatal(err)
- }
- bs = authorsRe.ReplaceAll(bs, []byte("id=\"contributor-list\">\n"+replacement+"\n </div>"))
- if err := os.WriteFile(htmlFile, bs, 0o644); err != nil {
- log.Fatal(err)
- }
- // Write AUTHORS file
- out, err := os.Create("AUTHORS")
- if err != nil {
- log.Fatal(err)
- }
- fmt.Fprintf(out, "%s\n", authorsHeader)
- for _, author := range authors {
- fmt.Fprintf(out, "%s", author.name)
- if author.nickname != "" {
- fmt.Fprintf(out, " (%s)", author.nickname)
- }
- for _, email := range author.emails {
- fmt.Fprintf(out, " <%s>", email)
- }
- fmt.Fprint(out, "\n")
- }
- out.Close()
- }
- func getAuthors() *authorSet {
- bs, err := os.ReadFile("AUTHORS")
- if err != nil {
- log.Fatal(err)
- }
- lines := strings.Split(string(bs), "\n")
- authors := &authorSet{
- emails: make(map[string]int),
- commits: make(map[string]stringSet),
- }
- for _, line := range lines {
- if len(line) == 0 || line[0] == '#' {
- continue
- }
- fields := strings.Fields(line)
- var author author
- for _, field := range fields {
- if field == "#" {
- break
- } else if m := nicknameRe.FindStringSubmatch(field); len(m) > 1 {
- author.nickname = m[1]
- } else if m := emailRe.FindStringSubmatch(field); len(m) > 1 {
- author.emails = append(author.emails, m[1])
- } else {
- if author.name == "" {
- author.name = field
- } else {
- author.name = author.name + " " + field
- }
- }
- }
- authors.add(author)
- }
- return authors
- }
- // list of commits that we don't include in our author file; because they
- // are legacy things that don't affect code, are committed with incorrect
- // address, or for other reasons.
- var excludeCommits = stringSetFromStrings([]string{
- "a9339d0627fff439879d157c75077f02c9fac61b",
- "254c63763a3ad42fd82259f1767db526cff94a14",
- "32a76901a91ff0f663db6f0830e0aedec946e4d0",
- "bc7639b0ffcea52b2197efb1c0bb68b338d1c915",
- "9bdcadf6345aba3a939e9e58d85b89dbe9d44bc9",
- "b933e9666abdfcd22919dd458c930d944e1e1b7f",
- "b84d960a81c1282a79e2b9477558de4f1af6faae",
- "4dfb9d7c83ed172f12ae19408517961f4a49beeb",
- })
- func addAuthors(authors *authorSet) {
- // All existing source-tracked files
- bs, err := exec.Command("git", "ls-tree", "-r", "HEAD", "--name-only").CombinedOutput()
- if err != nil {
- fmt.Println(string(bs))
- log.Fatal("git ls-tree:", err)
- }
- files := strings.Split(string(bs), "\n")
- files = slices.DeleteFunc(files, func(s string) bool {
- return !(strings.HasPrefix(s, "assets/") ||
- strings.HasPrefix(s, "cmd/") ||
- strings.HasPrefix(s, "etc/") ||
- strings.HasPrefix(s, "gui/") ||
- strings.HasPrefix(s, "internal/") ||
- strings.HasPrefix(s, "lib/") ||
- strings.HasPrefix(s, "proto/") ||
- strings.HasPrefix(s, "script/") ||
- strings.HasPrefix(s, "test/") ||
- strings.HasPrefix(s, "Dockerfile") ||
- s == "build.go")
- })
- coAuthoredPrefix := "Co-authored-by: "
- for _, file := range files {
- // All commits affecting those files, following any renames to their
- // origin. Format is hash, email, name, newline, body. The body is
- // indented with one space, to differentiate from the hash lines.
- args := []string{"log", "--format=%H %ae %an%n%w(,1,1)%b", "--follow", "--", file}
- bs, err = exec.Command("git", args...).CombinedOutput()
- if err != nil {
- fmt.Println(string(bs))
- log.Fatal("git log:", err)
- }
- skipCommit := false
- var hash, email, name string
- for _, line := range bytes.Split(bs, []byte{'\n'}) {
- if len(line) == 0 {
- continue
- }
- switch line[0] {
- case ' ':
- // Look for Co-authored-by: lines in the commit body.
- if skipCommit {
- continue
- }
- line = line[1:]
- if bytes.HasPrefix(line, []byte(coAuthoredPrefix)) {
- // Co-authored-by: Name Name <[email protected]>
- line = line[len(coAuthoredPrefix):]
- if name, email, ok := strings.Cut(string(line), "<"); ok {
- name = strings.TrimSpace(name)
- email = strings.Trim(strings.TrimSpace(email), "<>")
- if email == "@" {
- // GitHub special for users who hide their email.
- continue
- }
- authors.setName(email, name)
- authors.addCommit(email, hash)
- }
- }
- default: // hash email name
- fields := strings.SplitN(string(line), " ", 3)
- if len(fields) != 3 {
- continue
- }
- hash, email, name = fields[0], fields[1], fields[2]
- if excludeCommits.has(hash) {
- skipCommit = true
- continue
- }
- skipCommit = false
- authors.setName(email, name)
- authors.addCommit(email, hash)
- }
- }
- }
- }
- // A simple string set type
- type stringSet map[string]struct{}
- func stringSetFromStrings(ss []string) stringSet {
- s := make(stringSet)
- for _, e := range ss {
- s.add(e)
- }
- return s
- }
- func (s stringSet) add(e string) {
- s[e] = struct{}{}
- }
- func (s stringSet) has(e string) bool {
- _, ok := s[e]
- return ok
- }
- // A set of authors
- type authorSet struct {
- authors []author
- emails map[string]int // email to author index
- commits map[string]stringSet // email to commit hashes
- }
- func (a *authorSet) add(author author) {
- for _, e := range author.emails {
- if idx, ok := a.emails[e]; ok {
- emails := append(author.emails, a.authors[idx].emails...)
- slices.Sort(emails)
- emails = slices.Compact(emails)
- a.authors[idx].name = author.name
- a.authors[idx].emails = emails
- for _, e := range emails {
- a.emails[e] = idx
- }
- return
- }
- }
- for _, e := range author.emails {
- a.emails[e] = len(a.authors)
- }
- a.authors = append(a.authors, author)
- }
- func (a *authorSet) setName(email, name string) {
- idx, ok := a.emails[email]
- if !ok {
- a.emails[email] = len(a.authors)
- a.authors = append(a.authors, author{name: name, emails: []string{email}})
- } else if a.authors[idx].name == "" {
- a.authors[idx].name = name
- }
- }
- func (a *authorSet) addCommit(email, hash string) {
- ss, ok := a.commits[email]
- if !ok {
- ss = make(stringSet)
- a.commits[email] = ss
- }
- ss.add(hash)
- }
- func (a *authorSet) filteredAuthors() []author {
- authors := make([]author, len(a.authors))
- copy(authors, a.authors)
- for i, author := range authors {
- for _, e := range author.emails {
- authors[i].commits += len(a.commits[e])
- }
- }
- authors = slices.DeleteFunc(authors, func(a author) bool {
- return a.commits == 0 || authorBotsRe.MatchString(a.name)
- })
- for i := range authors {
- authors[i].log10commits = int(math.Log10(float64(authors[i].commits)))
- }
- return authors
- }
|