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
}
|