patch.go 18 KB

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