forked from tangled.org/core
this repo has no description
at master 5.0 kB view raw
1package patchutil 2 3import ( 4 "fmt" 5 "os" 6 "os/exec" 7 "regexp" 8 "strings" 9 10 "github.com/bluekeyes/go-gitdiff/gitdiff" 11) 12 13type FormatPatch struct { 14 Files []*gitdiff.File 15 *gitdiff.PatchHeader 16} 17 18func ExtractPatches(formatPatch string) ([]FormatPatch, error) { 19 patches := splitFormatPatch(formatPatch) 20 21 result := []FormatPatch{} 22 23 for _, patch := range patches { 24 files, headerStr, err := gitdiff.Parse(strings.NewReader(patch)) 25 if err != nil { 26 return nil, fmt.Errorf("failed to parse patch: %w", err) 27 } 28 29 header, err := gitdiff.ParsePatchHeader(headerStr) 30 if err != nil { 31 return nil, fmt.Errorf("failed to parse patch header: %w", err) 32 } 33 34 result = append(result, FormatPatch{ 35 Files: files, 36 PatchHeader: header, 37 }) 38 } 39 40 return result, nil 41} 42 43// IsPatchValid checks if the given patch string is valid. 44// It performs very basic sniffing for either git-diff or git-format-patch 45// header lines. For format patches, it attempts to extract and validate each one. 46func IsPatchValid(patch string) bool { 47 if len(patch) == 0 { 48 return false 49 } 50 51 lines := strings.Split(patch, "\n") 52 if len(lines) < 2 { 53 return false 54 } 55 56 firstLine := strings.TrimSpace(lines[0]) 57 58 // check if it's a git diff 59 if strings.HasPrefix(firstLine, "diff ") || 60 strings.HasPrefix(firstLine, "--- ") || 61 strings.HasPrefix(firstLine, "Index: ") || 62 strings.HasPrefix(firstLine, "+++ ") || 63 strings.HasPrefix(firstLine, "@@ ") { 64 return true 65 } 66 67 // check if it's format-patch 68 if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") || 69 strings.HasPrefix(firstLine, "From: ") { 70 // ExtractPatches already runs it through gitdiff.Parse so if that errors, 71 // it's safe to say it's broken. 72 patches, err := ExtractPatches(patch) 73 if err != nil { 74 return false 75 } 76 return len(patches) > 0 77 } 78 79 return false 80} 81 82func IsFormatPatch(patch string) bool { 83 lines := strings.Split(patch, "\n") 84 if len(lines) < 2 { 85 return false 86 } 87 88 firstLine := strings.TrimSpace(lines[0]) 89 if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") { 90 return true 91 } 92 93 headerCount := 0 94 for i := range min(10, len(lines)) { 95 line := strings.TrimSpace(lines[i]) 96 if strings.HasPrefix(line, "From: ") || 97 strings.HasPrefix(line, "Date: ") || 98 strings.HasPrefix(line, "Subject: ") || 99 strings.HasPrefix(line, "commit ") { 100 headerCount++ 101 } 102 } 103 104 return headerCount >= 2 105} 106 107func splitFormatPatch(patchText string) []string { 108 re := regexp.MustCompile(`(?m)^From [0-9a-f]{40} .*$`) 109 110 indexes := re.FindAllStringIndex(patchText, -1) 111 112 if len(indexes) == 0 { 113 return []string{} 114 } 115 116 patches := make([]string, len(indexes)) 117 118 for i := range indexes { 119 startPos := indexes[i][0] 120 endPos := len(patchText) 121 122 if i < len(indexes)-1 { 123 endPos = indexes[i+1][0] 124 } 125 126 patches[i] = strings.TrimSpace(patchText[startPos:endPos]) 127 } 128 return patches 129} 130 131func bestName(file *gitdiff.File) string { 132 if file.IsDelete { 133 return file.OldName 134 } else { 135 return file.NewName 136 } 137} 138 139// in-place reverse of a diff 140func reverseDiff(file *gitdiff.File) { 141 file.OldName, file.NewName = file.NewName, file.OldName 142 file.OldMode, file.NewMode = file.NewMode, file.OldMode 143 file.BinaryFragment, file.ReverseBinaryFragment = file.ReverseBinaryFragment, file.BinaryFragment 144 145 for _, fragment := range file.TextFragments { 146 // swap postions 147 fragment.OldPosition, fragment.NewPosition = fragment.NewPosition, fragment.OldPosition 148 fragment.OldLines, fragment.NewLines = fragment.NewLines, fragment.OldLines 149 fragment.LinesAdded, fragment.LinesDeleted = fragment.LinesDeleted, fragment.LinesAdded 150 151 for i := range fragment.Lines { 152 switch fragment.Lines[i].Op { 153 case gitdiff.OpAdd: 154 fragment.Lines[i].Op = gitdiff.OpDelete 155 case gitdiff.OpDelete: 156 fragment.Lines[i].Op = gitdiff.OpAdd 157 default: 158 // do nothing 159 } 160 } 161 } 162} 163 164func Unified(oldText, oldFile, newText, newFile string) (string, error) { 165 oldTemp, err := os.CreateTemp("", "old_*") 166 if err != nil { 167 return "", fmt.Errorf("failed to create temp file for oldText: %w", err) 168 } 169 defer os.Remove(oldTemp.Name()) 170 if _, err := oldTemp.WriteString(oldText); err != nil { 171 return "", fmt.Errorf("failed to write to old temp file: %w", err) 172 } 173 oldTemp.Close() 174 175 newTemp, err := os.CreateTemp("", "new_*") 176 if err != nil { 177 return "", fmt.Errorf("failed to create temp file for newText: %w", err) 178 } 179 defer os.Remove(newTemp.Name()) 180 if _, err := newTemp.WriteString(newText); err != nil { 181 return "", fmt.Errorf("failed to write to new temp file: %w", err) 182 } 183 newTemp.Close() 184 185 cmd := exec.Command("diff", "-u", "--label", oldFile, "--label", newFile, oldTemp.Name(), newTemp.Name()) 186 output, err := cmd.CombinedOutput() 187 188 if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { 189 return string(output), nil 190 } 191 if err != nil { 192 return "", fmt.Errorf("diff command failed: %w", err) 193 } 194 195 return string(output), nil 196}