github.com/devstream-io/devstream@v0.13.3/internal/pkg/patch/patch.go (about) 1 package patch 2 3 import ( 4 "bufio" 5 "fmt" 6 "os" 7 "os/exec" 8 "regexp" 9 "strings" 10 11 "github.com/devstream-io/devstream/internal/log" 12 ) 13 14 const ( 15 processOptionTabToSpace ProcessOption = "tabToSpace" 16 processOptionSpaceToTab ProcessOption = "spaceToTab" 17 ) 18 19 type ProcessOption string 20 21 // Patch calls the patch command to apply a diff file to an original 22 func Patch(patchFile string) error { 23 log.Infof("Patching file: %s", patchFile) 24 25 // Fix patch file if it mixed tab and space indentation 26 err := fixPatchFile(patchFile) 27 if err != nil { 28 return fmt.Errorf("patch file fix failed: %w", err) 29 } 30 31 // Check if the patch command exists 32 patchPath, err := exec.LookPath("patch") 33 if err != nil { 34 return fmt.Errorf("patch command not found: %w", err) 35 } 36 37 // Use the patch tool to apply the patch 38 cmd := exec.Command(patchPath, "-i", patchFile, "-t", "-p0") 39 output, err := cmd.CombinedOutput() 40 if err != nil { 41 return fmt.Errorf("patch command failed: %w\nOutput: %s", err, string(output)) 42 } 43 44 log.Infof("Successfully patched the file") 45 return nil 46 } 47 48 // checkPatchCommand checks if the patch command exists and is executable 49 func checkPatchCommand() error { 50 // Check if the patch command exists 51 path, err := exec.LookPath("patch") 52 if err != nil { 53 return fmt.Errorf("patch command not found: %w", err) 54 } 55 56 // Check if the patch command is executable 57 fileInfo, err := os.Stat(path) 58 if err != nil { 59 return fmt.Errorf("failed to stat patch command: %w", err) 60 } 61 62 if fileInfo.Mode()&0111 == 0 { 63 return fmt.Errorf("patch command is not executable") 64 } 65 66 return nil 67 } 68 69 // fixPatchFile fixes the patch file if it mixed tab and space indentation. 70 // The patch file is generated by GPT4, and it may have different indentation with the original file. 71 // The original file path is contained in the patch file, so we can use the fix the patch file by using the original file. 72 // If the original file uses tab indentation, we replace all spaces with tabs in the patch file. 73 // If the original file uses space indentation, we replace all tabs with spaces in the patch file. 74 func fixPatchFile(patchFile string) error { 75 // Read the original file path from the patch file 76 originalFilePath, err := extractOriginalFilePathFromPatchFile(patchFile) 77 78 if err != nil { 79 return fmt.Errorf("failed to extract original file path from patch string: %w", err) 80 } 81 82 // Check if the original file contain tabs in the indentation 83 original, err := os.Open(originalFilePath) 84 if err != nil { 85 return fmt.Errorf("failed to open original file: %w", err) 86 } 87 defer original.Close() 88 89 hasTab := false 90 scanner := bufio.NewScanner(original) 91 for scanner.Scan() { 92 line := scanner.Text() 93 if strings.HasPrefix(line, "\t") { 94 hasTab = true 95 break 96 } 97 } 98 99 if err = scanner.Err(); err != nil { 100 return fmt.Errorf("failed to read original file: %w", err) 101 } 102 103 // The original file uses tab indentation 104 if hasTab { 105 // Replace all space indentation with tabs in the patch file 106 if err = processTabSpaceSwitch(patchFile, processOptionSpaceToTab); err != nil { 107 return fmt.Errorf("failed to process tab to space: %w", err) 108 } 109 // The original file uses space indentation 110 } else { 111 // Replace all tab indentation with spaces in the patch file 112 if err = processTabSpaceSwitch(patchFile, processOptionTabToSpace); err != nil { 113 return fmt.Errorf("failed to process space to tab: %w", err) 114 } 115 } 116 117 return nil 118 } 119 120 // ExtractOriginalFilePathFromPatchString extracts the original file path from a patch string 121 // e.g. --- pkg/patch/patch.go 2021-08-15 16:00:00.000000000 +0900 -> pkg/patch/patch.go 122 func extractOriginalFilePathFromPatchFile(patchFile string) (string, error) { 123 // Read content from the patch file 124 fileContent, err := os.ReadFile(patchFile) 125 if err != nil { 126 return "", fmt.Errorf("failed to read patch file: %w", err) 127 } 128 129 lines := strings.Split(string(fileContent), "\n") 130 131 for _, line := range lines { 132 if strings.HasPrefix(line, "--- ") { 133 fields := strings.Fields(line) 134 if len(fields) > 1 { 135 return fields[1], nil 136 } 137 } 138 } 139 140 return "", fmt.Errorf("original file path not found in patch string") 141 } 142 143 // processTabSpaceSwitch processes the tab/space indentation switch in a file 144 // If the option is processOptionTabToSpace, it replaces all tabs with spaces 145 // If the option is processOptionSpaceToTab, it replaces all spaces with tabs 146 func processTabSpaceSwitch(filePath string, option ProcessOption) error { 147 file, err := os.Open(filePath) 148 if err != nil { 149 return fmt.Errorf("failed to open file: %w", err) 150 } 151 defer file.Close() 152 153 scanner := bufio.NewScanner(file) 154 var processedLines []string 155 156 // Matches the start of the string (^) followed by an optional + or - sign, followed by one or more groups of 4 spaces ( {4})+ 157 spaceRegex := regexp.MustCompile(`^(\+|\-)?( {4})+`) 158 // Matches the start of the string (^) followed by an optional + or - sign, followed by one or more tabs (\t)+ 159 tabRegex := regexp.MustCompile(`^(\+|\-)?\t+`) 160 161 for scanner.Scan() { 162 line := scanner.Text() 163 if option == processOptionTabToSpace { 164 line = tabRegex.ReplaceAllStringFunc(line, func(s string) string { 165 prefix := "" 166 if s[0] == '+' || s[0] == '-' { 167 prefix = string(s[0]) 168 s = s[1:] 169 } 170 return prefix + strings.Repeat(" ", len(s)) 171 }) 172 } else if option == processOptionSpaceToTab { 173 line = spaceRegex.ReplaceAllStringFunc(line, func(s string) string { 174 prefix := "" 175 if s[0] == '+' || s[0] == '-' { 176 prefix = string(s[0]) 177 s = s[1:] 178 } 179 return prefix + strings.Repeat("\t", len(s)/4) 180 }) 181 } else { 182 return fmt.Errorf("invalid process option: %s", option) 183 } 184 processedLines = append(processedLines, line) 185 } 186 187 if err = scanner.Err(); err != nil { 188 return fmt.Errorf("failed to read file: %w", err) 189 } 190 191 err = os.WriteFile(filePath, []byte(strings.Join(processedLines, "\n")+"\n"), 0644) 192 if err != nil { 193 return fmt.Errorf("failed to write file: %w", err) 194 } 195 196 return nil 197 }