Beispiel #1
0
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
}
Beispiel #2
0
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
}
Beispiel #3
0
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
}
Beispiel #4
0
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
}
Beispiel #5
0
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)
	})
}
Beispiel #6
0
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
}
Beispiel #7
0
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
	}
}
Beispiel #8
0
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
}