package main import ( "bufio" "bytes" "fmt" "log" "net/http" "os" "os/exec" "sort" "strconv" "strings" "camlistore.org/pkg/osutil" ) var urlsMap = map[string]author{ "brad@danga.com": {URL: "http://bradfitz.com/", Role: "founder, lead"}, "bslatkin@gmail.com": {URL: "http://www.onebigfluke.com/", Role: "co-founder"}, "mathieu.lonjaret@gmail.com": {URL: "https://granivo.re/", Role: "has touched almost everything"}, "zboogs@gmail.com": {URL: "http://www.aaronboodman.com/", Role: "web interface lead"}, "adg@golang.org": {URL: "http://nf.id.au/"}, "dustin@spy.net": {URL: "http://dustin.sallings.org/"}, "dan@erat.org": {URL: "http://www.erat.org/"}, "martine@danga.com": {URL: "http://neugierig.org/"}, "agl@golang.org": {URL: "http://www.imperialviolet.org/"}, "lsimon@commoner.com": {Role: "original publishing UI"}, "s@0x65.net": {URL: "https://0x65.net/"}, } type author struct { Names []string Emails []string Commits int Role string URL string } // add merges src's fields into a's. func (a *author) add(src *author) { if src == nil { return } a.Emails = append(a.Emails, src.Emails...) a.Names = append(a.Names, src.Names...) a.Commits += src.Commits if src.Role != "" { a.Role = src.Role } if src.URL != "" { a.URL = src.URL } } type Authors []*author func (s Authors) Len() int { return len(s) } func (s Authors) Swap(i, j int) { s[i], s[j] = s[j], s[i] } func (s Authors) Less(i, j int) bool { return s[i].Commits > s[j].Commits } func parseLine(l string) (name, email string, commits int, err error) { t := strings.Split(strings.TrimSpace(l), " ") if len(t) < 2 { err = fmt.Errorf("line too short") return } i := strings.LastIndex(t[1], " ") email = strings.Trim(t[1][i+1:], "<>") name = t[1][:i] commits, err = strconv.Atoi(t[0]) return } func gitShortlog() *exec.Cmd { if !*gitContainer { return exec.Command("/bin/bash", "-c", "git log | git shortlog -sen") } args := []string{"run", "--rm"} if inProd { args = append(args, "-v", "/var/camweb:/var/camweb", "--workdir="+prodSrcDir, ) } else { hostRoot, err := osutil.GoPackagePath("camlistore.org") if err != nil { log.Fatal(err) } log.Printf("Using bind root of %q", hostRoot) args = append(args, "-v", hostRoot+":"+prodSrcDir, "--workdir="+prodSrcDir, ) } args = append(args, "camlistore/git", "/bin/bash", "-c", "git log | git shortlog -sen") cmd := exec.Command("docker", args...) cmd.Stderr = os.Stderr return cmd } func genContribPage() ([]byte, error) { contribHTML := readTemplate("contributors.html") // The same committers could've authored commits with different emails/usersnames. // We index the authors by name and by email to try and merge graphs connected by // the "same-email" and "same-name" relation into one entity. byName := make(map[string]*author) byEmail := make(map[string]*author) authorMap := make(map[*author]bool) shortlog := gitShortlog() shortlogOut, err := shortlog.StdoutPipe() if err != nil { return nil, err } err = shortlog.Start() if err != nil { return nil, fmt.Errorf("couldn't run git shortlog: %v", err) } scn := bufio.NewScanner(shortlogOut) for scn.Scan() { name, email, commits, err := parseLine(scn.Text()) if err != nil { return nil, err } a := &author{ Emails: []string{email}, Names: []string{name}, Commits: commits, } a.add(byName[name]) a.add(byEmail[email]) for _, n := range a.Names { delete(authorMap, byName[n]) byName[n] = a } for _, e := range a.Emails { delete(authorMap, byEmail[e]) byEmail[e] = a } authorMap[a] = true } if scn.Err() != nil { return nil, scn.Err() } err = shortlog.Wait() if err != nil { return nil, fmt.Errorf("git shortlog failed: %v", err) } // Add URLs and roles for email, m := range urlsMap { a := byEmail[email] if a != nil { a.add(&m) } } authors := Authors{} for a, _ := range authorMap { authors = append(authors, a) } sort.Sort(authors) b := &bytes.Buffer{} err = contribHTML.Execute(b, authors) return b.Bytes(), err } // contribHandler returns a handler that serves the generated contributors page, // or the static file handler if it couldn't run git for any reason. func contribHandler() http.HandlerFunc { c, err := genContribPage() if err != nil { log.Printf("Couldn't generate contributors page: %v", err) log.Printf("Using static contributors page") return mainHandler } title := "" if m := h1TitlePattern.FindSubmatch(c); len(m) > 1 { title = string(m[1]) } return func(w http.ResponseWriter, r *http.Request) { servePage(w, pageParams{ title: title, content: c, }) } }