forked from tangled.org/core
this repo has no description
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 Raw string 17} 18 19func (f FormatPatch) ChangeId() (string, error) { 20 if vals, ok := f.RawHeaders["Change-Id"]; ok && len(vals) == 1 { 21 return vals[0], nil 22 } 23 return "", fmt.Errorf("no change-id found") 24} 25 26func ExtractPatches(formatPatch string) ([]FormatPatch, error) { 27 patches := splitFormatPatch(formatPatch) 28 29 result := []FormatPatch{} 30 31 for _, patch := range patches { 32 files, headerStr, err := gitdiff.Parse(strings.NewReader(patch)) 33 if err != nil { 34 return nil, fmt.Errorf("failed to parse patch: %w", err) 35 } 36 37 header, err := gitdiff.ParsePatchHeader(headerStr) 38 if err != nil { 39 return nil, fmt.Errorf("failed to parse patch header: %w", err) 40 } 41 42 result = append(result, FormatPatch{ 43 Files: files, 44 PatchHeader: header, 45 Raw: patch, 46 }) 47 } 48 49 return result, nil 50} 51 52// IsPatchValid checks if the given patch string is valid. 53// It performs very basic sniffing for either git-diff or git-format-patch 54// header lines. For format patches, it attempts to extract and validate each one. 55func IsPatchValid(patch string) bool { 56 if len(patch) == 0 { 57 return false 58 } 59 60 lines := strings.Split(patch, "\n") 61 if len(lines) < 2 { 62 return false 63 } 64 65 firstLine := strings.TrimSpace(lines[0]) 66 67 // check if it's a git diff 68 if strings.HasPrefix(firstLine, "diff ") || 69 strings.HasPrefix(firstLine, "--- ") || 70 strings.HasPrefix(firstLine, "Index: ") || 71 strings.HasPrefix(firstLine, "+++ ") || 72 strings.HasPrefix(firstLine, "@@ ") { 73 return true 74 } 75 76 // check if it's format-patch 77 if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") || 78 strings.HasPrefix(firstLine, "From: ") { 79 // ExtractPatches already runs it through gitdiff.Parse so if that errors, 80 // it's safe to say it's broken. 81 patches, err := ExtractPatches(patch) 82 if err != nil { 83 return false 84 } 85 return len(patches) > 0 86 } 87 88 return false 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 116func splitFormatPatch(patchText string) []string { 117 re := regexp.MustCompile(`(?m)^From [0-9a-f]{40} .*$`) 118 119 indexes := re.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}