Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329package patchutil
import ( "errors" "fmt" "log" "os" "os/exec" "regexp" "slices" "strings"
"github.com/bluekeyes/go-gitdiff/gitdiff" "tangled.org/core/types")
func ExtractPatches(formatPatch string) ([]types.FormatPatch, error) { patches := splitFormatPatch(formatPatch)
result := []types.FormatPatch{}
for _, patch := range patches { files, headerStr, err := gitdiff.Parse(strings.NewReader(patch)) if err != nil { return nil, fmt.Errorf("failed to parse patch: %w", err) }
header, err := gitdiff.ParsePatchHeader(headerStr) if err != nil { return nil, fmt.Errorf("failed to parse patch header: %w", err) }
result = append(result, types.FormatPatch{ Files: files, PatchHeader: header, Raw: patch, }) }
return result, nil}
// IsPatchValid checks if the given patch string is valid.// It performs very basic sniffing for either git-diff or git-format-patch// header lines. For format patches, it attempts to extract and validate each one.var ( EmptyPatchError error = errors.New("patch is empty") GenericPatchError error = errors.New("patch is invalid") FormatPatchError error = errors.New("patch is not a valid format-patch"))
func IsPatchValid(patch string) error { if len(patch) == 0 { return EmptyPatchError }
lines := strings.Split(patch, "\n") if len(lines) < 2 { return EmptyPatchError }
firstLine := strings.TrimSpace(lines[0])
// check if it's a git diff if strings.HasPrefix(firstLine, "diff ") || strings.HasPrefix(firstLine, "--- ") || strings.HasPrefix(firstLine, "Index: ") || strings.HasPrefix(firstLine, "+++ ") || strings.HasPrefix(firstLine, "@@ ") { return nil }
// check if it's format-patch if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") || strings.HasPrefix(firstLine, "From: ") { // ExtractPatches already runs it through gitdiff.Parse so if that errors, // it's safe to say it's broken. patches, err := ExtractPatches(patch) if err != nil { return fmt.Errorf("%w: %w", FormatPatchError, err) } if len(patches) == 0 { return EmptyPatchError }
return nil }
return GenericPatchError}
func IsFormatPatch(patch string) bool { lines := strings.Split(patch, "\n") if len(lines) < 2 { return false }
firstLine := strings.TrimSpace(lines[0]) if strings.HasPrefix(firstLine, "From ") && strings.Contains(firstLine, " Mon Sep 17 00:00:00 2001") { return true }
headerCount := 0 for i := range min(10, len(lines)) { line := strings.TrimSpace(lines[i]) if strings.HasPrefix(line, "From: ") || strings.HasPrefix(line, "Date: ") || strings.HasPrefix(line, "Subject: ") || strings.HasPrefix(line, "commit ") { headerCount++ } }
return headerCount >= 2}
func splitFormatPatch(patchText string) []string { re := regexp.MustCompile(`(?m)^From [0-9a-f]{40} .*$`)
indexes := re.FindAllStringIndex(patchText, -1)
if len(indexes) == 0 { return []string{} }
patches := make([]string, len(indexes))
for i := range indexes { startPos := indexes[i][0] endPos := len(patchText)
if i < len(indexes)-1 { endPos = indexes[i+1][0] }
patches[i] = strings.TrimSpace(patchText[startPos:endPos]) } return patches}
func bestName(file *gitdiff.File) string { if file.IsDelete { return file.OldName } else { return file.NewName }}
// in-place reverse of a difffunc reverseDiff(file *gitdiff.File) { file.OldName, file.NewName = file.NewName, file.OldName file.OldMode, file.NewMode = file.NewMode, file.OldMode file.BinaryFragment, file.ReverseBinaryFragment = file.ReverseBinaryFragment, file.BinaryFragment
for _, fragment := range file.TextFragments { // swap postions fragment.OldPosition, fragment.NewPosition = fragment.NewPosition, fragment.OldPosition fragment.OldLines, fragment.NewLines = fragment.NewLines, fragment.OldLines fragment.LinesAdded, fragment.LinesDeleted = fragment.LinesDeleted, fragment.LinesAdded
for i := range fragment.Lines { switch fragment.Lines[i].Op { case gitdiff.OpAdd: fragment.Lines[i].Op = gitdiff.OpDelete case gitdiff.OpDelete: fragment.Lines[i].Op = gitdiff.OpAdd default: // do nothing } } }}
func Unified(oldText, oldFile, newText, newFile string) (string, error) { oldTemp, err := os.CreateTemp("", "old_*") if err != nil { return "", fmt.Errorf("failed to create temp file for oldText: %w", err) } defer os.Remove(oldTemp.Name()) if _, err := oldTemp.WriteString(oldText); err != nil { return "", fmt.Errorf("failed to write to old temp file: %w", err) } oldTemp.Close()
newTemp, err := os.CreateTemp("", "new_*") if err != nil { return "", fmt.Errorf("failed to create temp file for newText: %w", err) } defer os.Remove(newTemp.Name()) if _, err := newTemp.WriteString(newText); err != nil { return "", fmt.Errorf("failed to write to new temp file: %w", err) } newTemp.Close()
cmd := exec.Command("diff", "-u", "--label", oldFile, "--label", newFile, oldTemp.Name(), newTemp.Name()) output, err := cmd.CombinedOutput()
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { return string(output), nil } if err != nil { return "", fmt.Errorf("diff command failed: %w", err) }
return string(output), nil}
// are two patches identicalfunc Equal(a, b []*gitdiff.File) bool { return slices.EqualFunc(a, b, func(x, y *gitdiff.File) bool { // same pointer if x == y { return true } if x == nil || y == nil { return x == y }
// compare file metadata if x.OldName != y.OldName || x.NewName != y.NewName { return false } if x.OldMode != y.OldMode || x.NewMode != y.NewMode { return false } if x.IsNew != y.IsNew || x.IsDelete != y.IsDelete || x.IsCopy != y.IsCopy || x.IsRename != y.IsRename { return false }
if len(x.TextFragments) != len(y.TextFragments) { return false }
for i, xFrag := range x.TextFragments { yFrag := y.TextFragments[i]
// Compare fragment headers if xFrag.OldPosition != yFrag.OldPosition || xFrag.OldLines != yFrag.OldLines || xFrag.NewPosition != yFrag.NewPosition || xFrag.NewLines != yFrag.NewLines { return false }
// Compare fragment changes if len(xFrag.Lines) != len(yFrag.Lines) { return false }
for j, xLine := range xFrag.Lines { yLine := yFrag.Lines[j] if xLine.Op != yLine.Op || xLine.Line != yLine.Line { return false } } }
return true })}
// sort patch files in alphabetical orderfunc SortPatch(patch []*gitdiff.File) { slices.SortFunc(patch, func(a, b *gitdiff.File) int { return strings.Compare(bestName(a), bestName(b)) })}
func AsDiff(patch string) ([]*gitdiff.File, error) { // if format-patch; then extract each patch var diffs []*gitdiff.File if IsFormatPatch(patch) { patches, err := ExtractPatches(patch) if err != nil { return nil, err } var ps [][]*gitdiff.File for _, p := range patches { ps = append(ps, p.Files) }
diffs = CombineDiff(ps...) } else { d, _, err := gitdiff.Parse(strings.NewReader(patch)) if err != nil { return nil, err } diffs = d }
return diffs, nil}
func AsNiceDiff(patch, targetBranch string) types.NiceDiff { diffs, err := AsDiff(patch) if err != nil { log.Println(err) }
nd := types.NiceDiff{}
for _, d := range diffs { ndiff := types.Diff{} ndiff.Name.New = d.NewName ndiff.Name.Old = d.OldName ndiff.IsBinary = d.IsBinary ndiff.IsNew = d.IsNew ndiff.IsDelete = d.IsDelete ndiff.IsCopy = d.IsCopy ndiff.IsRename = d.IsRename
for _, tf := range d.TextFragments { ndiff.TextFragments = append(ndiff.TextFragments, *tf) for _, l := range tf.Lines { switch l.Op { case gitdiff.OpAdd: nd.Stat.Insertions += 1 case gitdiff.OpDelete: nd.Stat.Deletions += 1 } } }
nd.Diff = append(nd.Diff, ndiff) }
nd.Stat.FilesChanged = len(diffs)
return nd}