authors.go 6.9 KB

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