14 ActionAdd ActionType = "add"
15 ActionDelete ActionType = "delete"
16 ActionUpdate ActionType = "update"
19type FileChange struct {
27 Changes map[string]FileChange
31 OrigIndex int // line index of the first line in the original file
32 DelLines []string // lines to delete
33 InsLines []string // lines to insert
36type PatchAction struct {
44 Actions map[string]PatchAction
47type DiffError struct {
51func (e DiffError) Error() string {
55// Helper functions for error handling
56func NewDiffError(message string) DiffError {
57 return DiffError{message: message}
60func fileError(action, reason, path string) DiffError {
61 return NewDiffError(fmt.Sprintf("%s File Error: %s: %s", action, reason, path))
64func contextError(index int, context string, isEOF bool) DiffError {
65 prefix := "Invalid Context"
67 prefix = "Invalid EOF Context"
69 return NewDiffError(fmt.Sprintf("%s %d:\n%s", prefix, index, context))
73 currentFiles map[string]string
80func NewParser(currentFiles map[string]string, lines []string) *Parser {
82 currentFiles: currentFiles,
85 patch: Patch{Actions: make(map[string]PatchAction, len(currentFiles))},
90func (p *Parser) isDone(prefixes []string) bool {
91 if p.index >= len(p.lines) {
95 for _, prefix := range prefixes {
96 if strings.HasPrefix(p.lines[p.index], prefix) {
104func (p *Parser) startsWith(prefix any) bool {
105 var prefixes []string
106 switch v := prefix.(type) {
108 prefixes = []string{v}
113 for _, pfx := range prefixes {
114 if strings.HasPrefix(p.lines[p.index], pfx) {
121func (p *Parser) readStr(prefix string, returnEverything bool) string {
122 if p.index >= len(p.lines) {
123 return "" // Changed from panic to return empty string for safer operation
125 if strings.HasPrefix(p.lines[p.index], prefix) {
127 if returnEverything {
128 text = p.lines[p.index]
130 text = p.lines[p.index][len(prefix):]
138func (p *Parser) Parse() error {
139 endPatchPrefixes := []string{"*** End Patch"}
141 for !p.isDone(endPatchPrefixes) {
142 path := p.readStr("*** Update File: ", false)
144 if _, exists := p.patch.Actions[path]; exists {
145 return fileError("Update", "Duplicate Path", path)
147 moveTo := p.readStr("*** Move to: ", false)
148 if _, exists := p.currentFiles[path]; !exists {
149 return fileError("Update", "Missing File", path)
151 text := p.currentFiles[path]
152 action, err := p.parseUpdateFile(text)
157 action.MovePath = &moveTo
159 p.patch.Actions[path] = action
163 path = p.readStr("*** Delete File: ", false)
165 if _, exists := p.patch.Actions[path]; exists {
166 return fileError("Delete", "Duplicate Path", path)
168 if _, exists := p.currentFiles[path]; !exists {
169 return fileError("Delete", "Missing File", path)
171 p.patch.Actions[path] = PatchAction{Type: ActionDelete, Chunks: []Chunk{}}
175 path = p.readStr("*** Add File: ", false)
177 if _, exists := p.patch.Actions[path]; exists {
178 return fileError("Add", "Duplicate Path", path)
180 if _, exists := p.currentFiles[path]; exists {
181 return fileError("Add", "File already exists", path)
183 action, err := p.parseAddFile()
187 p.patch.Actions[path] = action
191 return NewDiffError(fmt.Sprintf("Unknown Line: %s", p.lines[p.index]))
194 if !p.startsWith("*** End Patch") {
195 return NewDiffError("Missing End Patch")
202func (p *Parser) parseUpdateFile(text string) (PatchAction, error) {
203 action := PatchAction{Type: ActionUpdate, Chunks: []Chunk{}}
204 fileLines := strings.Split(text, "\n")
207 endPrefixes := []string{
215 for !p.isDone(endPrefixes) {
216 defStr := p.readStr("@@ ", false)
218 if defStr == "" && p.index < len(p.lines) && p.lines[p.index] == "@@" {
219 sectionStr = p.lines[p.index]
222 if !(defStr != "" || sectionStr != "" || index == 0) {
223 return action, NewDiffError(fmt.Sprintf("Invalid Line:\n%s", p.lines[p.index]))
225 if strings.TrimSpace(defStr) != "" {
227 for i := range fileLines[:index] {
228 if fileLines[i] == defStr {
235 for i := index; i < len(fileLines); i++ {
236 if fileLines[i] == defStr {
245 for i := range fileLines[:index] {
246 if strings.TrimSpace(fileLines[i]) == strings.TrimSpace(defStr) {
254 for i := index; i < len(fileLines); i++ {
255 if strings.TrimSpace(fileLines[i]) == strings.TrimSpace(defStr) {
265 nextChunkContext, chunks, endPatchIndex, eof := peekNextSection(p.lines, p.index)
266 newIndex, fuzz := findContext(fileLines, nextChunkContext, index, eof)
268 ctxText := strings.Join(nextChunkContext, "\n")
269 return action, contextError(index, ctxText, eof)
273 for _, ch := range chunks {
274 ch.OrigIndex += newIndex
275 action.Chunks = append(action.Chunks, ch)
277 index = newIndex + len(nextChunkContext)
278 p.index = endPatchIndex
283func (p *Parser) parseAddFile() (PatchAction, error) {
284 lines := make([]string, 0, 16) // Preallocate space for better performance
285 endPrefixes := []string{
292 for !p.isDone(endPrefixes) {
293 s := p.readStr("", true)
294 if !strings.HasPrefix(s, "+") {
295 return PatchAction{}, NewDiffError(fmt.Sprintf("Invalid Add File Line: %s", s))
297 lines = append(lines, s[1:])
300 newFile := strings.Join(lines, "\n")
308// Refactored to use a matcher function for each comparison type
309func findContextCore(lines []string, context []string, start int) (int, int) {
310 if len(context) == 0 {
315 if idx, fuzz := tryFindMatch(lines, context, start, func(a, b string) bool {
321 // Try trimming right whitespace
322 if idx, fuzz := tryFindMatch(lines, context, start, func(a, b string) bool {
323 return strings.TrimRight(a, " \t") == strings.TrimRight(b, " \t")
328 // Try trimming all whitespace
329 if idx, fuzz := tryFindMatch(lines, context, start, func(a, b string) bool {
330 return strings.TrimSpace(a) == strings.TrimSpace(b)
338// Helper function to DRY up the match logic
339func tryFindMatch(lines []string, context []string, start int,
340 compareFunc func(string, string) bool,
342 for i := start; i < len(lines); i++ {
343 if i+len(context) <= len(lines) {
345 for j := range context {
346 if !compareFunc(lines[i+j], context[j]) {
352 // Return fuzz level: 0 for exact, 1 for trimRight, 100 for trimSpace
354 if compareFunc("a ", "a") && !compareFunc("a", "b") {
356 } else if compareFunc("a ", "a") {
366func findContext(lines []string, context []string, start int, eof bool) (int, int) {
368 newIndex, fuzz := findContextCore(lines, context, len(lines)-len(context))
370 return newIndex, fuzz
372 newIndex, fuzz = findContextCore(lines, context, start)
373 return newIndex, fuzz + 10000
375 return findContextCore(lines, context, start)
378func peekNextSection(lines []string, initialIndex int) ([]string, []Chunk, int, bool) {
379 index := initialIndex
380 old := make([]string, 0, 32) // Preallocate for better performance
381 delLines := make([]string, 0, 8)
382 insLines := make([]string, 0, 8)
383 chunks := make([]Chunk, 0, 4)
386 // End conditions for the section
387 endSectionConditions := func(s string) bool {
388 return strings.HasPrefix(s, "@@") ||
389 strings.HasPrefix(s, "*** End Patch") ||
390 strings.HasPrefix(s, "*** Update File:") ||
391 strings.HasPrefix(s, "*** Delete File:") ||
392 strings.HasPrefix(s, "*** Add File:") ||
393 strings.HasPrefix(s, "*** End of File") ||
395 strings.HasPrefix(s, "***")
398 for index < len(lines) {
400 if endSectionConditions(s) {
425 if mode == "keep" && lastMode != mode {
426 if len(insLines) > 0 || len(delLines) > 0 {
427 chunks = append(chunks, Chunk{
428 OrigIndex: len(old) - len(delLines),
433 delLines = make([]string, 0, 8)
434 insLines = make([]string, 0, 8)
436 if mode == "delete" {
437 delLines = append(delLines, line)
438 old = append(old, line)
439 } else if mode == "add" {
440 insLines = append(insLines, line)
442 old = append(old, line)
446 if len(insLines) > 0 || len(delLines) > 0 {
447 chunks = append(chunks, Chunk{
448 OrigIndex: len(old) - len(delLines),
454 if index < len(lines) && lines[index] == "*** End of File" {
456 return old, chunks, index, true
458 return old, chunks, index, false
461func TextToPatch(text string, orig map[string]string) (Patch, int, error) {
462 text = strings.TrimSpace(text)
463 lines := strings.Split(text, "\n")
464 if len(lines) < 2 || !strings.HasPrefix(lines[0], "*** Begin Patch") || lines[len(lines)-1] != "*** End Patch" {
465 return Patch{}, 0, NewDiffError("Invalid patch text")
467 parser := NewParser(orig, lines)
469 if err := parser.Parse(); err != nil {
470 return Patch{}, 0, err
472 return parser.patch, parser.fuzz, nil
475func IdentifyFilesNeeded(text string) []string {
476 text = strings.TrimSpace(text)
477 lines := strings.Split(text, "\n")
478 result := make(map[string]bool)
480 for _, line := range lines {
481 if strings.HasPrefix(line, "*** Update File: ") {
482 result[line[len("*** Update File: "):]] = true
484 if strings.HasPrefix(line, "*** Delete File: ") {
485 result[line[len("*** Delete File: "):]] = true
489 files := make([]string, 0, len(result))
490 for file := range result {
491 files = append(files, file)
496func IdentifyFilesAdded(text string) []string {
497 text = strings.TrimSpace(text)
498 lines := strings.Split(text, "\n")
499 result := make(map[string]bool)
501 for _, line := range lines {
502 if strings.HasPrefix(line, "*** Add File: ") {
503 result[line[len("*** Add File: "):]] = true
507 files := make([]string, 0, len(result))
508 for file := range result {
509 files = append(files, file)
514func getUpdatedFile(text string, action PatchAction, path string) (string, error) {
515 if action.Type != ActionUpdate {
516 return "", errors.New("Expected UPDATE action")
518 origLines := strings.Split(text, "\n")
519 destLines := make([]string, 0, len(origLines)) // Preallocate with capacity
522 for _, chunk := range action.Chunks {
523 if chunk.OrigIndex > len(origLines) {
524 return "", NewDiffError(fmt.Sprintf("%s: chunk.orig_index %d > len(lines) %d", path, chunk.OrigIndex, len(origLines)))
526 if origIndex > chunk.OrigIndex {
527 return "", NewDiffError(fmt.Sprintf("%s: orig_index %d > chunk.orig_index %d", path, origIndex, chunk.OrigIndex))
529 destLines = append(destLines, origLines[origIndex:chunk.OrigIndex]...)
530 delta := chunk.OrigIndex - origIndex
533 if len(chunk.InsLines) > 0 {
534 destLines = append(destLines, chunk.InsLines...)
536 origIndex += len(chunk.DelLines)
539 destLines = append(destLines, origLines[origIndex:]...)
540 return strings.Join(destLines, "\n"), nil
543func PatchToCommit(patch Patch, orig map[string]string) (Commit, error) {
544 commit := Commit{Changes: make(map[string]FileChange, len(patch.Actions))}
545 for pathKey, action := range patch.Actions {
546 if action.Type == ActionDelete {
547 oldContent := orig[pathKey]
548 commit.Changes[pathKey] = FileChange{
550 OldContent: &oldContent,
552 } else if action.Type == ActionAdd {
553 commit.Changes[pathKey] = FileChange{
555 NewContent: action.NewFile,
557 } else if action.Type == ActionUpdate {
558 newContent, err := getUpdatedFile(orig[pathKey], action, pathKey)
562 oldContent := orig[pathKey]
563 fileChange := FileChange{
565 OldContent: &oldContent,
566 NewContent: &newContent,
568 if action.MovePath != nil {
569 fileChange.MovePath = action.MovePath
571 commit.Changes[pathKey] = fileChange
577func AssembleChanges(orig map[string]string, updatedFiles map[string]string) Commit {
578 commit := Commit{Changes: make(map[string]FileChange, len(updatedFiles))}
579 for p, newContent := range updatedFiles {
580 oldContent, exists := orig[p]
581 if exists && oldContent == newContent {
585 if exists && newContent != "" {
586 commit.Changes[p] = FileChange{
588 OldContent: &oldContent,
589 NewContent: &newContent,
591 } else if newContent != "" {
592 commit.Changes[p] = FileChange{
594 NewContent: &newContent,
597 commit.Changes[p] = FileChange{
599 OldContent: &oldContent,
602 return commit // Changed from panic to simply return current commit
608func LoadFiles(paths []string, openFn func(string) (string, error)) (map[string]string, error) {
609 orig := make(map[string]string, len(paths))
610 for _, p := range paths {
611 content, err := openFn(p)
613 return nil, fileError("Open", "File not found", p)
620func ApplyCommit(commit Commit, writeFn func(string, string) error, removeFn func(string) error) error {
621 for p, change := range commit.Changes {
622 if change.Type == ActionDelete {
623 if err := removeFn(p); err != nil {
626 } else if change.Type == ActionAdd {
627 if change.NewContent == nil {
628 return NewDiffError(fmt.Sprintf("Add action for %s has nil new_content", p))
630 if err := writeFn(p, *change.NewContent); err != nil {
633 } else if change.Type == ActionUpdate {
634 if change.NewContent == nil {
635 return NewDiffError(fmt.Sprintf("Update action for %s has nil new_content", p))
637 if change.MovePath != nil {
638 if err := writeFn(*change.MovePath, *change.NewContent); err != nil {
641 if err := removeFn(p); err != nil {
645 if err := writeFn(p, *change.NewContent); err != nil {
654func ProcessPatch(text string, openFn func(string) (string, error), writeFn func(string, string) error, removeFn func(string) error) (string, error) {
655 if !strings.HasPrefix(text, "*** Begin Patch") {
656 return "", NewDiffError("Patch must start with *** Begin Patch")
658 paths := IdentifyFilesNeeded(text)
659 orig, err := LoadFiles(paths, openFn)
664 patch, fuzz, err := TextToPatch(text, orig)
670 return "", NewDiffError(fmt.Sprintf("Patch contains fuzzy matches (fuzz level: %d)", fuzz))
673 commit, err := PatchToCommit(patch, orig)
678 if err := ApplyCommit(commit, writeFn, removeFn); err != nil {
682 return "Patch applied successfully", nil
685func OpenFile(p string) (string, error) {
686 data, err := os.ReadFile(p)
690 return string(data), nil
693func WriteFile(p string, content string) error {
694 if filepath.IsAbs(p) {
695 return NewDiffError("We do not support absolute paths.")
698 dir := filepath.Dir(p)
700 if err := os.MkdirAll(dir, 0o755); err != nil {
705 return os.WriteFile(p, []byte(content), 0o644)
708func RemoveFile(p string) error {
712func ValidatePatch(patchText string, files map[string]string) (bool, string, error) {
713 if !strings.HasPrefix(patchText, "*** Begin Patch") {
714 return false, "Patch must start with *** Begin Patch", nil
717 neededFiles := IdentifyFilesNeeded(patchText)
718 for _, filePath := range neededFiles {
719 if _, exists := files[filePath]; !exists {
720 return false, fmt.Sprintf("File not found: %s", filePath), nil
724 patch, fuzz, err := TextToPatch(patchText, files)
726 return false, err.Error(), nil
730 return false, fmt.Sprintf("Patch contains fuzzy matches (fuzz level: %d)", fuzz), nil
733 _, err = PatchToCommit(patch, files)
735 return false, err.Error(), nil
738 return true, "Patch is valid", nil
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list