syncthing/script/authors.go

359 lines
8.1 KiB
Go
Raw Permalink Normal View History

// 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"
"fmt"
"io"
"log"
"math"
"os"
"os/exec"
"regexp"
"sort"
"strings"
)
2016-01-10 15:37:31 +00:00
const htmlFile = "gui/default/syncthing/core/aboutModalView.html"
2015-08-02 06:27:05 +00:00
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) <email1@example.com> <email2@example.com>
#
# The in-GUI authors list is periodically automatically updated from the
# contents of this file.
2017-12-04 21:57:35 +00:00
#
`
type author struct {
name string
nickname string
emails []string
commits int
log10commits int
}
func main() {
// Read authors from the AUTHORS file
authors := getAuthors()
2017-12-04 21:57:35 +00:00
// Grab the set of thus known email addresses
listed := make(stringSet)
names := make(map[string]int)
for i, a := range authors {
names[a.name] = i
for _, e := range a.emails {
listed.add(e)
}
}
// Grab the set of all known authors based on the git log, and add any
// missing ones to the authors list.
all := allAuthors()
for email, name := range all {
if listed.has(email) {
continue
}
if _, ok := names[name]; ok && name != "" {
// We found a match on name
authors[names[name]].emails = append(authors[names[name]].emails, email)
listed.add(email)
continue
}
authors = append(authors, author{
name: name,
emails: []string{email},
})
names[name] = len(authors) - 1
listed.add(email)
}
2017-12-04 21:57:35 +00:00
// Write author names in GUI about modal
getContributions(authors)
sort.Sort(byContributions(authors))
var lines []string
for _, author := range authors {
if authorBotsRe.MatchString(author.name) {
// Only humans are eligible, pending future legislation to the
// contrary.
continue
}
lines = append(lines, author.name)
}
replacement := strings.Join(lines, ", ")
authorsRe := regexp.MustCompile(`(?s)id="contributor-list">.*?</div>`)
bs := readAll(htmlFile)
gui, api: Show internal config and state paths (fixes #8323) (#8324) * lib/locations: Fix enum values camelCase. * lib/locations: Remove unused FailuresFile. * cmd/syncthing: Turn around role of locations storage. Previously the locations package was used to provide default paths, possibly with an overridden home directory. Extra paths supplied on the command line were handled and passed around in the options object. To make the changed paths available to any other interested package, override the location setting from the option if supplied, instead of vice versa when not supplied. Adapt code using this to read from the locations package instead of passing through the options object. * lib/locations: Refactor showPaths to locations package. Generate a reusable string in locations.PrettyPrintPaths(). Enumerating all possible locations in different packages is error prone, so add a new public function to generate the listing as a string in the locations package. Adapt cmd/syncthing --paths to use that instead of its own console output. * lib/locations: Include CSRF token in pretty printed paths. * lib/api: New endpoint /rest/system/paths. The paths should be available for troubleshooting from a running instance. Using the --paths CLI option is not easy in some environments, so expose the locations mapping to a JSON endpoint. Add utility function ListExpandedPaths() that also filters out any entries which still contain variable placeholders. * gui: List runtime paths in separate log viewer tab. * Wrap paths. * lib/syncthing: Utilize locations.Get() instead of passing an arg. * Include base directories, move label to table caption. * gui: Switch to hard-coded paths instead of iterating over all. * gui: Break aboutModalView into tabs. Use tabs to separate authors from included third-party software. * gui: Move paths from log viewer to about modal. * lib/locations: Adjust pretty print output order to match GUI. * gui, authors: Remove additional bot names and fix indent. The indentation changed because of the tabbed about dialog, fix the authors script to respect that. Skip Syncthing*Automation in authors list as well. * Update AUTHORS list to remove bot names. * Revert AUTHORS email order change. * Do not emphasize DB and log file locations. * Review line wrapping. * review part 1: strings.Builder, naming * Rename and extend locations.Set() with error handling. Remodel the Override() function along the existing SetBaseDir() and rename it to simply Set(). Make sure to use absolute paths when given log file or GUI assets override options. Add proper error reporting if that goes wrong. * Remove obsolete comment about empty logfile option. * Don't filter out unexpanded baseDir placeholders, only ${timestamp}. * Restore behavior regarding special "-" logfile argument. If the option is given, but with empty value, assume the no log file (same as "-"). Don't try to convert the special value to an absolute path though and document this fact in a comment for the Set() function. * Use template to check for location key validity. * Don't filter out timestamp placeholders. * lib/api: Remove paths from /rest/system/status. * lib/ur: Properly initialize map in failure data (fixes #8479) Co-authored-by: Jakob Borg <jakob@kastelo.net>
2022-08-10 06:25:13 +00:00
bs = authorsRe.ReplaceAll(bs, []byte("id=\"contributor-list\">\n"+replacement+"\n </div>"))
if err := os.WriteFile(htmlFile, bs, 0644); err != nil {
log.Fatal(err)
}
// Write AUTHORS file
sort.Sort(byName(authors))
out, err := os.Create("AUTHORS")
if err != nil {
log.Fatal(err)
}
2017-12-04 21:57:35 +00:00
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.Fprintf(out, "\n")
}
out.Close()
}
func getAuthors() []author {
bs := readAll("AUTHORS")
lines := strings.Split(string(bs), "\n")
var authors []author
for _, line := range lines {
if len(line) == 0 || line[0] == '#' {
continue
}
fields := strings.Fields(line)
var author author
for _, field := range fields {
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 = append(authors, author)
}
return authors
}
func readAll(path string) []byte {
fd, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
defer fd.Close()
bs, err := io.ReadAll(fd)
if err != nil {
log.Fatal(err)
}
return bs
}
// Add number of commits per author to the author list.
func getContributions(authors []author) {
buf := new(bytes.Buffer)
cmd := exec.Command("git", "log", "--pretty=format:%ae")
cmd.Stdout = buf
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
next:
for _, line := range strings.Split(buf.String(), "\n") {
for i := range authors {
for _, email := range authors[i].emails {
if email == line {
authors[i].commits++
continue next
}
}
}
}
for i := range authors {
authors[i].log10commits = int(math.Log10(float64(authors[i].commits + 1)))
}
}
// 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",
2020-03-02 15:31:29 +00:00
"9bdcadf6345aba3a939e9e58d85b89dbe9d44bc9",
"b933e9666abdfcd22919dd458c930d944e1e1b7f",
"b84d960a81c1282a79e2b9477558de4f1af6faae",
})
// allAuthors returns the set of authors in the git commit log, except those
// in excluded commits.
func allAuthors() map[string]string {
// Format is hash, email, name, newline, body. The body is indented with
// one space, to differentiate from the hash lines.
args := append([]string{"log", "--format=%H %ae %an%n%w(,1,1)%b"})
cmd := exec.Command("git", args...)
bs, err := cmd.Output()
if err != nil {
log.Fatal("git:", err)
}
coAuthoredPrefix := "Co-authored-by: "
names := make(map[string]string)
skipCommit := false
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@example.com>
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
}
if names[email] == "" {
names[email] = name
}
}
}
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
if names[email] == "" {
names[email] = name
}
}
}
return names
}
type byContributions []author
func (l byContributions) Len() int { return len(l) }
// Sort first by log10(commits), then by name. This means that we first get
// an alphabetic list of people with >= 1000 commits, then a list of people
// with >= 100 commits, and so on.
func (l byContributions) Less(a, b int) bool {
if l[a].log10commits != l[b].log10commits {
return l[a].log10commits > l[b].log10commits
}
return l[a].name < l[b].name
}
func (l byContributions) Swap(a, b int) { l[a], l[b] = l[b], l[a] }
type byName []author
func (l byName) Len() int { return len(l) }
func (l byName) Less(a, b int) bool {
aname := strings.ToLower(l[a].name)
bname := strings.ToLower(l[b].name)
return aname < bname
}
func (l byName) Swap(a, b int) { l[a], l[b] = l[b], l[a] }
// 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
}
func (s stringSet) except(other stringSet) stringSet {
diff := make(stringSet)
for e := range s {
if !other.has(e) {
diff.add(e)
}
}
return diff
}