golang.org/x/build@v0.0.0-20240506185731-218518f32b70/devapp/gophercon.go (about)

     1  // Copyright 2017 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Logic for the /gophercon endpoint which shows a semi-realtime dashboard of
     6  // contribution activity. Users add their Gerrit user IDs to a hard-coded GitHub
     7  // issue to provide a mapping of Gerrit ID to GitHub user, which allows any
     8  // activity from the user across GitHub and Gerrit to be associated with a
     9  // single GitHub user object. Points are awarded depending on the type of
    10  // activity performed and an aggregated total for all participants is displayed.
    11  
    12  package main
    13  
    14  import (
    15  	"encoding/json"
    16  	"fmt"
    17  	"log"
    18  	"net/http"
    19  	"sort"
    20  	"strconv"
    21  	"time"
    22  
    23  	"golang.org/x/build/maintner"
    24  )
    25  
    26  const issueNumGerritUserMapping = 27373 // Special sign-up issue.
    27  
    28  // intFromStr returns the first integer within s, allowing for non-numeric
    29  // characters to be present.
    30  func intFromStr(s string) (int, bool) {
    31  	var (
    32  		foundNum bool
    33  		r        int
    34  	)
    35  	for i := 0; i < len(s); i++ {
    36  		if s[i] >= '0' && s[i] <= '9' {
    37  			foundNum = true
    38  			r = r*10 + int(s[i]-'0')
    39  		} else if foundNum {
    40  			return r, true
    41  		}
    42  	}
    43  	if foundNum {
    44  		return r, true
    45  	}
    46  	return 0, false
    47  }
    48  
    49  // Keep these in sync with the frontend JS.
    50  const (
    51  	activityTypeRegister     = "REGISTER"
    52  	activityTypeCreateChange = "CREATE_CHANGE"
    53  	activityTypeAmendChange  = "AMEND_CHANGE"
    54  	activityTypeMergeChange  = "MERGE_CHANGE"
    55  )
    56  
    57  var pointsPerActivity = map[string]int{
    58  	activityTypeRegister:     1,
    59  	activityTypeCreateChange: 2,
    60  	activityTypeAmendChange:  2,
    61  	activityTypeMergeChange:  3,
    62  }
    63  
    64  // An activity represents something a contributor has done. e.g. register on
    65  // the GitHub issue, create a change, amend a change, etc.
    66  type activity struct {
    67  	Type    string    `json:"type"`
    68  	Created time.Time `json:"created"`
    69  	User    string    `json:"gitHubUser"`
    70  	Points  int       `json:"points"`
    71  }
    72  
    73  func (s *server) updateActivities() {
    74  	s.cMu.Lock()
    75  	defer s.cMu.Unlock()
    76  	repo := s.corpus.GitHub().Repo("golang", "go")
    77  	if repo == nil {
    78  		log.Println(`s.corpus.GitHub().Repo("golang", "go") = nil`)
    79  		return
    80  	}
    81  	issue := repo.Issue(issueNumGerritUserMapping)
    82  	if issue == nil {
    83  		log.Printf("repo.Issue(%d) = nil", issueNumGerritUserMapping)
    84  		return
    85  	}
    86  	latest := issue.Created
    87  	if len(s.activities) > 0 {
    88  		latest = s.activities[len(s.activities)-1].Created
    89  	}
    90  
    91  	var newActivities []activity
    92  	issue.ForeachComment(func(c *maintner.GitHubComment) error {
    93  		if !c.Created.After(latest) {
    94  			return nil
    95  		}
    96  		id, ok := intFromStr(c.Body)
    97  		if !ok {
    98  			return fmt.Errorf("intFromStr(%q) = %v", c.Body, ok)
    99  		}
   100  		s.userMapping[id] = c.User
   101  
   102  		newActivities = append(newActivities, activity{
   103  			Type:    activityTypeRegister,
   104  			Created: c.Created,
   105  			User:    c.User.Login,
   106  			Points:  pointsPerActivity[activityTypeRegister],
   107  		})
   108  		s.totalPoints += pointsPerActivity[activityTypeRegister]
   109  		return nil
   110  	})
   111  
   112  	s.corpus.Gerrit().ForeachProjectUnsorted(func(p *maintner.GerritProject) error {
   113  		p.ForeachCLUnsorted(func(cl *maintner.GerritCL) error {
   114  			// TODO(golang.org/issue/21984)
   115  			if cl.Commit == nil {
   116  				log.Printf("Got CL with nil Commit field: %+v", cl)
   117  				return nil
   118  			}
   119  			if !cl.Commit.CommitTime.After(latest) {
   120  				return nil
   121  			}
   122  			user := s.userMapping[cl.OwnerID()]
   123  			if user == nil {
   124  				return nil
   125  			}
   126  
   127  			newActivities = append(newActivities, activity{
   128  				Type:    activityTypeCreateChange,
   129  				Created: cl.Created,
   130  				User:    user.Login,
   131  				Points:  pointsPerActivity[activityTypeCreateChange],
   132  			})
   133  			s.totalPoints += pointsPerActivity[activityTypeCreateChange]
   134  			if cl.Version > 1 {
   135  				newActivities = append(newActivities, activity{
   136  					Type:    activityTypeAmendChange,
   137  					Created: cl.Commit.CommitTime,
   138  					User:    user.Login,
   139  					Points:  pointsPerActivity[activityTypeAmendChange],
   140  				})
   141  				s.totalPoints += pointsPerActivity[activityTypeAmendChange]
   142  			}
   143  			if cl.Status == "merged" {
   144  				newActivities = append(newActivities, activity{
   145  					Type:    activityTypeMergeChange,
   146  					Created: cl.Commit.CommitTime,
   147  					User:    user.Login,
   148  					Points:  pointsPerActivity[activityTypeMergeChange],
   149  				})
   150  				s.totalPoints += pointsPerActivity[activityTypeMergeChange]
   151  			}
   152  			return nil
   153  		})
   154  		return nil
   155  	})
   156  
   157  	sort.Sort(byCreated(newActivities))
   158  	s.activities = append(s.activities, newActivities...)
   159  }
   160  
   161  type byCreated []activity
   162  
   163  func (a byCreated) Len() int           { return len(a) }
   164  func (a byCreated) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   165  func (a byCreated) Less(i, j int) bool { return a[i].Created.Before(a[j].Created) }
   166  
   167  func (s *server) handleActivities(w http.ResponseWriter, r *http.Request) {
   168  	i, _ := strconv.Atoi(r.FormValue("since"))
   169  	since := time.Unix(int64(i)/1000, 0)
   170  
   171  	recentActivity := []activity{}
   172  	for _, a := range s.activities {
   173  		if a.Created.After(since) {
   174  			recentActivity = append(recentActivity, a)
   175  		}
   176  	}
   177  
   178  	s.cMu.RLock()
   179  	defer s.cMu.RUnlock()
   180  	w.Header().Set("Content-Type", "application/json")
   181  	result := struct {
   182  		Activities  []activity `json:"activities"`
   183  		TotalPoints int        `json:"totalPoints"`
   184  	}{
   185  		Activities:  recentActivity,
   186  		TotalPoints: s.totalPoints,
   187  	}
   188  	if err := json.NewEncoder(w).Encode(result); err != nil {
   189  		log.Printf("Encode(%+v) = %v", result, err)
   190  		return
   191  	}
   192  }