1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
|
package gitpb
import (
"bufio"
"bytes"
"crypto/sha1"
"encoding/hex"
"fmt"
"io"
"os/exec"
"strings"
"go.wit.com/log"
)
// git show 5b277e7686974d2195586d5f5b82838ee9ddb036 |git patch-id --stable
// bf86be06af03b1a89ee155b214358362ec76f7b6 5b277e7686974d2195586d5f5b82838ee9ddb036
// forged patch: "working on ping pong"
// The --stable flag is an important detail. When you use it, git patch-id outputs two hashes:
// <stable_patch_id> <unstable_patch_id>
func (repo *Repo) FindPatchIdByHash(hash string) (string, error) {
if hash == "" {
return "", log.Errorf("commit hash blank")
}
// 1. Create the command to get the diff for the commit.
// "git show" is the perfect tool for this.
cmdShow := exec.Command("git", "show", hash)
cmdShow.Dir = repo.GetFullPath()
// 2. Create the command to calculate the patch-id from stdin.
cmdPipeID := exec.Command("git", "patch-id", "--stable")
cmdPipeID.Dir = repo.GetFullPath()
// 3. Connect the output of "git show" to the input of "git patch-id".
// This is the Go equivalent of the shell pipe `|`.
pipe, err := cmdShow.StdoutPipe()
if err != nil {
return "", fmt.Errorf("failed to create pipe: %w", err)
}
cmdPipeID.Stdin = pipe
// 4. We need a buffer to capture the final output from git patch-id.
var output bytes.Buffer
cmdPipeID.Stdout = &output
// 5. Start the reading command (patch-id) first.
if err := cmdPipeID.Start(); err != nil {
return "", fmt.Errorf("failed to start git-patch-id: %w", err)
}
// 6. Run the writing command (show). This will block until it's done.
if err := cmdShow.Run(); err != nil {
return "", fmt.Errorf("failed to run git-show: %w", err)
}
// 7. Wait for the reading command to finish.
if err := cmdPipeID.Wait(); err != nil {
return "", fmt.Errorf("failed to wait for git-patch-id: %w", err)
}
fields := strings.Fields(output.String())
if len(fields) != 2 {
return "", fmt.Errorf("git-patch-id produced empty output")
}
if fields[1] != hash {
return "", fmt.Errorf("patchid did not match %s != %v", hash, fields)
}
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 <[email protected]>
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)
}
*/
|