code.gitea.io/gitea@v1.19.3/modules/gitgraph/parser.go (about) 1 // Copyright 2020 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package gitgraph 5 6 import ( 7 "bytes" 8 "fmt" 9 ) 10 11 // Parser represents a git graph parser. It is stateful containing the previous 12 // glyphs, detected flows and color assignments. 13 type Parser struct { 14 glyphs []byte 15 oldGlyphs []byte 16 flows []int64 17 oldFlows []int64 18 maxFlow int64 19 colors []int 20 oldColors []int 21 availableColors []int 22 nextAvailable int 23 firstInUse int 24 firstAvailable int 25 maxAllowedColors int 26 } 27 28 // Reset resets the internal parser state. 29 func (parser *Parser) Reset() { 30 parser.glyphs = parser.glyphs[0:0] 31 parser.oldGlyphs = parser.oldGlyphs[0:0] 32 parser.flows = parser.flows[0:0] 33 parser.oldFlows = parser.oldFlows[0:0] 34 parser.maxFlow = 0 35 parser.colors = parser.colors[0:0] 36 parser.oldColors = parser.oldColors[0:0] 37 parser.availableColors = parser.availableColors[0:0] 38 parser.availableColors = append(parser.availableColors, 1, 2) 39 parser.nextAvailable = 0 40 parser.firstInUse = -1 41 parser.firstAvailable = 0 42 parser.maxAllowedColors = 0 43 } 44 45 // AddLineToGraph adds the line as a row to the graph 46 func (parser *Parser) AddLineToGraph(graph *Graph, row int, line []byte) error { 47 idx := bytes.Index(line, []byte("DATA:")) 48 if idx < 0 { 49 parser.ParseGlyphs(line) 50 } else { 51 parser.ParseGlyphs(line[:idx]) 52 } 53 54 var err error 55 commitDone := false 56 57 for column, glyph := range parser.glyphs { 58 if glyph == ' ' { 59 continue 60 } 61 62 flowID := parser.flows[column] 63 64 graph.AddGlyph(row, column, flowID, parser.colors[column], glyph) 65 66 if glyph == '*' { 67 if commitDone { 68 if err != nil { 69 err = fmt.Errorf("double commit on line %d: %s. %w", row, string(line), err) 70 } else { 71 err = fmt.Errorf("double commit on line %d: %s", row, string(line)) 72 } 73 } 74 commitDone = true 75 if idx < 0 { 76 if err != nil { 77 err = fmt.Errorf("missing data section on line %d with commit: %s. %w", row, string(line), err) 78 } else { 79 err = fmt.Errorf("missing data section on line %d with commit: %s", row, string(line)) 80 } 81 continue 82 } 83 err2 := graph.AddCommit(row, column, flowID, line[idx+5:]) 84 if err != nil && err2 != nil { 85 err = fmt.Errorf("%v %w", err2, err) 86 continue 87 } else if err2 != nil { 88 err = err2 89 continue 90 } 91 } 92 } 93 if !commitDone { 94 graph.Commits = append(graph.Commits, RelationCommit) 95 } 96 return err 97 } 98 99 func (parser *Parser) releaseUnusedColors() { 100 if parser.firstInUse > -1 { 101 // Here we step through the old colors, searching for them in the 102 // "in-use" section of availableColors (that is, the colors between 103 // firstInUse and firstAvailable) 104 // Ensure that the benchmarks are not worsened with proposed changes 105 stepstaken := 0 106 position := parser.firstInUse 107 for _, color := range parser.oldColors { 108 if color == 0 { 109 continue 110 } 111 found := false 112 i := position 113 for j := stepstaken; i != parser.firstAvailable && j < len(parser.availableColors); j++ { 114 colorToCheck := parser.availableColors[i] 115 if colorToCheck == color { 116 found = true 117 break 118 } 119 i = (i + 1) % len(parser.availableColors) 120 } 121 if !found { 122 // Duplicate color 123 continue 124 } 125 // Swap them around 126 parser.availableColors[position], parser.availableColors[i] = parser.availableColors[i], parser.availableColors[position] 127 stepstaken++ 128 position = (parser.firstInUse + stepstaken) % len(parser.availableColors) 129 if position == parser.firstAvailable || stepstaken == len(parser.availableColors) { 130 break 131 } 132 } 133 if stepstaken == len(parser.availableColors) { 134 parser.firstAvailable = -1 135 } else { 136 parser.firstAvailable = position 137 if parser.nextAvailable == -1 { 138 parser.nextAvailable = parser.firstAvailable 139 } 140 } 141 } 142 } 143 144 // ParseGlyphs parses the provided glyphs and sets the internal state 145 func (parser *Parser) ParseGlyphs(glyphs []byte) { 146 // Clean state for parsing this row 147 parser.glyphs, parser.oldGlyphs = parser.oldGlyphs, parser.glyphs 148 parser.glyphs = parser.glyphs[0:0] 149 parser.flows, parser.oldFlows = parser.oldFlows, parser.flows 150 parser.flows = parser.flows[0:0] 151 parser.colors, parser.oldColors = parser.oldColors, parser.colors 152 153 // Ensure we have enough flows and colors 154 parser.colors = parser.colors[0:0] 155 for range glyphs { 156 parser.flows = append(parser.flows, 0) 157 parser.colors = append(parser.colors, 0) 158 } 159 160 // Copy the provided glyphs in to state.glyphs for safekeeping 161 parser.glyphs = append(parser.glyphs, glyphs...) 162 163 // release unused colors 164 parser.releaseUnusedColors() 165 166 for i := len(glyphs) - 1; i >= 0; i-- { 167 glyph := glyphs[i] 168 switch glyph { 169 case '|': 170 fallthrough 171 case '*': 172 parser.setUpFlow(i) 173 case '/': 174 parser.setOutFlow(i) 175 case '\\': 176 parser.setInFlow(i) 177 case '_': 178 parser.setRightFlow(i) 179 case '.': 180 fallthrough 181 case '-': 182 parser.setLeftFlow(i) 183 case ' ': 184 // no-op 185 default: 186 parser.newFlow(i) 187 } 188 } 189 } 190 191 func (parser *Parser) takePreviousFlow(i, j int) { 192 if j < len(parser.oldFlows) && parser.oldFlows[j] > 0 { 193 parser.flows[i] = parser.oldFlows[j] 194 parser.oldFlows[j] = 0 195 parser.colors[i] = parser.oldColors[j] 196 parser.oldColors[j] = 0 197 } else { 198 parser.newFlow(i) 199 } 200 } 201 202 func (parser *Parser) takeCurrentFlow(i, j int) { 203 if j < len(parser.flows) && parser.flows[j] > 0 { 204 parser.flows[i] = parser.flows[j] 205 parser.colors[i] = parser.colors[j] 206 } else { 207 parser.newFlow(i) 208 } 209 } 210 211 func (parser *Parser) newFlow(i int) { 212 parser.maxFlow++ 213 parser.flows[i] = parser.maxFlow 214 215 // Now give this flow a color 216 if parser.nextAvailable == -1 { 217 next := len(parser.availableColors) 218 if parser.maxAllowedColors < 1 || next < parser.maxAllowedColors { 219 parser.nextAvailable = next 220 parser.firstAvailable = next 221 parser.availableColors = append(parser.availableColors, next+1) 222 } 223 } 224 parser.colors[i] = parser.availableColors[parser.nextAvailable] 225 if parser.firstInUse == -1 { 226 parser.firstInUse = parser.nextAvailable 227 } 228 parser.availableColors[parser.firstAvailable], parser.availableColors[parser.nextAvailable] = parser.availableColors[parser.nextAvailable], parser.availableColors[parser.firstAvailable] 229 230 parser.nextAvailable = (parser.nextAvailable + 1) % len(parser.availableColors) 231 parser.firstAvailable = (parser.firstAvailable + 1) % len(parser.availableColors) 232 233 if parser.nextAvailable == parser.firstInUse { 234 parser.nextAvailable = parser.firstAvailable 235 } 236 if parser.nextAvailable == parser.firstInUse { 237 parser.nextAvailable = -1 238 parser.firstAvailable = -1 239 } 240 } 241 242 // setUpFlow handles '|' or '*' 243 func (parser *Parser) setUpFlow(i int) { 244 // In preference order: 245 // 246 // Previous Row: '\? ' ' |' ' /' 247 // Current Row: ' | ' ' |' ' | ' 248 if i > 0 && i-1 < len(parser.oldGlyphs) && parser.oldGlyphs[i-1] == '\\' { 249 parser.takePreviousFlow(i, i-1) 250 } else if i < len(parser.oldGlyphs) && (parser.oldGlyphs[i] == '|' || parser.oldGlyphs[i] == '*') { 251 parser.takePreviousFlow(i, i) 252 } else if i+1 < len(parser.oldGlyphs) && parser.oldGlyphs[i+1] == '/' { 253 parser.takePreviousFlow(i, i+1) 254 } else { 255 parser.newFlow(i) 256 } 257 } 258 259 // setOutFlow handles '/' 260 func (parser *Parser) setOutFlow(i int) { 261 // In preference order: 262 // 263 // Previous Row: ' |/' ' |_' ' |' ' /' ' _' '\' 264 // Current Row: '/| ' '/| ' '/ ' '/ ' '/ ' '/' 265 if i+2 < len(parser.oldGlyphs) && 266 (parser.oldGlyphs[i+1] == '|' || parser.oldGlyphs[i+1] == '*') && 267 (parser.oldGlyphs[i+2] == '/' || parser.oldGlyphs[i+2] == '_') && 268 i+1 < len(parser.glyphs) && 269 (parser.glyphs[i+1] == '|' || parser.glyphs[i+1] == '*') { 270 parser.takePreviousFlow(i, i+2) 271 } else if i+1 < len(parser.oldGlyphs) && 272 (parser.oldGlyphs[i+1] == '|' || parser.oldGlyphs[i+1] == '*' || 273 parser.oldGlyphs[i+1] == '/' || parser.oldGlyphs[i+1] == '_') { 274 parser.takePreviousFlow(i, i+1) 275 if parser.oldGlyphs[i+1] == '/' { 276 parser.glyphs[i] = '|' 277 } 278 } else if i < len(parser.oldGlyphs) && parser.oldGlyphs[i] == '\\' { 279 parser.takePreviousFlow(i, i) 280 } else { 281 parser.newFlow(i) 282 } 283 } 284 285 // setInFlow handles '\' 286 func (parser *Parser) setInFlow(i int) { 287 // In preference order: 288 // 289 // Previous Row: '| ' '-. ' '| ' '\ ' '/' '---' 290 // Current Row: '|\' ' \' ' \' ' \' '\' ' \ ' 291 if i > 0 && i-1 < len(parser.oldGlyphs) && 292 (parser.oldGlyphs[i-1] == '|' || parser.oldGlyphs[i-1] == '*') && 293 (parser.glyphs[i-1] == '|' || parser.glyphs[i-1] == '*') { 294 parser.newFlow(i) 295 } else if i > 0 && i-1 < len(parser.oldGlyphs) && 296 (parser.oldGlyphs[i-1] == '|' || parser.oldGlyphs[i-1] == '*' || 297 parser.oldGlyphs[i-1] == '.' || parser.oldGlyphs[i-1] == '\\') { 298 parser.takePreviousFlow(i, i-1) 299 if parser.oldGlyphs[i-1] == '\\' { 300 parser.glyphs[i] = '|' 301 } 302 } else if i < len(parser.oldGlyphs) && parser.oldGlyphs[i] == '/' { 303 parser.takePreviousFlow(i, i) 304 } else { 305 parser.newFlow(i) 306 } 307 } 308 309 // setRightFlow handles '_' 310 func (parser *Parser) setRightFlow(i int) { 311 // In preference order: 312 // 313 // Current Row: '__' '_/' '_|_' '_|/' 314 if i+1 < len(parser.glyphs) && 315 (parser.glyphs[i+1] == '_' || parser.glyphs[i+1] == '/') { 316 parser.takeCurrentFlow(i, i+1) 317 } else if i+2 < len(parser.glyphs) && 318 (parser.glyphs[i+1] == '|' || parser.glyphs[i+1] == '*') && 319 (parser.glyphs[i+2] == '_' || parser.glyphs[i+2] == '/') { 320 parser.takeCurrentFlow(i, i+2) 321 } else { 322 parser.newFlow(i) 323 } 324 } 325 326 // setLeftFlow handles '----.' 327 func (parser *Parser) setLeftFlow(i int) { 328 if parser.glyphs[i] == '.' { 329 parser.newFlow(i) 330 } else if i+1 < len(parser.glyphs) && 331 (parser.glyphs[i+1] == '-' || parser.glyphs[i+1] == '.') { 332 parser.takeCurrentFlow(i, i+1) 333 } else { 334 parser.newFlow(i) 335 } 336 }