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  }