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