authors.go 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. // Copyright (C) 2015 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. //go:build ignore
  7. // +build ignore
  8. // Generates the list of contributors in gui/index.html based on contents of
  9. // AUTHORS.
  10. package main
  11. import (
  12. "bytes"
  13. "cmp"
  14. "fmt"
  15. "io"
  16. "log"
  17. "math"
  18. "os"
  19. "os/exec"
  20. "regexp"
  21. "slices"
  22. "strings"
  23. )
  24. const htmlFile = "gui/default/syncthing/core/aboutModalView.html"
  25. var (
  26. nicknameRe = regexp.MustCompile(`\(([^\s]*)\)`)
  27. emailRe = regexp.MustCompile(`<([^\s]*)>`)
  28. authorBotsRegexps = []string{
  29. `\[bot\]`,
  30. `Syncthing.*Automation`,
  31. }
  32. )
  33. var authorBotsRe = regexp.MustCompile(strings.Join(authorBotsRegexps, "|"))
  34. const authorsHeader = `# This is the official list of Syncthing authors for copyright purposes.
  35. #
  36. # THIS FILE IS MOSTLY AUTO GENERATED. IF YOU'VE MADE A COMMIT TO THE
  37. # REPOSITORY YOU WILL BE ADDED HERE AUTOMATICALLY WITHOUT THE NEED FOR
  38. # ANY MANUAL ACTION.
  39. #
  40. # That said, you are welcome to correct your name or add a nickname / GitHub
  41. # user name as appropriate. The format is:
  42. #
  43. # Name Name Name (nickname) <[email protected]> <[email protected]>
  44. #
  45. # The in-GUI authors list is periodically automatically updated from the
  46. # contents of this file.
  47. #
  48. `
  49. type author struct {
  50. name string
  51. nickname string
  52. emails []string
  53. commits int
  54. log10commits int
  55. }
  56. func main() {
  57. // Read authors from the AUTHORS file
  58. authors := getAuthors()
  59. // Grab the set of thus known email addresses
  60. listed := make(stringSet)
  61. names := make(map[string]int)
  62. for i, a := range authors {
  63. names[a.name] = i
  64. for _, e := range a.emails {
  65. listed.add(e)
  66. }
  67. }
  68. // Grab the set of all known authors based on the git log, and add any
  69. // missing ones to the authors list.
  70. all := allAuthors()
  71. for email, name := range all {
  72. if listed.has(email) {
  73. continue
  74. }
  75. if _, ok := names[name]; ok && name != "" {
  76. // We found a match on name
  77. authors[names[name]].emails = append(authors[names[name]].emails, email)
  78. listed.add(email)
  79. continue
  80. }
  81. authors = append(authors, author{
  82. name: name,
  83. emails: []string{email},
  84. })
  85. names[name] = len(authors) - 1
  86. listed.add(email)
  87. }
  88. // Write author names in GUI about modal
  89. getContributions(authors)
  90. // Sort by contributions
  91. slices.SortFunc(authors, func(a, b author) int {
  92. // Sort first by log10(commits), then by name. This means that we first get
  93. // an alphabetic list of people with >= 1000 commits, then a list of people
  94. // with >= 100 commits, and so on.
  95. if a.log10commits != b.log10commits {
  96. return cmp.Compare(b.log10commits, a.log10commits)
  97. }
  98. return strings.Compare(a.name, b.name)
  99. })
  100. var lines []string
  101. for _, author := range authors {
  102. if authorBotsRe.MatchString(author.name) {
  103. // Only humans are eligible, pending future legislation to the
  104. // contrary.
  105. continue
  106. }
  107. lines = append(lines, author.name)
  108. }
  109. replacement := strings.Join(lines, ", ")
  110. authorsRe := regexp.MustCompile(`(?s)id="contributor-list">.*?</div>`)
  111. bs := readAll(htmlFile)
  112. bs = authorsRe.ReplaceAll(bs, []byte("id=\"contributor-list\">\n"+replacement+"\n </div>"))
  113. if err := os.WriteFile(htmlFile, bs, 0o644); err != nil {
  114. log.Fatal(err)
  115. }
  116. // Write AUTHORS file
  117. // Sort by author name
  118. slices.SortFunc(authors, func(a, b author) int {
  119. return strings.Compare(strings.ToLower(a.name), strings.ToLower(b.name))
  120. })
  121. out, err := os.Create("AUTHORS")
  122. if err != nil {
  123. log.Fatal(err)
  124. }
  125. fmt.Fprintf(out, "%s\n", authorsHeader)
  126. for _, author := range authors {
  127. fmt.Fprintf(out, "%s", author.name)
  128. if author.nickname != "" {
  129. fmt.Fprintf(out, " (%s)", author.nickname)
  130. }
  131. for _, email := range author.emails {
  132. fmt.Fprintf(out, " <%s>", email)
  133. }
  134. fmt.Fprintf(out, "\n")
  135. }
  136. out.Close()
  137. }
  138. func getAuthors() []author {
  139. bs := readAll("AUTHORS")
  140. lines := strings.Split(string(bs), "\n")
  141. var authors []author
  142. for _, line := range lines {
  143. if len(line) == 0 || line[0] == '#' {
  144. continue
  145. }
  146. fields := strings.Fields(line)
  147. var author author
  148. for _, field := range fields {
  149. if m := nicknameRe.FindStringSubmatch(field); len(m) > 1 {
  150. author.nickname = m[1]
  151. } else if m := emailRe.FindStringSubmatch(field); len(m) > 1 {
  152. author.emails = append(author.emails, m[1])
  153. } else {
  154. if author.name == "" {
  155. author.name = field
  156. } else {
  157. author.name = author.name + " " + field
  158. }
  159. }
  160. }
  161. authors = append(authors, author)
  162. }
  163. return authors
  164. }
  165. func readAll(path string) []byte {
  166. fd, err := os.Open(path)
  167. if err != nil {
  168. log.Fatal(err)
  169. }
  170. defer fd.Close()
  171. bs, err := io.ReadAll(fd)
  172. if err != nil {
  173. log.Fatal(err)
  174. }
  175. return bs
  176. }
  177. // Add number of commits per author to the author list.
  178. func getContributions(authors []author) {
  179. buf := new(bytes.Buffer)
  180. cmd := exec.Command("git", "log", "--pretty=format:%ae")
  181. cmd.Stdout = buf
  182. err := cmd.Run()
  183. if err != nil {
  184. log.Fatal(err)
  185. }
  186. next:
  187. for _, line := range strings.Split(buf.String(), "\n") {
  188. for i := range authors {
  189. for _, email := range authors[i].emails {
  190. if email == line {
  191. authors[i].commits++
  192. continue next
  193. }
  194. }
  195. }
  196. }
  197. for i := range authors {
  198. authors[i].log10commits = int(math.Log10(float64(authors[i].commits + 1)))
  199. }
  200. }
  201. // list of commits that we don't include in our author file; because they
  202. // are legacy things that don't affect code, are committed with incorrect
  203. // address, or for other reasons.
  204. var excludeCommits = stringSetFromStrings([]string{
  205. "a9339d0627fff439879d157c75077f02c9fac61b",
  206. "254c63763a3ad42fd82259f1767db526cff94a14",
  207. "32a76901a91ff0f663db6f0830e0aedec946e4d0",
  208. "bc7639b0ffcea52b2197efb1c0bb68b338d1c915",
  209. "9bdcadf6345aba3a939e9e58d85b89dbe9d44bc9",
  210. "b933e9666abdfcd22919dd458c930d944e1e1b7f",
  211. "b84d960a81c1282a79e2b9477558de4f1af6faae",
  212. })
  213. // allAuthors returns the set of authors in the git commit log, except those
  214. // in excluded commits.
  215. func allAuthors() map[string]string {
  216. // Format is hash, email, name, newline, body. The body is indented with
  217. // one space, to differentiate from the hash lines.
  218. args := append([]string{"log", "--format=%H %ae %an%n%w(,1,1)%b"})
  219. cmd := exec.Command("git", args...)
  220. bs, err := cmd.Output()
  221. if err != nil {
  222. log.Fatal("git:", err)
  223. }
  224. coAuthoredPrefix := "Co-authored-by: "
  225. names := make(map[string]string)
  226. skipCommit := false
  227. for _, line := range bytes.Split(bs, []byte{'\n'}) {
  228. if len(line) == 0 {
  229. continue
  230. }
  231. switch line[0] {
  232. case ' ':
  233. // Look for Co-authored-by: lines in the commit body.
  234. if skipCommit {
  235. continue
  236. }
  237. line = line[1:]
  238. if bytes.HasPrefix(line, []byte(coAuthoredPrefix)) {
  239. // Co-authored-by: Name Name <[email protected]>
  240. line = line[len(coAuthoredPrefix):]
  241. if name, email, ok := strings.Cut(string(line), "<"); ok {
  242. name = strings.TrimSpace(name)
  243. email = strings.Trim(strings.TrimSpace(email), "<>")
  244. if email == "@" {
  245. // GitHub special for users who hide their email.
  246. continue
  247. }
  248. if names[email] == "" {
  249. names[email] = name
  250. }
  251. }
  252. }
  253. default: // hash email name
  254. fields := strings.SplitN(string(line), " ", 3)
  255. if len(fields) != 3 {
  256. continue
  257. }
  258. hash, email, name := fields[0], fields[1], fields[2]
  259. if excludeCommits.has(hash) {
  260. skipCommit = true
  261. continue
  262. }
  263. skipCommit = false
  264. if names[email] == "" {
  265. names[email] = name
  266. }
  267. }
  268. }
  269. return names
  270. }
  271. // A simple string set type
  272. type stringSet map[string]struct{}
  273. func stringSetFromStrings(ss []string) stringSet {
  274. s := make(stringSet)
  275. for _, e := range ss {
  276. s.add(e)
  277. }
  278. return s
  279. }
  280. func (s stringSet) add(e string) {
  281. s[e] = struct{}{}
  282. }
  283. func (s stringSet) has(e string) bool {
  284. _, ok := s[e]
  285. return ok
  286. }
  287. func (s stringSet) except(other stringSet) stringSet {
  288. diff := make(stringSet)
  289. for e := range s {
  290. if !other.has(e) {
  291. diff.add(e)
  292. }
  293. }
  294. return diff
  295. }