code.gitea.io/gitea@v1.19.3/modules/git/foreachref/parser.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package foreachref 5 6 import ( 7 "bufio" 8 "bytes" 9 "fmt" 10 "io" 11 "strings" 12 ) 13 14 // Parser parses 'git for-each-ref' output according to a given output Format. 15 type Parser struct { 16 // tokenizes 'git for-each-ref' output into "reference paragraphs". 17 scanner *bufio.Scanner 18 19 // format represents the '--format' string that describes the expected 20 // 'git for-each-ref' output structure. 21 format Format 22 23 // err holds the last encountered error during parsing. 24 err error 25 } 26 27 // NewParser creates a 'git for-each-ref' output parser that will parse all 28 // references in the provided Reader. The references in the output are assumed 29 // to follow the specified Format. 30 func NewParser(r io.Reader, format Format) *Parser { 31 scanner := bufio.NewScanner(r) 32 33 // in addition to the reference delimiter we specified in the --format, 34 // `git for-each-ref` will always add a newline after every reference. 35 refDelim := make([]byte, 0, len(format.refDelim)+1) 36 refDelim = append(refDelim, format.refDelim...) 37 refDelim = append(refDelim, '\n') 38 39 // Split input into delimiter-separated "reference blocks". 40 scanner.Split( 41 func(data []byte, atEOF bool) (advance int, token []byte, err error) { 42 // Scan until delimiter, marking end of reference. 43 delimIdx := bytes.Index(data, refDelim) 44 if delimIdx >= 0 { 45 token := data[:delimIdx] 46 advance := delimIdx + len(refDelim) 47 return advance, token, nil 48 } 49 // If we're at EOF, we have a final, non-terminated reference. Return it. 50 if atEOF { 51 return len(data), data, nil 52 } 53 // Not yet a full field. Request more data. 54 return 0, nil, nil 55 }) 56 57 return &Parser{ 58 scanner: scanner, 59 format: format, 60 err: nil, 61 } 62 } 63 64 // Next returns the next reference as a collection of key-value pairs. nil 65 // denotes EOF but is also returned on errors. The Err method should always be 66 // consulted after Next returning nil. 67 // 68 // It could, for example return something like: 69 // 70 // { "objecttype": "tag", "refname:short": "v1.16.4", "object": "f460b7543ed500e49c133c2cd85c8c55ee9dbe27" } 71 func (p *Parser) Next() map[string]string { 72 if !p.scanner.Scan() { 73 return nil 74 } 75 fields, err := p.parseRef(p.scanner.Text()) 76 if err != nil { 77 p.err = err 78 return nil 79 } 80 return fields 81 } 82 83 // Err returns the latest encountered parsing error. 84 func (p *Parser) Err() error { 85 return p.err 86 } 87 88 // parseRef parses out all key-value pairs from a single reference block, such as 89 // 90 // "objecttype tag\0refname:short v1.16.4\0object f460b7543ed500e49c133c2cd85c8c55ee9dbe27" 91 func (p *Parser) parseRef(refBlock string) (map[string]string, error) { 92 if refBlock == "" { 93 // must be at EOF 94 return nil, nil 95 } 96 97 fieldValues := make(map[string]string) 98 99 fields := strings.Split(refBlock, p.format.fieldDelimStr) 100 if len(fields) != len(p.format.fieldNames) { 101 return nil, fmt.Errorf("unexpected number of reference fields: wanted %d, was %d", 102 len(fields), len(p.format.fieldNames)) 103 } 104 for i, field := range fields { 105 field = strings.TrimSpace(field) 106 107 var fieldKey string 108 var fieldVal string 109 firstSpace := strings.Index(field, " ") 110 if firstSpace > 0 { 111 fieldKey = field[:firstSpace] 112 fieldVal = field[firstSpace+1:] 113 } else { 114 // could be the case if the requested field had no value 115 fieldKey = field 116 } 117 118 // enforce the format order of fields 119 if p.format.fieldNames[i] != fieldKey { 120 return nil, fmt.Errorf("unexpected field name at position %d: wanted: '%s', was: '%s'", 121 i, p.format.fieldNames[i], fieldKey) 122 } 123 124 fieldValues[fieldKey] = fieldVal 125 } 126 127 return fieldValues, nil 128 }