patch.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. package diff
  2. import (
  3. "errors"
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. )
  9. type ActionType string
  10. const (
  11. ActionAdd ActionType = "add"
  12. ActionDelete ActionType = "delete"
  13. ActionUpdate ActionType = "update"
  14. )
  15. type FileChange struct {
  16. Type ActionType
  17. OldContent *string
  18. NewContent *string
  19. MovePath *string
  20. }
  21. type Commit struct {
  22. Changes map[string]FileChange
  23. }
  24. type Chunk struct {
  25. OrigIndex int // line index of the first line in the original file
  26. DelLines []string // lines to delete
  27. InsLines []string // lines to insert
  28. }
  29. type PatchAction struct {
  30. Type ActionType
  31. NewFile *string
  32. Chunks []Chunk
  33. MovePath *string
  34. }
  35. type Patch struct {
  36. Actions map[string]PatchAction
  37. }
  38. type DiffError struct {
  39. message string
  40. }
  41. func (e DiffError) Error() string {
  42. return e.message
  43. }
  44. // Helper functions for error handling
  45. func NewDiffError(message string) DiffError {
  46. return DiffError{message: message}
  47. }
  48. func fileError(action, reason, path string) DiffError {
  49. return NewDiffError(fmt.Sprintf("%s File Error: %s: %s", action, reason, path))
  50. }
  51. func contextError(index int, context string, isEOF bool) DiffError {
  52. prefix := "Invalid Context"
  53. if isEOF {
  54. prefix = "Invalid EOF Context"
  55. }
  56. return NewDiffError(fmt.Sprintf("%s %d:\n%s", prefix, index, context))
  57. }
  58. type Parser struct {
  59. currentFiles map[string]string
  60. lines []string
  61. index int
  62. patch Patch
  63. fuzz int
  64. }
  65. func NewParser(currentFiles map[string]string, lines []string) *Parser {
  66. return &Parser{
  67. currentFiles: currentFiles,
  68. lines: lines,
  69. index: 0,
  70. patch: Patch{Actions: make(map[string]PatchAction, len(currentFiles))},
  71. fuzz: 0,
  72. }
  73. }
  74. func (p *Parser) isDone(prefixes []string) bool {
  75. if p.index >= len(p.lines) {
  76. return true
  77. }
  78. for _, prefix := range prefixes {
  79. if strings.HasPrefix(p.lines[p.index], prefix) {
  80. return true
  81. }
  82. }
  83. return false
  84. }
  85. func (p *Parser) startsWith(prefix any) bool {
  86. var prefixes []string
  87. switch v := prefix.(type) {
  88. case string:
  89. prefixes = []string{v}
  90. case []string:
  91. prefixes = v
  92. }
  93. for _, pfx := range prefixes {
  94. if strings.HasPrefix(p.lines[p.index], pfx) {
  95. return true
  96. }
  97. }
  98. return false
  99. }
  100. func (p *Parser) readStr(prefix string, returnEverything bool) string {
  101. if p.index >= len(p.lines) {
  102. return "" // Changed from panic to return empty string for safer operation
  103. }
  104. if strings.HasPrefix(p.lines[p.index], prefix) {
  105. var text string
  106. if returnEverything {
  107. text = p.lines[p.index]
  108. } else {
  109. text = p.lines[p.index][len(prefix):]
  110. }
  111. p.index++
  112. return text
  113. }
  114. return ""
  115. }
  116. func (p *Parser) Parse() error {
  117. endPatchPrefixes := []string{"*** End Patch"}
  118. for !p.isDone(endPatchPrefixes) {
  119. path := p.readStr("*** Update File: ", false)
  120. if path != "" {
  121. if _, exists := p.patch.Actions[path]; exists {
  122. return fileError("Update", "Duplicate Path", path)
  123. }
  124. moveTo := p.readStr("*** Move to: ", false)
  125. if _, exists := p.currentFiles[path]; !exists {
  126. return fileError("Update", "Missing File", path)
  127. }
  128. text := p.currentFiles[path]
  129. action, err := p.parseUpdateFile(text)
  130. if err != nil {
  131. return err
  132. }
  133. if moveTo != "" {
  134. action.MovePath = &moveTo
  135. }
  136. p.patch.Actions[path] = action
  137. continue
  138. }
  139. path = p.readStr("*** Delete File: ", false)
  140. if path != "" {
  141. if _, exists := p.patch.Actions[path]; exists {
  142. return fileError("Delete", "Duplicate Path", path)
  143. }
  144. if _, exists := p.currentFiles[path]; !exists {
  145. return fileError("Delete", "Missing File", path)
  146. }
  147. p.patch.Actions[path] = PatchAction{Type: ActionDelete, Chunks: []Chunk{}}
  148. continue
  149. }
  150. path = p.readStr("*** Add File: ", false)
  151. if path != "" {
  152. if _, exists := p.patch.Actions[path]; exists {
  153. return fileError("Add", "Duplicate Path", path)
  154. }
  155. if _, exists := p.currentFiles[path]; exists {
  156. return fileError("Add", "File already exists", path)
  157. }
  158. action, err := p.parseAddFile()
  159. if err != nil {
  160. return err
  161. }
  162. p.patch.Actions[path] = action
  163. continue
  164. }
  165. return NewDiffError(fmt.Sprintf("Unknown Line: %s", p.lines[p.index]))
  166. }
  167. if !p.startsWith("*** End Patch") {
  168. return NewDiffError("Missing End Patch")
  169. }
  170. p.index++
  171. return nil
  172. }
  173. func (p *Parser) parseUpdateFile(text string) (PatchAction, error) {
  174. action := PatchAction{Type: ActionUpdate, Chunks: []Chunk{}}
  175. fileLines := strings.Split(text, "\n")
  176. index := 0
  177. endPrefixes := []string{
  178. "*** End Patch",
  179. "*** Update File:",
  180. "*** Delete File:",
  181. "*** Add File:",
  182. "*** End of File",
  183. }
  184. for !p.isDone(endPrefixes) {
  185. defStr := p.readStr("@@ ", false)
  186. sectionStr := ""
  187. if defStr == "" && p.index < len(p.lines) && p.lines[p.index] == "@@" {
  188. sectionStr = p.lines[p.index]
  189. p.index++
  190. }
  191. if defStr == "" && sectionStr == "" && index != 0 {
  192. return action, NewDiffError(fmt.Sprintf("Invalid Line:\n%s", p.lines[p.index]))
  193. }
  194. if strings.TrimSpace(defStr) != "" {
  195. found := false
  196. for i := range fileLines[:index] {
  197. if fileLines[i] == defStr {
  198. found = true
  199. break
  200. }
  201. }
  202. if !found {
  203. for i := index; i < len(fileLines); i++ {
  204. if fileLines[i] == defStr {
  205. index = i + 1
  206. found = true
  207. break
  208. }
  209. }
  210. }
  211. if !found {
  212. for i := range fileLines[:index] {
  213. if strings.TrimSpace(fileLines[i]) == strings.TrimSpace(defStr) {
  214. found = true
  215. break
  216. }
  217. }
  218. }
  219. if !found {
  220. for i := index; i < len(fileLines); i++ {
  221. if strings.TrimSpace(fileLines[i]) == strings.TrimSpace(defStr) {
  222. index = i + 1
  223. p.fuzz++
  224. found = true
  225. break
  226. }
  227. }
  228. }
  229. }
  230. nextChunkContext, chunks, endPatchIndex, eof := peekNextSection(p.lines, p.index)
  231. newIndex, fuzz := findContext(fileLines, nextChunkContext, index, eof)
  232. if newIndex == -1 {
  233. ctxText := strings.Join(nextChunkContext, "\n")
  234. return action, contextError(index, ctxText, eof)
  235. }
  236. p.fuzz += fuzz
  237. for _, ch := range chunks {
  238. ch.OrigIndex += newIndex
  239. action.Chunks = append(action.Chunks, ch)
  240. }
  241. index = newIndex + len(nextChunkContext)
  242. p.index = endPatchIndex
  243. }
  244. return action, nil
  245. }
  246. func (p *Parser) parseAddFile() (PatchAction, error) {
  247. lines := make([]string, 0, 16) // Preallocate space for better performance
  248. endPrefixes := []string{
  249. "*** End Patch",
  250. "*** Update File:",
  251. "*** Delete File:",
  252. "*** Add File:",
  253. }
  254. for !p.isDone(endPrefixes) {
  255. s := p.readStr("", true)
  256. if !strings.HasPrefix(s, "+") {
  257. return PatchAction{}, NewDiffError(fmt.Sprintf("Invalid Add File Line: %s", s))
  258. }
  259. lines = append(lines, s[1:])
  260. }
  261. newFile := strings.Join(lines, "\n")
  262. return PatchAction{
  263. Type: ActionAdd,
  264. NewFile: &newFile,
  265. Chunks: []Chunk{},
  266. }, nil
  267. }
  268. // Refactored to use a matcher function for each comparison type
  269. func findContextCore(lines []string, context []string, start int) (int, int) {
  270. if len(context) == 0 {
  271. return start, 0
  272. }
  273. // Try exact match
  274. if idx, fuzz := tryFindMatch(lines, context, start, func(a, b string) bool {
  275. return a == b
  276. }); idx >= 0 {
  277. return idx, fuzz
  278. }
  279. // Try trimming right whitespace
  280. if idx, fuzz := tryFindMatch(lines, context, start, func(a, b string) bool {
  281. return strings.TrimRight(a, " \t") == strings.TrimRight(b, " \t")
  282. }); idx >= 0 {
  283. return idx, fuzz
  284. }
  285. // Try trimming all whitespace
  286. if idx, fuzz := tryFindMatch(lines, context, start, func(a, b string) bool {
  287. return strings.TrimSpace(a) == strings.TrimSpace(b)
  288. }); idx >= 0 {
  289. return idx, fuzz
  290. }
  291. return -1, 0
  292. }
  293. // Helper function to DRY up the match logic
  294. func tryFindMatch(lines []string, context []string, start int,
  295. compareFunc func(string, string) bool,
  296. ) (int, int) {
  297. for i := start; i < len(lines); i++ {
  298. if i+len(context) <= len(lines) {
  299. match := true
  300. for j := range context {
  301. if !compareFunc(lines[i+j], context[j]) {
  302. match = false
  303. break
  304. }
  305. }
  306. if match {
  307. // Return fuzz level: 0 for exact, 1 for trimRight, 100 for trimSpace
  308. var fuzz int
  309. if compareFunc("a ", "a") && !compareFunc("a", "b") {
  310. fuzz = 1
  311. } else if compareFunc("a ", "a") {
  312. fuzz = 100
  313. }
  314. return i, fuzz
  315. }
  316. }
  317. }
  318. return -1, 0
  319. }
  320. func findContext(lines []string, context []string, start int, eof bool) (int, int) {
  321. if eof {
  322. newIndex, fuzz := findContextCore(lines, context, len(lines)-len(context))
  323. if newIndex != -1 {
  324. return newIndex, fuzz
  325. }
  326. newIndex, fuzz = findContextCore(lines, context, start)
  327. return newIndex, fuzz + 10000
  328. }
  329. return findContextCore(lines, context, start)
  330. }
  331. func peekNextSection(lines []string, initialIndex int) ([]string, []Chunk, int, bool) {
  332. index := initialIndex
  333. old := make([]string, 0, 32) // Preallocate for better performance
  334. delLines := make([]string, 0, 8)
  335. insLines := make([]string, 0, 8)
  336. chunks := make([]Chunk, 0, 4)
  337. mode := "keep"
  338. // End conditions for the section
  339. endSectionConditions := func(s string) bool {
  340. return strings.HasPrefix(s, "@@") ||
  341. strings.HasPrefix(s, "*** End Patch") ||
  342. strings.HasPrefix(s, "*** Update File:") ||
  343. strings.HasPrefix(s, "*** Delete File:") ||
  344. strings.HasPrefix(s, "*** Add File:") ||
  345. strings.HasPrefix(s, "*** End of File") ||
  346. s == "***" ||
  347. strings.HasPrefix(s, "***")
  348. }
  349. for index < len(lines) {
  350. s := lines[index]
  351. if endSectionConditions(s) {
  352. break
  353. }
  354. index++
  355. lastMode := mode
  356. line := s
  357. if len(line) > 0 {
  358. switch line[0] {
  359. case '+':
  360. mode = "add"
  361. case '-':
  362. mode = "delete"
  363. case ' ':
  364. mode = "keep"
  365. default:
  366. mode = "keep"
  367. line = " " + line
  368. }
  369. } else {
  370. mode = "keep"
  371. line = " "
  372. }
  373. line = line[1:]
  374. if mode == "keep" && lastMode != mode {
  375. if len(insLines) > 0 || len(delLines) > 0 {
  376. chunks = append(chunks, Chunk{
  377. OrigIndex: len(old) - len(delLines),
  378. DelLines: delLines,
  379. InsLines: insLines,
  380. })
  381. }
  382. delLines = make([]string, 0, 8)
  383. insLines = make([]string, 0, 8)
  384. }
  385. switch mode {
  386. case "delete":
  387. delLines = append(delLines, line)
  388. old = append(old, line)
  389. case "add":
  390. insLines = append(insLines, line)
  391. default:
  392. old = append(old, line)
  393. }
  394. }
  395. if len(insLines) > 0 || len(delLines) > 0 {
  396. chunks = append(chunks, Chunk{
  397. OrigIndex: len(old) - len(delLines),
  398. DelLines: delLines,
  399. InsLines: insLines,
  400. })
  401. }
  402. if index < len(lines) && lines[index] == "*** End of File" {
  403. index++
  404. return old, chunks, index, true
  405. }
  406. return old, chunks, index, false
  407. }
  408. func TextToPatch(text string, orig map[string]string) (Patch, int, error) {
  409. text = strings.TrimSpace(text)
  410. lines := strings.Split(text, "\n")
  411. if len(lines) < 2 || !strings.HasPrefix(lines[0], "*** Begin Patch") || lines[len(lines)-1] != "*** End Patch" {
  412. return Patch{}, 0, NewDiffError("Invalid patch text")
  413. }
  414. parser := NewParser(orig, lines)
  415. parser.index = 1
  416. if err := parser.Parse(); err != nil {
  417. return Patch{}, 0, err
  418. }
  419. return parser.patch, parser.fuzz, nil
  420. }
  421. func IdentifyFilesNeeded(text string) []string {
  422. text = strings.TrimSpace(text)
  423. lines := strings.Split(text, "\n")
  424. result := make(map[string]bool)
  425. for _, line := range lines {
  426. if strings.HasPrefix(line, "*** Update File: ") {
  427. result[line[len("*** Update File: "):]] = true
  428. }
  429. if strings.HasPrefix(line, "*** Delete File: ") {
  430. result[line[len("*** Delete File: "):]] = true
  431. }
  432. }
  433. files := make([]string, 0, len(result))
  434. for file := range result {
  435. files = append(files, file)
  436. }
  437. return files
  438. }
  439. func IdentifyFilesAdded(text string) []string {
  440. text = strings.TrimSpace(text)
  441. lines := strings.Split(text, "\n")
  442. result := make(map[string]bool)
  443. for _, line := range lines {
  444. if strings.HasPrefix(line, "*** Add File: ") {
  445. result[line[len("*** Add File: "):]] = true
  446. }
  447. }
  448. files := make([]string, 0, len(result))
  449. for file := range result {
  450. files = append(files, file)
  451. }
  452. return files
  453. }
  454. func getUpdatedFile(text string, action PatchAction, path string) (string, error) {
  455. if action.Type != ActionUpdate {
  456. return "", errors.New("expected UPDATE action")
  457. }
  458. origLines := strings.Split(text, "\n")
  459. destLines := make([]string, 0, len(origLines)) // Preallocate with capacity
  460. origIndex := 0
  461. for _, chunk := range action.Chunks {
  462. if chunk.OrigIndex > len(origLines) {
  463. return "", NewDiffError(fmt.Sprintf("%s: chunk.orig_index %d > len(lines) %d", path, chunk.OrigIndex, len(origLines)))
  464. }
  465. if origIndex > chunk.OrigIndex {
  466. return "", NewDiffError(fmt.Sprintf("%s: orig_index %d > chunk.orig_index %d", path, origIndex, chunk.OrigIndex))
  467. }
  468. destLines = append(destLines, origLines[origIndex:chunk.OrigIndex]...)
  469. delta := chunk.OrigIndex - origIndex
  470. origIndex += delta
  471. if len(chunk.InsLines) > 0 {
  472. destLines = append(destLines, chunk.InsLines...)
  473. }
  474. origIndex += len(chunk.DelLines)
  475. }
  476. destLines = append(destLines, origLines[origIndex:]...)
  477. return strings.Join(destLines, "\n"), nil
  478. }
  479. func PatchToCommit(patch Patch, orig map[string]string) (Commit, error) {
  480. commit := Commit{Changes: make(map[string]FileChange, len(patch.Actions))}
  481. for pathKey, action := range patch.Actions {
  482. switch action.Type {
  483. case ActionDelete:
  484. oldContent := orig[pathKey]
  485. commit.Changes[pathKey] = FileChange{
  486. Type: ActionDelete,
  487. OldContent: &oldContent,
  488. }
  489. case ActionAdd:
  490. commit.Changes[pathKey] = FileChange{
  491. Type: ActionAdd,
  492. NewContent: action.NewFile,
  493. }
  494. case ActionUpdate:
  495. newContent, err := getUpdatedFile(orig[pathKey], action, pathKey)
  496. if err != nil {
  497. return Commit{}, err
  498. }
  499. oldContent := orig[pathKey]
  500. fileChange := FileChange{
  501. Type: ActionUpdate,
  502. OldContent: &oldContent,
  503. NewContent: &newContent,
  504. }
  505. if action.MovePath != nil {
  506. fileChange.MovePath = action.MovePath
  507. }
  508. commit.Changes[pathKey] = fileChange
  509. }
  510. }
  511. return commit, nil
  512. }
  513. func AssembleChanges(orig map[string]string, updatedFiles map[string]string) Commit {
  514. commit := Commit{Changes: make(map[string]FileChange, len(updatedFiles))}
  515. for p, newContent := range updatedFiles {
  516. oldContent, exists := orig[p]
  517. if exists && oldContent == newContent {
  518. continue
  519. }
  520. if exists && newContent != "" {
  521. commit.Changes[p] = FileChange{
  522. Type: ActionUpdate,
  523. OldContent: &oldContent,
  524. NewContent: &newContent,
  525. }
  526. } else if newContent != "" {
  527. commit.Changes[p] = FileChange{
  528. Type: ActionAdd,
  529. NewContent: &newContent,
  530. }
  531. } else if exists {
  532. commit.Changes[p] = FileChange{
  533. Type: ActionDelete,
  534. OldContent: &oldContent,
  535. }
  536. } else {
  537. return commit // Changed from panic to simply return current commit
  538. }
  539. }
  540. return commit
  541. }
  542. func LoadFiles(paths []string, openFn func(string) (string, error)) (map[string]string, error) {
  543. orig := make(map[string]string, len(paths))
  544. for _, p := range paths {
  545. content, err := openFn(p)
  546. if err != nil {
  547. return nil, fileError("Open", "File not found", p)
  548. }
  549. orig[p] = content
  550. }
  551. return orig, nil
  552. }
  553. func ApplyCommit(commit Commit, writeFn func(string, string) error, removeFn func(string) error) error {
  554. for p, change := range commit.Changes {
  555. switch change.Type {
  556. case ActionDelete:
  557. if err := removeFn(p); err != nil {
  558. return err
  559. }
  560. case ActionAdd:
  561. if change.NewContent == nil {
  562. return NewDiffError(fmt.Sprintf("Add action for %s has nil new_content", p))
  563. }
  564. if err := writeFn(p, *change.NewContent); err != nil {
  565. return err
  566. }
  567. case ActionUpdate:
  568. if change.NewContent == nil {
  569. return NewDiffError(fmt.Sprintf("Update action for %s has nil new_content", p))
  570. }
  571. if change.MovePath != nil {
  572. if err := writeFn(*change.MovePath, *change.NewContent); err != nil {
  573. return err
  574. }
  575. if err := removeFn(p); err != nil {
  576. return err
  577. }
  578. } else {
  579. if err := writeFn(p, *change.NewContent); err != nil {
  580. return err
  581. }
  582. }
  583. }
  584. }
  585. return nil
  586. }
  587. func ProcessPatch(text string, openFn func(string) (string, error), writeFn func(string, string) error, removeFn func(string) error) (string, error) {
  588. if !strings.HasPrefix(text, "*** Begin Patch") {
  589. return "", NewDiffError("Patch must start with *** Begin Patch")
  590. }
  591. paths := IdentifyFilesNeeded(text)
  592. orig, err := LoadFiles(paths, openFn)
  593. if err != nil {
  594. return "", err
  595. }
  596. patch, fuzz, err := TextToPatch(text, orig)
  597. if err != nil {
  598. return "", err
  599. }
  600. if fuzz > 0 {
  601. return "", NewDiffError(fmt.Sprintf("Patch contains fuzzy matches (fuzz level: %d)", fuzz))
  602. }
  603. commit, err := PatchToCommit(patch, orig)
  604. if err != nil {
  605. return "", err
  606. }
  607. if err := ApplyCommit(commit, writeFn, removeFn); err != nil {
  608. return "", err
  609. }
  610. return "Patch applied successfully", nil
  611. }
  612. func OpenFile(p string) (string, error) {
  613. data, err := os.ReadFile(p)
  614. if err != nil {
  615. return "", err
  616. }
  617. return string(data), nil
  618. }
  619. func WriteFile(p string, content string) error {
  620. if filepath.IsAbs(p) {
  621. return NewDiffError("We do not support absolute paths.")
  622. }
  623. dir := filepath.Dir(p)
  624. if dir != "." {
  625. if err := os.MkdirAll(dir, 0o755); err != nil {
  626. return err
  627. }
  628. }
  629. return os.WriteFile(p, []byte(content), 0o644)
  630. }
  631. func RemoveFile(p string) error {
  632. return os.Remove(p)
  633. }
  634. func ValidatePatch(patchText string, files map[string]string) (bool, string, error) {
  635. if !strings.HasPrefix(patchText, "*** Begin Patch") {
  636. return false, "Patch must start with *** Begin Patch", nil
  637. }
  638. neededFiles := IdentifyFilesNeeded(patchText)
  639. for _, filePath := range neededFiles {
  640. if _, exists := files[filePath]; !exists {
  641. return false, fmt.Sprintf("File not found: %s", filePath), nil
  642. }
  643. }
  644. patch, fuzz, err := TextToPatch(patchText, files)
  645. if err != nil {
  646. return false, err.Error(), nil
  647. }
  648. if fuzz > 0 {
  649. return false, fmt.Sprintf("Patch contains fuzzy matches (fuzz level: %d)", fuzz), nil
  650. }
  651. _, err = PatchToCommit(patch, files)
  652. if err != nil {
  653. return false, err.Error(), nil
  654. }
  655. return true, "Patch is valid", nil
  656. }