b2eea502d0
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 23s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 32s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m32s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 2m23s
- position.go: Replace O(n) maxPosition scan with O(1) lookup by tracking max position during map construction. Also eliminates shadowing of the builtin max identifier (Go 1.21+). - position.go: Add comment clarifying +++ prefix ordering intent. - adapter.go: Document diff-fetch tradeoff in PostReview. - adapter_test.go: Remove extra blank line between test functions.
191 lines
5.2 KiB
Go
191 lines
5.2 KiB
Go
package gitea
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// PositionMap holds a per-file mapping of GitHub diff-position to new-file line number.
|
|
// Position is a 1-indexed offset from the @@ hunk header line in the unified diff.
|
|
type PositionMap struct {
|
|
// files maps filename → (position → new-file line number).
|
|
// Deletion lines are mapped to -1 (no new-file line).
|
|
files map[string]map[int]int
|
|
// maxPositions caches the highest position number per file,
|
|
// tracked during construction to avoid O(n) scans at translate time.
|
|
maxPositions map[string]int
|
|
}
|
|
|
|
// Translate converts a GitHub diff-position to a new-file line number for a given file.
|
|
// Returns an error if the file is not in the diff or the position is out of range.
|
|
// If the position targets a deletion line, it maps to the nearest non-deletion line below;
|
|
// if no such line exists, returns an error.
|
|
func (pm *PositionMap) Translate(file string, position int) (int, error) {
|
|
if pm == nil || pm.files == nil {
|
|
return 0, fmt.Errorf("empty position map")
|
|
}
|
|
|
|
fileMap, ok := pm.files[file]
|
|
if !ok {
|
|
return 0, fmt.Errorf("file %q not found in diff", file)
|
|
}
|
|
|
|
if position < 1 {
|
|
return 0, fmt.Errorf("position %d out of range (must be >= 1)", position)
|
|
}
|
|
|
|
lineNum, ok := fileMap[position]
|
|
if !ok {
|
|
return 0, fmt.Errorf("position %d out of range for file %q", position, file)
|
|
}
|
|
|
|
// lineNum == -1 means this position is a deletion line.
|
|
// Map to the nearest non-deletion line below.
|
|
if lineNum == -1 {
|
|
maxPos := pm.maxPosition(file)
|
|
for p := position + 1; p <= maxPos; p++ {
|
|
if ln, exists := fileMap[p]; exists && ln > 0 {
|
|
return ln, nil
|
|
}
|
|
}
|
|
return 0, fmt.Errorf("position %d targets a deletion line with no subsequent new-file line in %q", position, file)
|
|
}
|
|
|
|
return lineNum, nil
|
|
}
|
|
|
|
// maxPosition returns the highest position number for a file.
|
|
// O(1) — the maximum is tracked during map construction.
|
|
func (pm *PositionMap) maxPosition(file string) int {
|
|
return pm.maxPositions[file]
|
|
}
|
|
|
|
// BuildPositionToLineMap parses a unified diff and builds a PositionMap
|
|
// mapping diff-position → new-file line number per file.
|
|
//
|
|
// Diff-position counting rules (GitHub spec):
|
|
// - The @@ hunk header line is position 1 for the file's first hunk
|
|
// - Every subsequent line increments position by 1 — context, additions, AND deletions
|
|
// - A new @@ hunk within the same file continues incrementing (does not reset)
|
|
// - Position maps to the new file line number for additions and context lines
|
|
// - Deletion lines have a position but no new-file line number (stored as -1)
|
|
func BuildPositionToLineMap(diff string) *PositionMap {
|
|
pm := &PositionMap{
|
|
files: make(map[string]map[int]int),
|
|
maxPositions: make(map[string]int),
|
|
}
|
|
|
|
lines := strings.Split(diff, "\n")
|
|
var currentFile string
|
|
var position int
|
|
var newLine int
|
|
|
|
for _, line := range lines {
|
|
// Detect new file in diff.
|
|
// "+++ b/" is checked before "+++ /dev/null" — the two prefixes are
|
|
// non-overlapping ("+++ /dev/null" does not start with "+++ b/"), so
|
|
// ordering is independent. Checking the common case first for clarity.
|
|
if strings.HasPrefix(line, "+++ b/") {
|
|
currentFile = strings.TrimPrefix(line, "+++ b/")
|
|
position = 0
|
|
newLine = 0
|
|
if pm.files[currentFile] == nil {
|
|
pm.files[currentFile] = make(map[int]int)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Deleted file: +++ /dev/null means the file is being deleted
|
|
if strings.HasPrefix(line, "+++ /dev/null") {
|
|
currentFile = ""
|
|
continue
|
|
}
|
|
|
|
// Skip --- lines (old file header)
|
|
if strings.HasPrefix(line, "--- ") {
|
|
continue
|
|
}
|
|
|
|
// Skip diff --git lines
|
|
if strings.HasPrefix(line, "diff --git") {
|
|
continue
|
|
}
|
|
|
|
// Skip index lines
|
|
if strings.HasPrefix(line, "index ") {
|
|
continue
|
|
}
|
|
|
|
// Binary file detection
|
|
if strings.HasPrefix(line, "Binary files") {
|
|
currentFile = ""
|
|
continue
|
|
}
|
|
|
|
// Parse hunk headers
|
|
if strings.HasPrefix(line, "@@") && currentFile != "" {
|
|
position++
|
|
pm.maxPositions[currentFile] = position
|
|
newLine = parseHunkStart(line)
|
|
continue
|
|
}
|
|
|
|
if currentFile == "" {
|
|
continue
|
|
}
|
|
|
|
// Skip "\ No newline at end of file" markers
|
|
if strings.HasPrefix(line, `\`) {
|
|
continue
|
|
}
|
|
|
|
// Process diff content lines
|
|
if strings.HasPrefix(line, "+") {
|
|
// Addition: has a new-file line number
|
|
position++
|
|
pm.files[currentFile][position] = newLine
|
|
pm.maxPositions[currentFile] = position
|
|
newLine++
|
|
} else if strings.HasPrefix(line, "-") {
|
|
// Deletion: has a position but no new-file line number
|
|
position++
|
|
pm.files[currentFile][position] = -1
|
|
pm.maxPositions[currentFile] = position
|
|
} else if strings.HasPrefix(line, " ") {
|
|
// Context line
|
|
position++
|
|
pm.files[currentFile][position] = newLine
|
|
pm.maxPositions[currentFile] = position
|
|
newLine++
|
|
}
|
|
}
|
|
|
|
return pm
|
|
}
|
|
|
|
// parseHunkStart extracts the new-file starting line number from a hunk header.
|
|
// Format: @@ -old_start[,old_count] +new_start[,new_count] @@
|
|
func parseHunkStart(hunkLine string) int {
|
|
plusIdx := strings.Index(hunkLine, "+")
|
|
if plusIdx < 0 {
|
|
return 1
|
|
}
|
|
rest := hunkLine[plusIdx+1:]
|
|
|
|
endIdx := 0
|
|
for endIdx < len(rest) && rest[endIdx] >= '0' && rest[endIdx] <= '9' {
|
|
endIdx++
|
|
}
|
|
|
|
if endIdx == 0 {
|
|
return 1
|
|
}
|
|
|
|
n, err := strconv.Atoi(rest[:endIdx])
|
|
if err != nil {
|
|
return 1
|
|
}
|
|
return n
|
|
}
|