blob: 00eeb78c17a5aa55c19f52427810434e7370e740 [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package botdata implements parsing and generation logic for BotData.
package botdata
import (
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"go.chromium.org/luci/common/errors"
gerritpb "go.chromium.org/luci/common/proto/gerrit"
"go.chromium.org/luci/cv/internal/gerrit"
)
// ActionType describes the type of the action taken by CV against a Gerrit
// Change.
type ActionType string
const (
// Start indicates that LUCI CV has started a Run for a Gerrit Change.
Start ActionType = "start"
// Cancel indicates that LUCI CV has cancelled a Run for Gerrit Change.
Cancel = "cancel"
)
const (
// botDataPrefix is a string that prepends to the BotData message.
botDataPrefix = "Bot data: "
// botDataSep separates non-empty human message and BotData message.
botDataSep = "\n\n"
)
// UnmarshalJSON sets `*at` to the `ActionType` the given bytes represents.
// Returns error for types other than `Start` or `Cancel`.
func (at *ActionType) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return errors.Annotate(err, "unmarshal action").Err()
}
switch t := ActionType(s); t {
case Start, Cancel:
*at = t
return nil
default:
return errors.Reason("invalid action: %s", t).Err()
}
}
// ChangeID is the unique identifier for a Gerrit Change.
type ChangeID struct {
// Host is the Gerrit host name for the Change.
Host string
// Number is the Gerrit Change number.
Number int64
}
// MarshalText encodes `c` in the from of "c.Host:c.Number".
func (c *ChangeID) MarshalText() ([]byte, error) {
return []byte(fmt.Sprintf("%s:%d", c.Host, c.Number)), nil
}
// UnmarshalText decodes the bytes and sets `*c` to the resurrected `ChangeID`.
func (c *ChangeID) UnmarshalText(b []byte) error {
segs := strings.Split(string(b), ":")
if len(segs) != 2 {
return errors.Reason("too many separators ':' in ChangeID text: %s", string(b)).Err()
}
num, err := strconv.ParseInt(segs[1], 10, 64)
if err != nil {
return errors.Annotate(err, "invalid Change number: %s", segs[1]).Err()
}
c.Host, c.Number = segs[0], num
return nil
}
// BotData records an action taken by CV against a Gerrit Change.
type BotData struct {
// Action describes the action taken by CV against a Gerrit Change.
Action ActionType `json:"action"`
// TriggeredAt is the timestamp when this action is triggered.
TriggeredAt time.Time `json:"triggered_at"`
// Revision is the revision (patch set) of the change at the time this
// action is triggered.
Revision string `json:"revision"`
}
// Parse tries to extract BotData from the given Gerrit Change message.
// Returns ok=false if no valid BotData is found in the message.
func Parse(cmi *gerritpb.ChangeMessageInfo) (ret BotData, ok bool) {
if cmi == nil {
return
}
idx := strings.Index(cmi.Message, botDataPrefix)
if idx < 0 {
return
}
if err := json.Unmarshal([]byte(cmi.Message[idx+len(botDataPrefix):]), &ret); err == nil {
ok = true
}
return
}
// Append appends BotData message (in JSON form) to the given human message.
// The human message will be truncated if the length of result message exceeds
// `MaxMessageLength`. Returns error if marshalling BotData to JSON fails or
// the BotData message itself is already too long.
func Append(humanMsg string, bd BotData) (string, error) {
return append(humanMsg, bd, gerrit.MaxMessageLength)
}
func append(humanMsg string, bd BotData, maxLen int) (string, error) {
b, err := json.Marshal(bd)
if err != nil {
return "", errors.Annotate(err, "marshal bot data").Err()
}
buf := bytes.NewBufferString(humanMsg)
switch hmLen, sepLen, bdLen := len(humanMsg), len(botDataSep), len(botDataPrefix)+len(b); {
case bdLen > maxLen:
return "", errors.Reason("bot data too long; max length: %d, got %d", maxLen, bdLen).Err()
case hmLen == 0:
buf.Grow(bdLen)
writeBotData(buf, b, false)
case hmLen+sepLen+bdLen <= maxLen:
buf.Grow(sepLen + bdLen)
writeBotData(buf, b, true)
default:
keepLen := maxLen - bdLen - sepLen - len(gerrit.PlaceHolder)
if keepLen <= 0 {
return "", errors.Reason("bot data too long to display human message; max length: %d, got %d", maxLen, bdLen).Err()
}
buf.Truncate(keepLen)
buf.Grow(maxLen - keepLen)
buf.WriteString(gerrit.PlaceHolder)
writeBotData(buf, b, true)
}
return buf.String(), nil
}
func writeBotData(buf *bytes.Buffer, bd []byte, addSep bool) {
if addSep {
buf.WriteString(botDataSep)
}
buf.WriteString(botDataPrefix)
buf.Write(bd)
}