blob: 8d6587495088e3493034d94b3d5eec6826b235be [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gerrit
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"go.fuchsia.dev/jiri/collect"
)
// The functions in this file are provided to support writing a presubmit
// system that queries Gerrit for new changes and does <something> with them.
// ReadLog returns a map of CLs indexed by their refs, read from the given log file.
func ReadLog(logFilePath string) (CLRefMap, error) {
results := CLRefMap{}
bytes, err := ioutil.ReadFile(logFilePath)
if err != nil {
// File not existing is OK: just return an empty map of CLs.
if os.IsNotExist(err) {
return results, nil
}
return nil, fmt.Errorf("ReadFile(%q) failed: %v", logFilePath, err)
}
if err := json.Unmarshal(bytes, &results); err != nil {
return nil, fmt.Errorf("Unmarshal failed reading file %q: %v", logFilePath, err)
}
return results, nil
}
// WriteLog writes the given list of CLs to a log file, as a json-encoded
// map of ref strings => CLs.
func WriteLog(logFilePath string, cls CLList) (e error) {
// Index CLs with their refs.
results := CLRefMap{}
for _, cl := range cls {
results[cl.Reference()] = cl
}
fd, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
if err != nil {
return fmt.Errorf("OpenFile(%q) failed: %v", logFilePath, err)
}
defer collect.Error(func() error { return fd.Close() }, &e)
bytes, err := json.MarshalIndent(results, "", " ")
if err != nil {
return fmt.Errorf("MarshalIndent(%v) failed: %v", results, err)
}
if err := ioutil.WriteFile(logFilePath, bytes, os.FileMode(0644)); err != nil {
return fmt.Errorf("WriteFile(%q) failed: %v", logFilePath, err)
}
return nil
}
// NewOpenCLs returns a slice of CLLists that are "newer" relative to the
// previous query. A CLList is newer if one of the following condition holds:
// - If a CLList has only one cl, then it is newer if:
// * Its ref string cannot be found among the CLs from the previous query.
//
// For example: from the previous query, we got cl 1000/1 (cl number 1000 and
// patchset 1). Then CLLists [1000/2] and [2000/1] are both newer.
//
// - If a CLList has multiple CLs, then it is newer if:
// * It forms a "consistent" (its CLs have the same topic) and "complete"
// (it contains all the parts) multi-part CL set.
// * At least one of their ref strings cannot be found in the CLs from the
// previous query.
//
// For example: from the previous query, we got cl 3001/1 which is the first
// part of a multi part cl set with topic "T1". Suppose the current query
// returns cl 3002/1 which is the second part of the same set. In this case,
// a CLList [3001/1 3002/1] will be returned. Then suppose in the next query,
// we got cl 3002/2 which is newer then 3002/1. In this case, a CLList
// [3001/1 3002/2] will be returned.
func NewOpenCLs(prevCLsMap CLRefMap, curCLs CLList) ([]CLList, []error) {
retNewCLs := []CLList{}
topicsInNewCLs := map[string]bool{}
multiPartCLs := CLList{}
for _, curCL := range curCLs {
// Ref could be empty in cases where a patchset is causing conflicts.
if curCL.Reference() == "" {
continue
}
if _, ok := prevCLsMap[curCL.Reference()]; !ok { // This individual cl is newer.
if curCL.MultiPart == nil {
// This cl is not a multi part cl; add it to the return slice.
retNewCLs = append(retNewCLs, CLList{curCL})
} else {
// This cl is a multi part cl; record its topic and process it later.
topicsInNewCLs[curCL.MultiPart.Topic] = true
}
}
// Record all multi part CLs.
if curCL.MultiPart != nil {
multiPartCLs = append(multiPartCLs, curCL)
}
}
// Find complete multi part CL sets.
setMap := map[string]*MultiPartCLSet{}
retErrors := []error{}
for _, curCL := range multiPartCLs {
multiPartInfo := curCL.MultiPart
// Skip topics that contain no new CLs.
topic := multiPartInfo.Topic
if !topicsInNewCLs[topic] {
continue
}
// Golang equivalent of defaultdict...
if _, ok := setMap[topic]; !ok {
setMap[topic] = NewMultiPartCLSet()
}
curSet := setMap[topic]
if err := curSet.AddCL(curCL); err != nil {
// Errors adding multi part CLs aren't fatal since we want to keep processing
// the rest of the CLs. Return a list to the caller for probably just logging.
retErrors = append(retErrors, NewChangeError(curCL, err))
}
}
for _, set := range setMap {
if set.Complete() {
retNewCLs = append(retNewCLs, set.CLs())
}
}
return retNewCLs, retErrors
}