forked from tangled.org/core
this repo has no description
at master 7.7 kB view raw
1package patchutil 2 3import ( 4 "fmt" 5 "log" 6 "os" 7 "os/exec" 8 "regexp" 9 "slices" 10 "strings" 11 12 "github.com/bluekeyes/go-gitdiff/gitdiff" 13 "tangled.sh/tangled.sh/core/types" 14) 15 16func ExtractPatches(formatPatch string) ([]types.FormatPatch, error) { 17 patches := splitFormatPatch(formatPatch) 18 19 result := []types.FormatPatch{} 20 21 for _, patch := range patches { 22 files, headerStr, err := gitdiff.Parse(strings.NewReader(patch)) 23 if err != nil { 24 return nil, fmt.Errorf("failed to parse patch: %w", err) 25 } 26 27 header, err := gitdiff.ParsePatchHeader(headerStr) 28 if err != nil { 29 return nil, fmt.Errorf("failed to parse patch header: %w", err) 30 } 31 32 result = append(result, types.FormatPatch{ 33 Files: files, 34 PatchHeader: header, 35 Raw: patch, 36 }) 37 } 38 39 return result, nil 40} 41 42// IsPatchValid checks if the given patch string is valid. 43// It performs very basic sniffing for either git-diff or git-format-patch 44// header lines. For format patches, it attempts to extract and validate each one. 45func IsPatchValid(patch string) bool { 46 if len(patch) == 0 { 47 return false 48 } 49 50 lines := strings.Split(patch, "\n") 51 if len(lines) < 2 { 52 return false 53 } 54 55 firstLine := strings.TrimSpace(lines[0]) 56 57 // check if it's a git diff 58 if strings.HasPrefix(firstLine, "diff ") || 59 strings.HasPrefix(firstLine, "--- ") || 60 strings.HasPrefix(firstLine, "Index: ") || 61 strings.HasPrefix(firstLine, "+++ ") || 62 strings.HasPrefix(firstLine, "@@ ") { 63 return true 64 } 65 66 // check if it's format-patch 67 if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") || 68 strings.HasPrefix(firstLine, "From: ") { 69 // ExtractPatches already runs it through gitdiff.Parse so if that errors, 70 // it's safe to say it's broken. 71 patches, err := ExtractPatches(patch) 72 if err != nil { 73 return false 74 } 75 return len(patches) > 0 76 } 77 78 return false 79} 80 81func IsFormatPatch(patch string) bool { 82 lines := strings.Split(patch, "\n") 83 if len(lines) < 2 { 84 return false 85 } 86 87 firstLine := strings.TrimSpace(lines[0]) 88 if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") { 89 return true 90 } 91 92 headerCount := 0 93 for i := range min(10, len(lines)) { 94 line := strings.TrimSpace(lines[i]) 95 if strings.HasPrefix(line, "From: ") || 96 strings.HasPrefix(line, "Date: ") || 97 strings.HasPrefix(line, "Subject: ") || 98 strings.HasPrefix(line, "commit ") { 99 headerCount++ 100 } 101 } 102 103 return headerCount >= 2 104} 105 106func splitFormatPatch(patchText string) []string { 107 re := regexp.MustCompile(`(?m)^From [0-9a-f]{40} .*$`) 108 109 indexes := re.FindAllStringIndex(patchText, -1) 110 111 if len(indexes) == 0 { 112 return []string{} 113 } 114 115 patches := make([]string, len(indexes)) 116 117 for i := range indexes { 118 startPos := indexes[i][0] 119 endPos := len(patchText) 120 121 if i < len(indexes)-1 { 122 endPos = indexes[i+1][0] 123 } 124 125 patches[i] = strings.TrimSpace(patchText[startPos:endPos]) 126 } 127 return patches 128} 129 130func bestName(file *gitdiff.File) string { 131 if file.IsDelete { 132 return file.OldName 133 } else { 134 return file.NewName 135 } 136} 137 138// in-place reverse of a diff 139func reverseDiff(file *gitdiff.File) { 140 file.OldName, file.NewName = file.NewName, file.OldName 141 file.OldMode, file.NewMode = file.NewMode, file.OldMode 142 file.BinaryFragment, file.ReverseBinaryFragment = file.ReverseBinaryFragment, file.BinaryFragment 143 144 for _, fragment := range file.TextFragments { 145 // swap postions 146 fragment.OldPosition, fragment.NewPosition = fragment.NewPosition, fragment.OldPosition 147 fragment.OldLines, fragment.NewLines = fragment.NewLines, fragment.OldLines 148 fragment.LinesAdded, fragment.LinesDeleted = fragment.LinesDeleted, fragment.LinesAdded 149 150 for i := range fragment.Lines { 151 switch fragment.Lines[i].Op { 152 case gitdiff.OpAdd: 153 fragment.Lines[i].Op = gitdiff.OpDelete 154 case gitdiff.OpDelete: 155 fragment.Lines[i].Op = gitdiff.OpAdd 156 default: 157 // do nothing 158 } 159 } 160 } 161} 162 163func Unified(oldText, oldFile, newText, newFile string) (string, error) { 164 oldTemp, err := os.CreateTemp("", "old_*") 165 if err != nil { 166 return "", fmt.Errorf("failed to create temp file for oldText: %w", err) 167 } 168 defer os.Remove(oldTemp.Name()) 169 if _, err := oldTemp.WriteString(oldText); err != nil { 170 return "", fmt.Errorf("failed to write to old temp file: %w", err) 171 } 172 oldTemp.Close() 173 174 newTemp, err := os.CreateTemp("", "new_*") 175 if err != nil { 176 return "", fmt.Errorf("failed to create temp file for newText: %w", err) 177 } 178 defer os.Remove(newTemp.Name()) 179 if _, err := newTemp.WriteString(newText); err != nil { 180 return "", fmt.Errorf("failed to write to new temp file: %w", err) 181 } 182 newTemp.Close() 183 184 cmd := exec.Command("diff", "-u", "--label", oldFile, "--label", newFile, oldTemp.Name(), newTemp.Name()) 185 output, err := cmd.CombinedOutput() 186 187 if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { 188 return string(output), nil 189 } 190 if err != nil { 191 return "", fmt.Errorf("diff command failed: %w", err) 192 } 193 194 return string(output), nil 195} 196 197// are two patches identical 198func Equal(a, b []*gitdiff.File) bool { 199 return slices.EqualFunc(a, b, func(x, y *gitdiff.File) bool { 200 // same pointer 201 if x == y { 202 return true 203 } 204 if x == nil || y == nil { 205 return x == y 206 } 207 208 // compare file metadata 209 if x.OldName != y.OldName || x.NewName != y.NewName { 210 return false 211 } 212 if x.OldMode != y.OldMode || x.NewMode != y.NewMode { 213 return false 214 } 215 if x.IsNew != y.IsNew || x.IsDelete != y.IsDelete || x.IsCopy != y.IsCopy || x.IsRename != y.IsRename { 216 return false 217 } 218 219 if len(x.TextFragments) != len(y.TextFragments) { 220 return false 221 } 222 223 for i, xFrag := range x.TextFragments { 224 yFrag := y.TextFragments[i] 225 226 // Compare fragment headers 227 if xFrag.OldPosition != yFrag.OldPosition || xFrag.OldLines != yFrag.OldLines || 228 xFrag.NewPosition != yFrag.NewPosition || xFrag.NewLines != yFrag.NewLines { 229 return false 230 } 231 232 // Compare fragment changes 233 if len(xFrag.Lines) != len(yFrag.Lines) { 234 return false 235 } 236 237 for j, xLine := range xFrag.Lines { 238 yLine := yFrag.Lines[j] 239 if xLine.Op != yLine.Op || xLine.Line != yLine.Line { 240 return false 241 } 242 } 243 } 244 245 return true 246 }) 247} 248 249// sort patch files in alphabetical order 250func SortPatch(patch []*gitdiff.File) { 251 slices.SortFunc(patch, func(a, b *gitdiff.File) int { 252 return strings.Compare(bestName(a), bestName(b)) 253 }) 254} 255 256func AsDiff(patch string) ([]*gitdiff.File, error) { 257 // if format-patch; then extract each patch 258 var diffs []*gitdiff.File 259 if IsFormatPatch(patch) { 260 patches, err := ExtractPatches(patch) 261 if err != nil { 262 return nil, err 263 } 264 var ps [][]*gitdiff.File 265 for _, p := range patches { 266 ps = append(ps, p.Files) 267 } 268 269 diffs = CombineDiff(ps...) 270 } else { 271 d, _, err := gitdiff.Parse(strings.NewReader(patch)) 272 if err != nil { 273 return nil, err 274 } 275 diffs = d 276 } 277 278 return diffs, nil 279} 280 281func AsNiceDiff(patch, targetBranch string) types.NiceDiff { 282 diffs, err := AsDiff(patch) 283 if err != nil { 284 log.Println(err) 285 } 286 287 nd := types.NiceDiff{} 288 nd.Commit.Parent = targetBranch 289 290 for _, d := range diffs { 291 ndiff := types.Diff{} 292 ndiff.Name.New = d.NewName 293 ndiff.Name.Old = d.OldName 294 ndiff.IsBinary = d.IsBinary 295 ndiff.IsNew = d.IsNew 296 ndiff.IsDelete = d.IsDelete 297 ndiff.IsCopy = d.IsCopy 298 ndiff.IsRename = d.IsRename 299 300 for _, tf := range d.TextFragments { 301 ndiff.TextFragments = append(ndiff.TextFragments, *tf) 302 for _, l := range tf.Lines { 303 switch l.Op { 304 case gitdiff.OpAdd: 305 nd.Stat.Insertions += 1 306 case gitdiff.OpDelete: 307 nd.Stat.Deletions += 1 308 } 309 } 310 } 311 312 nd.Diff = append(nd.Diff, ndiff) 313 } 314 315 nd.Stat.FilesChanged = len(diffs) 316 317 return nd 318}