authors.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  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. # The format is:
  29. #
  30. # Name Name Name (nickname) <[email protected]> <[email protected]>
  31. #
  32. # After changing this list, run "go run script/authors.go" to sort and update
  33. # the GUI HTML.
  34. #
  35. `
  36. type author struct {
  37. name string
  38. nickname string
  39. emails []string
  40. commits int
  41. log10commits int
  42. }
  43. func main() {
  44. authors := getAuthors()
  45. // Write author names in GUI about modal
  46. getContributions(authors)
  47. sort.Sort(byContributions(authors))
  48. var lines []string
  49. for _, author := range authors {
  50. lines = append(lines, author.name)
  51. }
  52. replacement := strings.Join(lines, ", ")
  53. authorsRe := regexp.MustCompile(`(?s)id="contributor-list">.*?</div>`)
  54. bs := readAll(htmlFile)
  55. bs = authorsRe.ReplaceAll(bs, []byte("id=\"contributor-list\">\n"+replacement+"\n </div>"))
  56. if err := ioutil.WriteFile(htmlFile, bs, 0644); err != nil {
  57. log.Fatal(err)
  58. }
  59. // Write AUTHORS file
  60. sort.Sort(byName(authors))
  61. out, err := os.Create("AUTHORS")
  62. if err != nil {
  63. log.Fatal(err)
  64. }
  65. fmt.Fprintf(out, "%s\n", authorsHeader)
  66. for _, author := range authors {
  67. fmt.Fprintf(out, "%s", author.name)
  68. if author.nickname != "" {
  69. fmt.Fprintf(out, " (%s)", author.nickname)
  70. }
  71. for _, email := range author.emails {
  72. fmt.Fprintf(out, " <%s>", email)
  73. }
  74. fmt.Fprintf(out, "\n")
  75. }
  76. out.Close()
  77. }
  78. func getAuthors() []author {
  79. bs := readAll("AUTHORS")
  80. lines := strings.Split(string(bs), "\n")
  81. var authors []author
  82. for _, line := range lines {
  83. if len(line) == 0 || line[0] == '#' {
  84. continue
  85. }
  86. fields := strings.Fields(line)
  87. var author author
  88. for _, field := range fields {
  89. if m := nicknameRe.FindStringSubmatch(field); len(m) > 1 {
  90. author.nickname = m[1]
  91. } else if m := emailRe.FindStringSubmatch(field); len(m) > 1 {
  92. author.emails = append(author.emails, m[1])
  93. } else {
  94. if author.name == "" {
  95. author.name = field
  96. } else {
  97. author.name = author.name + " " + field
  98. }
  99. }
  100. }
  101. authors = append(authors, author)
  102. }
  103. return authors
  104. }
  105. func readAll(path string) []byte {
  106. fd, err := os.Open(path)
  107. if err != nil {
  108. log.Fatal(err)
  109. }
  110. defer fd.Close()
  111. bs, err := ioutil.ReadAll(fd)
  112. if err != nil {
  113. log.Fatal(err)
  114. }
  115. return bs
  116. }
  117. // Add number of commits per author to the author list.
  118. func getContributions(authors []author) {
  119. buf := new(bytes.Buffer)
  120. cmd := exec.Command("git", "log", "--pretty=format:%ae")
  121. cmd.Stdout = buf
  122. err := cmd.Run()
  123. if err != nil {
  124. log.Fatal(err)
  125. }
  126. next:
  127. for _, line := range strings.Split(buf.String(), "\n") {
  128. for i := range authors {
  129. for _, email := range authors[i].emails {
  130. if email == line {
  131. authors[i].commits++
  132. continue next
  133. }
  134. }
  135. }
  136. }
  137. for i := range authors {
  138. authors[i].log10commits = int(math.Log10(float64(authors[i].commits + 1)))
  139. }
  140. }
  141. type byContributions []author
  142. func (l byContributions) Len() int { return len(l) }
  143. // Sort first by log10(commits), then by name. This means that we first get
  144. // an alphabetic list of people with >= 1000 commits, then a list of people
  145. // with >= 100 commits, and so on.
  146. func (l byContributions) Less(a, b int) bool {
  147. if l[a].log10commits != l[b].log10commits {
  148. return l[a].log10commits > l[b].log10commits
  149. }
  150. return l[a].name < l[b].name
  151. }
  152. func (l byContributions) Swap(a, b int) { l[a], l[b] = l[b], l[a] }
  153. type byName []author
  154. func (l byName) Len() int { return len(l) }
  155. func (l byName) Less(a, b int) bool {
  156. aname := strings.ToLower(l[a].name)
  157. bname := strings.ToLower(l[b].name)
  158. return aname < bname
  159. }
  160. func (l byName) Swap(a, b int) { l[a], l[b] = l[b], l[a] }