File: fetch_scanner.go

package info (click to toggle)
gitlab-shell 14.35.0%2Bds1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 23,652 kB
  • sloc: ruby: 1,129; makefile: 583; sql: 391; sh: 384
file content (191 lines) | stat: -rw-r--r-- 6,284 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
package git

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

// RefUpdateType represents the type of update a FetchStatusLine is. The
// valid types are documented here: https://git-scm.com/docs/git-fetch/2.30.0#Documentation/git-fetch.txt-flag
type RefUpdateType byte

// Valid checks whether the RefUpdateType is one of the seven valid types of update
func (t RefUpdateType) Valid() bool {
	_, ok := validRefUpdateTypes[t]

	return ok
}

// FetchStatusLine represents a line of status output from `git fetch`, as
// documented here: https://git-scm.com/docs/git-fetch/2.30.0#_output. Each
// line is a change to a git reference in the local repository that was caused
// by the fetch
type FetchStatusLine struct {
	// Type encodes the kind of change that git fetch has made
	Type RefUpdateType
	// Summary is a brief description of the change. This may be text such as
	// [new tag], or a compact-form SHA range showing the old and new values of
	// the updated reference, depending on the type of update
	Summary string
	// From is usually the name of the remote ref being fetched from, missing
	// the refs/<type>/ prefix. If a ref is being deleted, this will be "(none)"
	From string
	// To is the name of the local ref being updated, missing the refs/<type>/
	// prefix.
	To string
	// Reason optionally contains human-readable information about the change. It
	// is typically used to explain why making a given change failed (e.g., the
	// type will be RefUpdateTypeUpdateFailed). It may be empty.
	Reason string
}

const (
	// RefUpdateTypeFastForwardUpdate represents a 'fast forward update' fetch status line
	RefUpdateTypeFastForwardUpdate RefUpdateType = ' '
	// RefUpdateTypeForcedUpdate represents a 'forced update' fetch status line
	RefUpdateTypeForcedUpdate RefUpdateType = '+'
	// RefUpdateTypePruned represents a 'pruned' fetch status line
	RefUpdateTypePruned RefUpdateType = '-'
	// RefUpdateTypeTagUpdate represents a 'tag update' fetch status line
	RefUpdateTypeTagUpdate RefUpdateType = 't'
	// RefUpdateTypeFetched represents a 'fetched' fetch status line. This
	// indicates that a new reference has been created in the local repository
	RefUpdateTypeFetched RefUpdateType = '*'
	// RefUpdateTypeUpdateFailed represents an 'update failed' fetch status line
	RefUpdateTypeUpdateFailed RefUpdateType = '!'
	// RefUpdateTypeUnchanged represents an 'unchanged' fetch status line
	RefUpdateTypeUnchanged RefUpdateType = '='
)

var validRefUpdateTypes = map[RefUpdateType]struct{}{
	RefUpdateTypeFastForwardUpdate: {},
	RefUpdateTypeForcedUpdate:      {},
	RefUpdateTypePruned:            {},
	RefUpdateTypeTagUpdate:         {},
	RefUpdateTypeFetched:           {},
	RefUpdateTypeUpdateFailed:      {},
	RefUpdateTypeUnchanged:         {},
}

// IsTagAdded returns true if this status line indicates a new tag was added
func (f FetchStatusLine) IsTagAdded() bool {
	return f.Type == RefUpdateTypeFetched && f.Summary == "[new tag]"
}

// IsTagUpdated returns true if this status line indicates a tag was changed
func (f FetchStatusLine) IsTagUpdated() bool {
	return f.Type == RefUpdateTypeTagUpdate
}

// FetchScanner scans the output of `git fetch`, allowing information about
// the updated refs to be gathered
type FetchScanner struct {
	scanner  *bufio.Scanner
	lastLine FetchStatusLine
}

// NewFetchScanner returns a new FetchScanner
func NewFetchScanner(r io.Reader) *FetchScanner {
	return &FetchScanner{scanner: bufio.NewScanner(r)}
}

// Scan looks for the next fetch status line in the reader supplied to
// NewFetchScanner(). Any lines that are not valid status lines are discarded
// without error. It returns true if you should call Scan() again, and false if
// scanning has come to an end.
func (f *FetchScanner) Scan() bool {
	for f.scanner.Scan() {
		// Silently ignore non-matching lines
		line, ok := parseFetchStatusLine(f.scanner.Bytes())
		if !ok {
			continue
		}

		f.lastLine = line
		return true
	}

	return false
}

// Err returns any error encountered while scanning the reader supplied to
// NewFetchScanner(). Note that lines not matching the expected format are not
// an error.
func (f *FetchScanner) Err() error {
	return f.scanner.Err()
}

// StatusLine returns the most recent fetch status line encountered by the
// FetchScanner. It changes after each call to Scan(), unless there is an error.
func (f *FetchScanner) StatusLine() FetchStatusLine {
	return f.lastLine
}

// parseFetchStatusLine parses lines outputted by git-fetch(1), which are expected
// to be in the format " <flag> <summary> <from> -> <to> [<reason>]"
func parseFetchStatusLine(line []byte) (FetchStatusLine, bool) {
	var blank FetchStatusLine
	var out FetchStatusLine

	// Handle the flag very strictly, since status and non-status text mingle
	if len(line) < 4 || line[0] != ' ' || line[2] != ' ' {
		return blank, false
	}

	out.Type, line = RefUpdateType(line[1]), line[3:]
	if !out.Type.Valid() {
		return blank, false
	}

	// Get the summary, which may be composed of multiple words
	if line[0] == '[' {
		end := bytes.IndexByte(line, ']')
		if end < 0 || len(line) <= end+2 {
			return blank, false
		}

		out.Summary, line = string(line[0:end+1]), line[end+1:]
	} else {
		end := bytes.IndexByte(line, ' ')
		if end < 0 || len(line) <= end+1 {
			return blank, false
		}

		out.Summary, line = string(line[0:end]), line[end:]
	}

	// We're now scanning the "<from> -> <to>" part, where "<from>" is the remote branch name
	// while "<to>" is the local branch name transformed by the refspec. As branches cannot
	// contain whitespace, it's fine to scan by word now.
	scanner := bufio.NewScanner(bytes.NewReader(line))
	scanner.Split(bufio.ScanWords)

	// From field
	if !scanner.Scan() {
		return blank, false
	}
	out.From = scanner.Text()

	// Hardcoded -> delimiter
	if !scanner.Scan() || !bytes.Equal(scanner.Bytes(), []byte("->")) {
		return blank, false
	}

	// To field
	if !scanner.Scan() {
		return blank, false
	}
	out.To = scanner.Text()

	// Reason field - optional, the rest of the line. This implementation will
	// squeeze multiple spaces into one, but that shouldn't be a big problem
	var reason []string
	for scanner.Scan() {
		reason = append(reason, scanner.Text())
	}
	out.Reason = strings.Join(reason, " ")

	return out, true
}