func codelogin(ctxt appengine.Context, w http.ResponseWriter, req *http.Request) { randState, err := randomID() if err != nil { http.Error(w, err.Error(), 500) return } clientID := req.FormValue("clientid") if clientID != "" { app.WriteMeta(ctxt, "googleapi.clientid", &clientID) } clientSecret := req.FormValue("clientsecret") if clientSecret != "" { app.WriteMeta(ctxt, "googleapi.clientsecret", &clientSecret) } if err := app.WriteMeta(ctxt, "codelogin.random", &randState); err != nil { http.Error(w, err.Error(), 500) return } cfg, err := oauthConfig(ctxt) if err != nil { http.Error(w, err.Error(), 500) return } authURL := cfg.AuthCodeURL(randState) http.Redirect(w, req, authURL, 301) }
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 codetoken(ctxt appengine.Context, w http.ResponseWriter, req *http.Request) { var randState string if err := app.ReadMeta(ctxt, "codelogin.random", &randState); err != nil { panic(err) } cfg, err := oauthConfig(ctxt) if err != nil { http.Error(w, err.Error(), 500) return } if req.FormValue("state") != randState { http.Error(w, "bad state", 500) return } code := req.FormValue("code") if code == "" { http.Error(w, "missing code", 500) return } tr := &oauth.Transport{ Config: cfg, Transport: urlfetch.Client(ctxt).Transport, } _, err = tr.Exchange(code) if err != nil { http.Error(w, "exchanging code: "+err.Error(), 500) return } if err := app.WriteMeta(ctxt, "codelogin.token", tr.Token); err != nil { http.Error(w, "writing token: "+err.Error(), 500) return } app.DeleteMeta(ctxt, "codelogin.random") fmt.Fprintf(w, "have token; expires at %v\n", tr.Token.Expiry) }
func load(ctxt appengine.Context) error { mtime := time.Date(2009, 1, 1, 0, 0, 0, 0, time.UTC) if appengine.IsDevAppServer() { mtime = time.Now().UTC().Add(-24 * time.Hour) } app.ReadMeta(ctxt, "issue.mtime", &mtime) now := time.Now() var issues []*Issue var err error const maxResults = 500 var try int needMore := false for try = 0; ; try++ { issues, err = search(ctxt, "go", "all", "", false, mtime, now, maxResults) if err != nil { ctxt.Errorf("load issues since %v: %v", mtime, err) return nil } if len(issues) == 0 { ctxt.Infof("no updates found from %v to %v", mtime, now) app.WriteMeta(ctxt, "issue.mtime", now.Add(-1*time.Minute)) if try > 0 { // We shortened the time range; try again now that we've updated mtime. return app.ErrMoreCron } return nil } if len(issues) < maxResults { ctxt.Infof("%d issues from %v to %v", len(issues), mtime, now) if try > 0 { // Keep exploring once we finish this load. needMore = true } break } ctxt.Errorf("updater found too many updates from %v to %v", mtime, now) if now.Sub(mtime) <= 2*time.Second { ctxt.Errorf("cannot shorten update time frame") return nil } if now.Sub(mtime) > 1*time.Hour { now = mtime.Add(now.Sub(mtime) / 10) } else { now = mtime.Add(now.Sub(mtime) / 2) } ctxt.Infof("shortened to %v to %v", mtime, now) } issues, err = search(ctxt, "go", "all", "", true, mtime, now, maxResults) if err != nil { ctxt.Errorf("full load of issues from %v to %v: %v", mtime, now, err) return nil } if len(issues) == 0 { ctxt.Errorf("unexpected: no issues from %v to %v", mtime, now) return nil } for _, issue := range issues { println("WRITE ISSUE", issue.ID) if err := writeIssue(ctxt, issue, "", nil); err != nil { return nil } if mtime.Before(issue.Modified) { mtime = issue.Modified } } if try > 0 { mtime = now.Add(-1 * time.Second) } app.WriteMeta(ctxt, "issue.mtime", mtime.UTC()) if needMore { return app.ErrMoreCron } return nil }
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 }