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