gitea

Development moved to Codeberg

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
  42. 42
  43. 43
  44. 44
  45. 45
  46. 46
  47. 47
  48. 48
  49. 49
  50. 50
  51. 51
  52. 52
  53. 53
  54. 54
  55. 55
  56. 56
  57. 57
  58. 58
  59. 59
  60. 60
  61. 61
  62. 62
  63. 63
  64. 64
  65. 65
  66. 66
  67. 67
  68. 68
  69. 69
  70. 70
  71. 71
  72. 72
  73. 73
  74. 74
  75. 75
  76. 76
  77. 77
  78. 78
  79. 79
  80. 80
  81. 81
  82. 82
  83. 83
  84. 84
  85. 85
  86. 86
  87. 87
  88. 88
  89. 89
  90. 90
  91. 91
  92. 92
  93. 93
  94. 94
  95. 95
  96. 96
  97. 97
  98. 98
  99. 99
  100. 100
  101. 101
  102. 102
  103. 103
  104. 104
  105. 105
  106. 106
  107. 107
  108. 108
  109. 109
  110. 110
  111. 111
  112. 112
  113. 113
  114. 114
  115. 115
  116. 116
  117. 117
  118. 118
  119. 119
  120. 120
  121. 121
  122. 122
  123. 123
  124. 124
  125. 125
  126. 126
  127. 127
  128. 128
  129. 129
// Copyright 2022 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package foreachref

import (
	"bufio"
	"bytes"
	"fmt"
	"io"
	"strings"
)

// Parser parses 'git for-each-ref' output according to a given output Format.
type Parser struct {
	//  tokenizes 'git for-each-ref' output into "reference paragraphs".
	scanner *bufio.Scanner

	// format represents the '--format' string that describes the expected
	// 'git for-each-ref' output structure.
	format Format

	// err holds the last encountered error during parsing.
	err error
}

// NewParser creates a 'git for-each-ref' output parser that will parse all
// references in the provided Reader. The references in the output are assumed
// to follow the specified Format.
func NewParser(r io.Reader, format Format) *Parser {
	scanner := bufio.NewScanner(r)

	// in addition to the reference delimiter we specified in the --format,
	// `git for-each-ref` will always add a newline after every reference.
	refDelim := make([]byte, 0, len(format.refDelim)+1)
	refDelim = append(refDelim, format.refDelim...)
	refDelim = append(refDelim, '\n')

	// Split input into delimiter-separated "reference blocks".
	scanner.Split(
		func(data []byte, atEOF bool) (advance int, token []byte, err error) {
			// Scan until delimiter, marking end of reference.
			delimIdx := bytes.Index(data, refDelim)
			if delimIdx >= 0 {
				token := data[:delimIdx]
				advance := delimIdx + len(refDelim)
				return advance, token, nil
			}
			// If we're at EOF, we have a final, non-terminated reference. Return it.
			if atEOF {
				return len(data), data, nil
			}
			// Not yet a full field. Request more data.
			return 0, nil, nil
		})

	return &Parser{
		scanner: scanner,
		format:  format,
		err:     nil,
	}
}

// Next returns the next reference as a collection of key-value pairs. nil
// denotes EOF but is also returned on errors. The Err method should always be
// consulted after Next returning nil.
//
// It could, for example return something like:
//
//	{ "objecttype": "tag", "refname:short": "v1.16.4", "object": "f460b7543ed500e49c133c2cd85c8c55ee9dbe27" }
func (p *Parser) Next() map[string]string {
	if !p.scanner.Scan() {
		return nil
	}
	fields, err := p.parseRef(p.scanner.Text())
	if err != nil {
		p.err = err
		return nil
	}
	return fields
}

// Err returns the latest encountered parsing error.
func (p *Parser) Err() error {
	return p.err
}

// parseRef parses out all key-value pairs from a single reference block, such as
//
//	"objecttype tag\0refname:short v1.16.4\0object f460b7543ed500e49c133c2cd85c8c55ee9dbe27"
func (p *Parser) parseRef(refBlock string) (map[string]string, error) {
	if refBlock == "" {
		// must be at EOF
		return nil, nil
	}

	fieldValues := make(map[string]string)

	fields := strings.Split(refBlock, p.format.fieldDelimStr)
	if len(fields) != len(p.format.fieldNames) {
		return nil, fmt.Errorf("unexpected number of reference fields: wanted %d, was %d",
			len(fields), len(p.format.fieldNames))
	}
	for i, field := range fields {
		field = strings.TrimSpace(field)

		var fieldKey string
		var fieldVal string
		firstSpace := strings.Index(field, " ")
		if firstSpace > 0 {
			fieldKey = field[:firstSpace]
			fieldVal = field[firstSpace+1:]
		} else {
			// could be the case if the requested field had no value
			fieldKey = field
		}

		// enforce the format order of fields
		if p.format.fieldNames[i] != fieldKey {
			return nil, fmt.Errorf("unexpected field name at position %d: wanted: '%s', was: '%s'",
				i, p.format.fieldNames[i], fieldKey)
		}

		fieldValues[fieldKey] = fieldVal
	}

	return fieldValues, nil
}