func mailissue(ctxt appengine.Context, kind, key string) error { ctxt.Infof("mailissue %s", key) var cl CL err := app.ReadData(ctxt, "CL", key, &cl) if err != nil { return nil // error already logged } if len(cl.NeedMailIssue) == 0 { return nil } var mailed []string for _, issue := range cl.NeedMailIssue { err := postIssueComment(ctxt, issue, "CL https://codereview.appspot.com/"+cl.CL+" mentions this issue.") if err != nil { ctxt.Criticalf("posting to issue %v: %v", issue, err) continue } mailed = append(mailed, issue) } err = app.Transaction(ctxt, func(ctxt appengine.Context) error { var old CL if err := app.ReadData(ctxt, "CL", key, &old); err != nil { return err } old.MailedIssue = append(old.MailedIssue, mailed...) return app.WriteData(ctxt, "CL", key, &old) }) return err }
func loadpatch(ctxt appengine.Context, kind, key string) error { ctxt.Infof("loadpatch %s", key) var cl CL err := app.ReadData(ctxt, "CL", key, &cl) if err != nil { return nil // error already logged } if cl.PatchSetsLoaded { return nil } var last *Patch for _, id := range cl.PatchSets { var jp jsonPatch err := fetchJSON(ctxt, &jp, fmt.Sprintf("https://codereview.appspot.com/api/%s/%s", cl.CL, id)) if err != nil { return nil // already logged } p := jp.toPatch(ctxt) if err := app.WriteData(ctxt, "Patch", fmt.Sprintf("%s/%s", cl.CL, id), p); err != nil { return nil // already logged } last = p } err = app.Transaction(ctxt, func(ctxt appengine.Context) error { var old CL if err := app.ReadData(ctxt, "CL", key, &old); err != nil { return err } if len(old.PatchSets) > len(cl.PatchSets) { return fmt.Errorf("more patch sets added") } old.PatchSetsLoaded = true old.FilesModified = last.Modified old.Files = nil old.Delta = 0 for _, f := range last.Files { old.Files = append(old.Files, f.Name) old.Delta += int64(f.NumAdded + f.NumRemoved) } if len(old.Files) > 100 { old.Files = old.Files[:100] old.MoreFiles = true } if m := diffRE.FindStringSubmatch(last.Message); m != nil { old.Repo = m[1] } else if m := diffRE2.FindStringSubmatch(last.Message); m != nil { old.Repo = "code.google.com/p/" + m[1] } else if m := diffRE3.FindStringSubmatch(last.Message); m != nil { old.Repo = "code.google.com/p/" + m[2] + "." + m[1] } // NOTE: updateCL will shorten code.google.com/p/go to go. return app.WriteData(ctxt, "CL", key, &old) }) return err }
func writeCL(ctxt appengine.Context, cl *CL, mtimeKey, modified string) error { err := app.Transaction(ctxt, func(ctxt appengine.Context) error { var old CL if err := app.ReadData(ctxt, "CL", cl.CL, &old); err != nil && err != datastore.ErrNoSuchEntity { return err } if old.CL == "" { // no old data var count int64 app.ReadMeta(ctxt, "codereview.count", &count) app.WriteMeta(ctxt, "codereview.count", count+1) } // Copy CL into original structure. // This allows us to maintain other information in the CL structure // and not overwrite it when the Rietveld information is updated. if cl.Dead { old.Dead = true } else { old.Dead = false if old.Modified.After(cl.Modified) { return fmt.Errorf("CL %v: have %v but Rietveld sent %v", cl.CL, old.Modified, cl.Modified) } old.CL = cl.CL old.Desc = cl.Desc old.Owner = cl.Owner old.OwnerEmail = cl.OwnerEmail old.Created = cl.Created old.Modified = cl.Modified old.MessagesLoaded = cl.MessagesLoaded if cl.MessagesLoaded { old.Messages = cl.Messages old.Submitted = cl.Submitted } old.Reviewers = cl.Reviewers old.CC = cl.CC old.Closed = cl.Closed if !reflect.DeepEqual(old.PatchSets, cl.PatchSets) { old.PatchSets = cl.PatchSets old.PatchSetsLoaded = false } } if err := app.WriteData(ctxt, "CL", cl.CL, &old); err != nil { return err } if mtimeKey != "" { app.WriteMeta(ctxt, mtimeKey, modified) } return nil }) if err != nil { ctxt.Errorf("storing CL %v: %v", cl.CL, err) } return err }
func writeIssue(ctxt appengine.Context, issue *Issue, stateKey string, state interface{}) error { err := app.Transaction(ctxt, func(ctxt appengine.Context) error { var old Issue if err := app.ReadData(ctxt, "Issue", fmt.Sprint(issue.ID), &old); err != nil && err != datastore.ErrNoSuchEntity { return err } if old.ID == 0 { // no old data var count int64 app.ReadMeta(ctxt, "issue.count", &count) app.WriteMeta(ctxt, "issue.count", count+1) } if old.Modified.After(issue.Modified) { return fmt.Errorf("issue %v: have %v but code.google.com sent %v", issue.ID, old.Modified, issue.Modified) } // Copy Issue into original structure. // This allows us to maintain other information in the Issue structure // and not overwrite it when the issue information is updated. old.ID = issue.ID old.Summary = issue.Summary old.Status = issue.Status old.Duplicate = issue.Duplicate old.Owner = issue.Owner old.CC = issue.CC old.Label = issue.Label old.Comment = issue.Comment old.State = issue.State old.Created = issue.Created old.Modified = issue.Modified old.Stars = issue.Stars old.ClosedDate = issue.ClosedDate updateIssue(&old) if err := app.WriteData(ctxt, "Issue", fmt.Sprint(issue.ID), &old); err != nil { return err } if stateKey != "" { app.WriteMeta(ctxt, stateKey, state) } return nil }) if err != nil { ctxt.Errorf("storing issue %v: %v", issue.ID, err) } return err }
func addTodo(ctxt appengine.Context, repo, branch, hash string) error { ctxt.Infof("add todo %s %s %s\n", repo, branch, hash) return app.Transaction(ctxt, func(ctxt appengine.Context) error { var rev Rev if err := app.ReadData(ctxt, "Rev", repo+"."+hash, &rev); err != datastore.ErrNoSuchEntity { if err == nil { if rev.Branch != branch { return errBranched } return errDone } return err } return writeTodo(ctxt, repo, branch, hash, false) }) }
func postMovedNote(ctxt appengine.Context, kind, id string) error { var old Issue if err := app.ReadData(ctxt, "Issue", id, &old); err != nil { return err } updateIssue(&old) if !old.NeedGithubNote { err := app.Transaction(ctxt, func(ctxt appengine.Context) error { var old Issue if err := app.ReadData(ctxt, "Issue", id, &old); err != nil { return err } old.NeedGithubNote = false return app.WriteData(ctxt, "Issue", id, &old) }) return err } cfg, err := oauthConfig(ctxt) if err != nil { return fmt.Errorf("oauthconfig: %v", err) } var tok oauth.Token if err := app.ReadMeta(ctxt, "codelogin.token", &tok); err != nil { return fmt.Errorf("reading token: %v", err) } tr := &oauth.Transport{ Config: cfg, Token: &tok, Transport: &urlfetch.Transport{Context: ctxt, Deadline: 45 * time.Second}, } client := tr.Client() status := "" if old.State != "closed" { status = "<issues:status>Moved</issues:status>" } var buf bytes.Buffer buf.WriteString(`<?xml version='1.0' encoding='UTF-8'?> <entry xmlns='http://www.w3.org/2005/Atom' xmlns:issues='http://schemas.google.com/projecthosting/issues/2009'> <content type='html'>`) xml.Escape(&buf, []byte(fmt.Sprintf("This issue has moved to https://golang.org/issue/%s\n", id))) buf.WriteString(`</content> <author> <name>ignored</name> </author> <issues:sendEmail>False</issues:sendEmail> <issues:updates> <issues:label>IssueMoved</issues:label> <issues:label>Restrict-AddIssueComment-Commit</issues:label> ` + status + ` </issues:updates> </entry> `) u := "https://code.google.com/feeds/issues/p/go/issues/" + id + "/comments/full" req, err := http.NewRequest("POST", u, &buf) if err != nil { return fmt.Errorf("write: %v", err) } req.Header.Set("Content-Type", "application/atom+xml") resp, err := client.Do(req) if err != nil { return fmt.Errorf("write: %v", err) } defer resp.Body.Close() if resp.StatusCode != 201 { buf.Reset() io.Copy(&buf, resp.Body) return fmt.Errorf("write: %v\n%s", resp.Status, buf.String()) } err = app.Transaction(ctxt, func(ctxt appengine.Context) error { var old Issue if err := app.ReadData(ctxt, "Issue", id, &old); err != nil { return err } old.NeedGithubNote = false old.Label = append(old.Label, "IssueMoved", "Restrict-AddIssueComment-Commit") return app.WriteData(ctxt, "Issue", id, &old) }) return err }
func uiOperation(ctxt appengine.Context, w http.ResponseWriter, req *http.Request) { email := findEmail(ctxt) d := display{email: email} if d.email == "" { w.WriteHeader(501) fmt.Fprintf(w, "must be logged in") return } if req.Method != "POST" { w.WriteHeader(501) fmt.Fprintf(w, "must POST") return } // TODO: XSRF protection switch op := req.FormValue("op"); op { default: w.WriteHeader(501) fmt.Fprintf(w, "invalid verb") return case "mute", "unmute": targ := req.FormValue("dir") if targ == "" { w.WriteHeader(501) fmt.Fprintf(w, "missing dir") return } err := app.Transaction(ctxt, func(ctxt appengine.Context) error { var pref UserPref app.ReadData(ctxt, "UserPref", d.email, &pref) for i, dir := range pref.Muted { if dir == targ { if op == "unmute" { pref.Muted = append(pref.Muted[:i], pref.Muted[i+1:]...) break } return nil } } if op == "mute" { pref.Muted = append(pref.Muted, targ) sort.Strings(pref.Muted) } return app.WriteData(ctxt, "UserPref", d.email, &pref) }) if err != nil { w.WriteHeader(501) fmt.Fprintf(w, "unable to update") return } case "reviewer": clnum := req.FormValue("cl") who := req.FormValue("reviewer") switch who { case "close", "golang-dev": // ok default: who = codereview.ExpandReviewer(who) } w.Header().Set("Content-Type", "text/plain; charset=utf-8") if who == "" { fmt.Fprintf(w, "ERROR: unknown reviewer") return } if err := codereview.SetReviewer(ctxt, clnum, who); err != nil { fmt.Fprintf(w, "ERROR: setting reviewer: %v", err) return } var cl codereview.CL if err := app.ReadData(ctxt, "CL", clnum, &cl); err != nil { fmt.Fprintf(w, "ERROR: refreshing CL: %v", err) return } fmt.Fprintf(w, "%s", d.short(d.reviewer(&cl))) return } }
func loadRevOnce(ctxt appengine.Context, repo, branch, hash string) (nextHash string) { ctxt.Infof("load todo %s %s %s", repo, branch, hash) // Check that this todo is still valid. // If so, extend the expiry time so that no one else tries it while we do. // This supercedes the usual use of app.Lock and app.Unlock and also // provides a way to rate limit the polling. todoKey := fmt.Sprintf("commit.todo.%s.%s", repo, hash) err := app.Transaction(ctxt, func(ctxt appengine.Context) error { var todo revTodo if err := app.ReadData(ctxt, "RevTodo", todoKey, &todo); err != nil { return err } if time.Now().Before(todo.Time) { ctxt.Infof("poll %s %s not scheduled until %v", repo, hash, todo.Time) return errWait } dtAll := todo.Time.Sub(todo.Start) dtOne := todo.Time.Sub(todo.Last) var dtMax time.Duration if dtAll < 24*time.Hour { dtMax = 5 * time.Minute } else if dtAll < 7*24*time.Hour { dtMax = 1 * time.Hour } else { dtMax = 24 * time.Hour } if dtOne *= 2; dtOne > dtMax { dtOne = dtMax } else if dtOne == 0 { dtOne = 1 * time.Minute } todo.Last = time.Now() todo.Time = todo.Last.Add(dtOne) if err := app.WriteData(ctxt, "RevTodo", todoKey, &todo); err != nil { return err } return nil }) if err != nil { ctxt.Errorf("skipping poll: %v", err) return "" } r, err := fetchRev(ctxt, repo, hash) if err != nil { ctxt.Errorf("fetching %v %v: %v", repo, hash, err) return "" } err = app.Transaction(ctxt, func(ctxt appengine.Context) error { var old Rev if err := app.ReadData(ctxt, "Rev", repo+"."+hash, &old); err != nil && err != datastore.ErrNoSuchEntity { return err } if old.Hash == r.Hash && len(old.Next) == len(r.Next) { // up to date return nil } if old.Hash == "" { // no old data var count int if err := app.ReadMeta(ctxt, "commit.count."+repo, &count); err != nil && err != datastore.ErrNoSuchEntity { return err } count++ old.Seq = count if err := app.WriteMeta(ctxt, "commit.count."+repo, count); err != nil { return err } if r.Branch != branch && len(r.Prev) == 1 { ctxt.Infof("detected branch; forcing todo of parent") err := writeTodo(ctxt, repo, branch, r.Prev[0], true) if err != nil { ctxt.Errorf("re-adding todo: %v", err) } nextHash = r.Prev[0] } } old.Repo = r.Repo old.Branch = r.Branch // old.Seq already correct; r.Seq is not old.Hash = r.Hash old.ShortHash = old.Hash[:12] old.Prev = r.Prev old.Next = r.Next old.Author = r.Author old.AuthorEmail = r.AuthorEmail old.Time = r.Time old.Log = r.Log old.Files = r.Files if err := app.WriteData(ctxt, "Rev", repo+"."+hash, &old); err != nil { return err } return nil }) if err != nil { ctxt.Errorf("updating %v %v: %v", repo, hash, err) return "" } if r.Next == nil { ctxt.Errorf("leaving todo for %s %s - no next yet", repo, hash) return "" } success := true forward := false for _, next := range r.Next { err := addTodo(ctxt, repo, r.Branch, next) if err == errDone { forward = true continue } if err == errBranched { ctxt.Infof("%v -> %v is a branch", r.Hash[:12], next[:12]) continue } if err != nil { ctxt.Errorf("storing todo for %s %s: %v %p %p", repo, next, err, err, errDone) success = false } forward = true // innocent until proven guilty if nextHash == "" { nextHash = next } else { laterLoadRev.Call(ctxt, repo, r.Branch, next) } } if forward && success { ctxt.Infof("delete todo %s\n", todoKey) app.DeleteData(ctxt, "RevTodo", todoKey) } else { ctxt.Errorf("leaving todo for %s %s due to errors or branching", repo, hash) } return nextHash }