package vcs import ( "context" "fmt" "strings" ) // GetAllFilesInPath recursively fetches all file contents under a path using the // provided FileReader. Returns a map of filepath -> content for all files found. // If the path points to an empty directory, returns an empty map. func GetAllFilesInPath(ctx context.Context, client FileReader, owner, repo, path string) (map[string]string, error) { results := make(map[string]string) entries, err := client.ListContents(ctx, owner, repo, path) if err != nil { return nil, fmt.Errorf("list contents %q: %w", path, err) } for _, entry := range entries { switch entry.Type { case "file": content, err := client.GetFileContent(ctx, owner, repo, entry.Path, "") if err != nil { return nil, fmt.Errorf("get file %q: %w", entry.Path, err) } results[entry.Path] = content case "dir": subResults, err := GetAllFilesInPath(ctx, client, owner, repo, entry.Path) if err != nil { return nil, fmt.Errorf("recurse into %q: %w", entry.Path, err) } for k, v := range subResults { results[k] = v } } } return results, nil } // BuildLineToPositionMap parses a unified diff and returns a map of // filename -> (new line number -> diff position). The diff position is a // 1-indexed offset from the @@ hunk header line for each file. // Only lines that appear in the new file (context lines and additions) are mapped. // Deletion-only lines are not included. func BuildLineToPositionMap(diff string) map[string]map[int]int { result := make(map[string]map[int]int) lines := strings.Split(diff, "\n") var currentFile string var position int var newLine int for _, line := range lines { // Detect new file in diff if strings.HasPrefix(line, "+++ b/") { currentFile = strings.TrimPrefix(line, "+++ b/") position = 0 newLine = 0 if result[currentFile] == nil { result[currentFile] = make(map[int]int) } 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 } // Parse hunk headers if strings.HasPrefix(line, "@@") { position++ // Extract new file start line from @@ -a,b +c,d @@ newLine = parseHunkNewStart(line) continue } // We need a current file to map lines if currentFile == "" { continue } // Process diff content lines if strings.HasPrefix(line, "+") { position++ result[currentFile][newLine] = position newLine++ } else if strings.HasPrefix(line, "-") { position++ // Deletion lines don't map to new line numbers } else if strings.HasPrefix(line, " ") { // Context line (space-prefixed) if position > 0 { position++ result[currentFile][newLine] = position newLine++ } } } return result } // parseHunkNewStart extracts the new-file starting line number from a hunk header. // Format: @@ -old_start[,old_count] +new_start[,new_count] @@ func parseHunkNewStart(hunkLine string) int { // Find the +N part plusIdx := strings.Index(hunkLine, "+") if plusIdx < 0 { return 1 } rest := hunkLine[plusIdx+1:] // Read digits until comma or space var numStr string for _, ch := range rest { if ch >= '0' && ch <= '9' { numStr += string(ch) } else { break } } if numStr == "" { return 1 } n := 0 for _, ch := range numStr { n = n*10 + int(ch-'0') } return n }