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