blob: c2e1798dce7c6395d0a39434902de2ce5cddfc59 [file] [log] [blame]
package firestore
import (
"fmt"
"net/url"
"time"
fs "cloud.google.com/go/firestore"
"go.skia.org/infra/go/util"
"go.skia.org/infra/task_scheduler/go/db"
"go.skia.org/infra/task_scheduler/go/types"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
COLLECTION_COMMIT_COMMENTS = "commit-comments"
COLLECTION_TASK_COMMENTS = "task-comments"
COLLECTION_TASK_SPEC_COMMENTS = "task-spec-comments"
)
// commitComments returns a reference to the commit comments collection.
func (d *firestoreDB) commitComments() *fs.CollectionRef {
return d.client.Collection(COLLECTION_COMMIT_COMMENTS)
}
// taskComments returns a reference to the task comments collection.
func (d *firestoreDB) taskComments() *fs.CollectionRef {
return d.client.Collection(COLLECTION_TASK_COMMENTS)
}
// taskSpecComments returns a reference to the task spec comments collection.
func (d *firestoreDB) taskSpecComments() *fs.CollectionRef {
return d.client.Collection(COLLECTION_TASK_SPEC_COMMENTS)
}
// See documentation for db.CommentDB interface.
func (d *firestoreDB) GetCommentsForRepos(repos []string, from time.Time) ([]*types.RepoComments, error) {
from = fixTimestamp(from)
// TODO(borenet): We should come up with something more efficient, but
// this is convenient because it doesn't require composite indexes.
commentsByRepo := make(map[string]*types.RepoComments, len(repos))
for _, repo := range repos {
commentsByRepo[repo] = &types.RepoComments{
Repo: repo,
}
}
q := d.commitComments().Where("Timestamp", ">=", from).OrderBy("Timestamp", fs.Asc)
if err := d.client.IterDocs("GetCommitCommentsForRepos", from.String(), q, DEFAULT_ATTEMPTS, GET_MULTI_TIMEOUT, func(doc *fs.DocumentSnapshot) error {
var c types.CommitComment
if err := doc.DataTo(&c); err != nil {
return err
}
if comments, ok := commentsByRepo[c.Repo]; ok {
if comments.CommitComments == nil {
comments.CommitComments = map[string][]*types.CommitComment{}
}
comments.CommitComments[c.Revision] = append(comments.CommitComments[c.Revision], &c)
}
return nil
}); err != nil {
return nil, err
}
q = d.taskComments().Where("Timestamp", ">=", from).OrderBy("Timestamp", fs.Asc)
if err := d.client.IterDocs("GetTaskCommentsForRepos", from.String(), q, DEFAULT_ATTEMPTS, GET_MULTI_TIMEOUT, func(doc *fs.DocumentSnapshot) error {
var c types.TaskComment
if err := doc.DataTo(&c); err != nil {
return err
}
if comments, ok := commentsByRepo[c.Repo]; ok {
if comments.TaskComments == nil {
comments.TaskComments = map[string]map[string][]*types.TaskComment{}
}
byCommit, ok := comments.TaskComments[c.Revision]
if !ok {
byCommit = map[string][]*types.TaskComment{}
comments.TaskComments[c.Revision] = byCommit
}
byCommit[c.Name] = append(byCommit[c.Name], &c)
}
return nil
}); err != nil {
return nil, err
}
q = d.taskSpecComments().OrderBy("Timestamp", fs.Asc)
if err := d.client.IterDocs("GetTaskSpecCommentsForRepos", "", q, DEFAULT_ATTEMPTS, GET_MULTI_TIMEOUT, func(doc *fs.DocumentSnapshot) error {
var c types.TaskSpecComment
if err := doc.DataTo(&c); err != nil {
return err
}
if comments, ok := commentsByRepo[c.Repo]; ok {
if comments.TaskSpecComments == nil {
comments.TaskSpecComments = map[string][]*types.TaskSpecComment{}
}
comments.TaskSpecComments[c.Name] = append(comments.TaskSpecComments[c.Name], &c)
}
return nil
}); err != nil {
return nil, err
}
rv := make([]*types.RepoComments, 0, len(repos))
for _, repo := range repos {
rv = append(rv, commentsByRepo[repo])
}
return rv, nil
}
// taskCommentId returns an ID for the TaskComment.
func taskCommentId(c *types.TaskComment) string {
return fmt.Sprintf("%s#%s#%s#%s", url.QueryEscape(c.Repo), c.Revision, c.Name, fixTimestamp(c.Timestamp).Format(util.SAFE_TIMESTAMP_FORMAT))
}
// See documentation for db.CommentDB interface.
func (d *firestoreDB) PutTaskComment(c *types.TaskComment) error {
c.Timestamp = fixTimestamp(c.Timestamp)
id := taskCommentId(c)
_, err := d.client.Create(d.taskComments().Doc(id), c, DEFAULT_ATTEMPTS, PUT_SINGLE_TIMEOUT)
if st, ok := status.FromError(err); ok && st.Code() == codes.AlreadyExists {
return db.ErrAlreadyExists
}
if err != nil {
return err
}
d.TrackModifiedTaskComment(c)
return nil
}
// See documentation for db.CommentDB interface.
func (d *firestoreDB) DeleteTaskComment(c *types.TaskComment) error {
id := taskCommentId(c)
ref := d.taskComments().Doc(id)
exists := true
if _, err := d.client.Get(ref, DEFAULT_ATTEMPTS, GET_SINGLE_TIMEOUT); err != nil {
if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound {
exists = false
} else {
return err
}
}
if exists {
if _, err := d.client.Delete(ref, DEFAULT_ATTEMPTS, PUT_SINGLE_TIMEOUT); err != nil {
return err
}
deleted := true
c.Deleted = &deleted
d.TrackModifiedTaskComment(c)
}
return nil
}
// taskSpecCommentId returns an ID for the TaskSpecComment.
func taskSpecCommentId(c *types.TaskSpecComment) string {
return fmt.Sprintf("%s#%s#%s", url.QueryEscape(c.Repo), c.Name, c.Timestamp.Format(util.SAFE_TIMESTAMP_FORMAT))
}
// See documentation for db.CommentDB interface.
func (d *firestoreDB) PutTaskSpecComment(c *types.TaskSpecComment) error {
c.Timestamp = fixTimestamp(c.Timestamp)
id := taskSpecCommentId(c)
_, err := d.client.Create(d.taskSpecComments().Doc(id), c, DEFAULT_ATTEMPTS, PUT_SINGLE_TIMEOUT)
if st, ok := status.FromError(err); ok && st.Code() == codes.AlreadyExists {
return db.ErrAlreadyExists
}
if err != nil {
return err
}
d.TrackModifiedTaskSpecComment(c)
return nil
}
// See documentation for db.CommentDB interface.
func (d *firestoreDB) DeleteTaskSpecComment(c *types.TaskSpecComment) error {
id := taskSpecCommentId(c)
ref := d.taskSpecComments().Doc(id)
exists := true
if _, err := d.client.Get(ref, DEFAULT_ATTEMPTS, GET_SINGLE_TIMEOUT); err != nil {
if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound {
exists = false
} else {
return err
}
}
if exists {
if _, err := d.client.Delete(ref, DEFAULT_ATTEMPTS, PUT_SINGLE_TIMEOUT); err != nil {
return err
}
deleted := true
c.Deleted = &deleted
d.TrackModifiedTaskSpecComment(c)
}
return nil
}
// commitCommentId returns an ID for the CommitComment.
func commitCommentId(c *types.CommitComment) string {
return fmt.Sprintf("%s#%s#%s", url.QueryEscape(c.Repo), c.Revision, c.Timestamp.Format(util.SAFE_TIMESTAMP_FORMAT))
}
// See documentation for db.CommentDB interface.
func (d *firestoreDB) PutCommitComment(c *types.CommitComment) error {
c.Timestamp = fixTimestamp(c.Timestamp)
id := commitCommentId(c)
_, err := d.client.Create(d.commitComments().Doc(id), c, DEFAULT_ATTEMPTS, PUT_SINGLE_TIMEOUT)
if st, ok := status.FromError(err); ok && st.Code() == codes.AlreadyExists {
return db.ErrAlreadyExists
}
if err != nil {
return err
}
d.TrackModifiedCommitComment(c)
return nil
}
// See documentation for db.CommentDB interface.
func (d *firestoreDB) DeleteCommitComment(c *types.CommitComment) error {
id := commitCommentId(c)
ref := d.commitComments().Doc(id)
exists := true
if _, err := d.client.Get(ref, DEFAULT_ATTEMPTS, GET_SINGLE_TIMEOUT); err != nil {
if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound {
exists = false
} else {
return err
}
}
if exists {
if _, err := d.client.Delete(d.commitComments().Doc(id), DEFAULT_ATTEMPTS, PUT_SINGLE_TIMEOUT); err != nil {
return err
}
deleted := true
c.Deleted = &deleted
d.TrackModifiedCommitComment(c)
}
return nil
}