From 5e3adeadf40b29f63e0e8236e120aeae124ff82a Mon Sep 17 00:00:00 2001 From: Jeff Carr Date: Mon, 6 Oct 2025 19:35:26 -0500 Subject: gen 'git patch-id' from raw bytes --- patchid.go | 135 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 131 insertions(+), 4 deletions(-) (limited to 'patchid.go') diff --git a/patchid.go b/patchid.go index 28c8313..72761a9 100644 --- a/patchid.go +++ b/patchid.go @@ -1,18 +1,18 @@ package gitpb import ( + "bufio" "bytes" - "errors" + "crypto/sha1" + "encoding/hex" "fmt" - "os" + "io" "os/exec" - "path/filepath" "strings" "go.wit.com/log" ) - // git show 5b277e7686974d2195586d5f5b82838ee9ddb036 |git patch-id --stable // bf86be06af03b1a89ee155b214358362ec76f7b6 5b277e7686974d2195586d5f5b82838ee9ddb036 // forged patch: "working on ping pong" @@ -70,3 +70,130 @@ func (repo *Repo) FindPatchIdByHash(hash string) (string, error) { return fields[0], nil } + +// ComputePatchID calculates the patch ID for a given patch file's content. +// It mimics the behavior of the `git patch-id` command by normalizing the +// patch content before hashing. +// +// Normalization rules: +// 1. It ignores all lines until the first "--- " line is encountered. +// 2. It ignores diff headers (lines starting with "---", "+++", "diff --git", "index"). +// 3. For content lines ('+', '-', or ' '), it keeps the first character and +// trims any trailing whitespace from the rest of the line before hashing. +// 4. All other lines (like hunk headers "@@ ... @@") are ignored. +func FindPatchIdByBytes(patchData io.Reader) (string, error) { + scanner := bufio.NewScanner(patchData) + var normalizedPatch bytes.Buffer + var inPatchBody bool + + for scanner.Scan() { + line := scanner.Text() + + // The patch content officially starts at the first `---` line. + // This helps skip email headers in patches generated by `git format-patch`. + if !inPatchBody && strings.HasPrefix(line, "---") { + inPatchBody = true + } + + if !inPatchBody { + continue + } + + // Skip headers and metadata lines + if strings.HasPrefix(line, "---") || + strings.HasPrefix(line, "+++") || + strings.HasPrefix(line, "index") || + strings.HasPrefix(line, "diff --git") { + continue + } + + // Process only the actual content lines (context, addition, deletion) + if len(line) > 0 && (line[0] == ' ' || line[0] == '+' || line[0] == '-') { + // Keep the first character (' ', '+', or '-') + firstChar := line[0] + content := line[1:] + + // Trim trailing whitespace from the content part of the line + trimmedContent := strings.TrimRight(content, " ") + + // Write the normalized line to our buffer for hashing + normalizedPatch.WriteByte(firstChar) + normalizedPatch.WriteString(trimmedContent) + normalizedPatch.WriteByte('\n') + } + // All other lines (e.g., "@@ ... @@") are ignored. + } + + if err := scanner.Err(); err != nil { + return "", fmt.Errorf("error reading patch data: %w", err) + } + + // Calculate the SHA-1 hash of the normalized patch content + hash := sha1.Sum(normalizedPatch.Bytes()) + patchID := hex.EncodeToString(hash[:]) + + return patchID, nil +} + +/* +// --- Example Usage --- + +func main() { + // Example 1: Using a string as input + patchString := ` +From 2f42795678c7113f7437c04f56570c08b68850f4 Mon Sep 17 00:00:00 2001 +From: A Developer +Subject: [PATCH] Example change to demonstrate patch-id + +--- a/README.md ++++ b/README.md +@@ -1,3 +1,4 @@ + # My Project + +-This is a sample project. ++This is a sample project. ++It has an additional line. +` + stringReader := strings.NewReader(patchString) + patchID, err := ComputePatchID(stringReader) + if err != nil { + fmt.Fprintf(os.Stderr, "Error computing patch ID from string: %v\n", err) + return + } + fmt.Printf("Patch ID from string: %s\n", patchID) + + // Example 2: Reading from a file + // Create a temporary patch file for demonstration + patchContent := []byte(patchString) + tmpfile, err := os.CreateTemp("", "example.*.patch") + if err != nil { + fmt.Fprintf(os.Stderr, "Error creating temp file: %v\n", err) + return + } + defer os.Remove(tmpfile.Name()) // clean up + + if _, err := tmpfile.Write(patchContent); err != nil { + fmt.Fprintf(os.Stderr, "Error writing to temp file: %v\n", err) + return + } + if err := tmpfile.Close(); err != nil { + fmt.Fprintf(os.Stderr, "Error closing temp file: %v\n", err) + return + } + + // Open the file to pass to our function + file, err := os.Open(tmpfile.Name()) + if err != nil { + fmt.Fprintf(os.Stderr, "Error opening temp file: %v\n", err) + return + } + defer file.Close() + + patchIDFromFile, err := ComputePatchID(file) + if err != nil { + fmt.Fprintf(os.Stderr, "Error computing patch ID from file: %v\n", err) + return + } + fmt.Printf("Patch ID from file: %s\n", patchIDFromFile) +} +*/ -- cgit v1.2.3