v.io/jiri@v0.0.0-20160715023856-abfb8b131290/gerrit/presubmit.go (about) 1 // Copyright 2016 The Vanadium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package gerrit 6 7 import ( 8 "encoding/json" 9 "fmt" 10 "io/ioutil" 11 "os" 12 13 "v.io/jiri/collect" 14 ) 15 16 // The functions in this file are provided to support writing a presubmit 17 // system that queries Gerrit for new changes and does <something> with them. 18 19 // ReadLog returns a map of CLs indexed by their refs, read from the given log file. 20 func ReadLog(logFilePath string) (CLRefMap, error) { 21 results := CLRefMap{} 22 bytes, err := ioutil.ReadFile(logFilePath) 23 if err != nil { 24 // File not existing is OK: just return an empty map of CLs. 25 if os.IsNotExist(err) { 26 return results, nil 27 } 28 return nil, fmt.Errorf("ReadFile(%q) failed: %v", logFilePath, err) 29 } 30 31 if err := json.Unmarshal(bytes, &results); err != nil { 32 return nil, fmt.Errorf("Unmarshal failed reading file %q: %v", logFilePath, err) 33 } 34 return results, nil 35 } 36 37 // WriteLog writes the given list of CLs to a log file, as a json-encoded 38 // map of ref strings => CLs. 39 func WriteLog(logFilePath string, cls CLList) (e error) { 40 // Index CLs with their refs. 41 results := CLRefMap{} 42 for _, cl := range cls { 43 results[cl.Reference()] = cl 44 } 45 46 fd, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) 47 if err != nil { 48 return fmt.Errorf("OpenFile(%q) failed: %v", logFilePath, err) 49 } 50 defer collect.Error(func() error { return fd.Close() }, &e) 51 52 bytes, err := json.MarshalIndent(results, "", " ") 53 if err != nil { 54 return fmt.Errorf("MarshalIndent(%v) failed: %v", results, err) 55 } 56 57 if err := ioutil.WriteFile(logFilePath, bytes, os.FileMode(0644)); err != nil { 58 return fmt.Errorf("WriteFile(%q) failed: %v", logFilePath, err) 59 } 60 return nil 61 } 62 63 // NewOpenCLs returns a slice of CLLists that are "newer" relative to the 64 // previous query. A CLList is newer if one of the following condition holds: 65 // - If a CLList has only one cl, then it is newer if: 66 // * Its ref string cannot be found among the CLs from the previous query. 67 // 68 // For example: from the previous query, we got cl 1000/1 (cl number 1000 and 69 // patchset 1). Then CLLists [1000/2] and [2000/1] are both newer. 70 // 71 // - If a CLList has multiple CLs, then it is newer if: 72 // * It forms a "consistent" (its CLs have the same topic) and "complete" 73 // (it contains all the parts) multi-part CL set. 74 // * At least one of their ref strings cannot be found in the CLs from the 75 // previous query. 76 // 77 // For example: from the previous query, we got cl 3001/1 which is the first 78 // part of a multi part cl set with topic "T1". Suppose the current query 79 // returns cl 3002/1 which is the second part of the same set. In this case, 80 // a CLList [3001/1 3002/1] will be returned. Then suppose in the next query, 81 // we got cl 3002/2 which is newer then 3002/1. In this case, a CLList 82 // [3001/1 3002/2] will be returned. 83 func NewOpenCLs(prevCLsMap CLRefMap, curCLs CLList) ([]CLList, []error) { 84 retNewCLs := []CLList{} 85 topicsInNewCLs := map[string]bool{} 86 multiPartCLs := CLList{} 87 for _, curCL := range curCLs { 88 // Ref could be empty in cases where a patchset is causing conflicts. 89 if curCL.Reference() == "" { 90 continue 91 } 92 if _, ok := prevCLsMap[curCL.Reference()]; !ok { // This individual cl is newer. 93 if curCL.MultiPart == nil { 94 // This cl is not a multi part cl; add it to the return slice. 95 retNewCLs = append(retNewCLs, CLList{curCL}) 96 } else { 97 // This cl is a multi part cl; record its topic and process it later. 98 topicsInNewCLs[curCL.MultiPart.Topic] = true 99 } 100 } 101 // Record all multi part CLs. 102 if curCL.MultiPart != nil { 103 multiPartCLs = append(multiPartCLs, curCL) 104 } 105 } 106 107 // Find complete multi part CL sets. 108 setMap := map[string]*MultiPartCLSet{} 109 retErrors := []error{} 110 for _, curCL := range multiPartCLs { 111 multiPartInfo := curCL.MultiPart 112 113 // Skip topics that contain no new CLs. 114 topic := multiPartInfo.Topic 115 if !topicsInNewCLs[topic] { 116 continue 117 } 118 119 // Golang equivalent of defaultdict... 120 if _, ok := setMap[topic]; !ok { 121 setMap[topic] = NewMultiPartCLSet() 122 } 123 124 curSet := setMap[topic] 125 if err := curSet.AddCL(curCL); err != nil { 126 // Errors adding multi part CLs aren't fatal since we want to keep processing 127 // the rest of the CLs. Return a list to the caller for probably just logging. 128 retErrors = append(retErrors, NewChangeError(curCL, err)) 129 } 130 } 131 for _, set := range setMap { 132 if set.Complete() { 133 retNewCLs = append(retNewCLs, set.CLs()) 134 } 135 } 136 137 return retNewCLs, retErrors 138 }