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
|
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package main
import (
"bytes"
"crypto/rand"
"encoding/hex"
"fmt"
"io"
"os"
"os/exec"
"strings"
"time"
"github.com/bluekeyes/go-gitdiff/gitdiff"
"github.com/go-git/go-git/v5"
"go.lindenii.runxiyu.org/forge/misc"
)
func (s *server) lmtpHandlePatch(session *lmtpSession, groupPath []string, repoName string, mbox io.Reader) (err error) {
var diffFiles []*gitdiff.File
var preamble string
if diffFiles, preamble, err = gitdiff.Parse(mbox); err != nil {
return fmt.Errorf("failed to parse patch: %w", err)
}
var header *gitdiff.PatchHeader
if header, err = gitdiff.ParsePatchHeader(preamble); err != nil {
return fmt.Errorf("failed to parse patch headers: %w", err)
}
var repo *git.Repository
var fsPath string
repo, _, _, fsPath, err = s.openRepo(session.ctx, groupPath, repoName)
if err != nil {
return fmt.Errorf("failed to open repo: %w", err)
}
headRef, err := repo.Head()
if err != nil {
return fmt.Errorf("failed to get repo head hash: %w", err)
}
headCommit, err := repo.CommitObject(headRef.Hash())
if err != nil {
return fmt.Errorf("failed to get repo head commit: %w", err)
}
headTree, err := headCommit.Tree()
if err != nil {
return fmt.Errorf("failed to get repo head tree: %w", err)
}
headTreeHash := headTree.Hash.String()
blobUpdates := make(map[string][]byte)
for _, diffFile := range diffFiles {
sourceFile, err := headTree.File(diffFile.OldName)
if err != nil {
return fmt.Errorf("failed to get file at old name %#v: %w", diffFile.OldName, err)
}
sourceString, err := sourceFile.Contents()
if err != nil {
return fmt.Errorf("failed to get contents: %w", err)
}
sourceBuf := bytes.NewReader(misc.StringToBytes(sourceString))
var patchedBuf bytes.Buffer
if err := gitdiff.Apply(&patchedBuf, sourceBuf, diffFile); err != nil {
return fmt.Errorf("failed to apply patch: %w", err)
}
var hashBuf bytes.Buffer
// It's really difficult to do this via go-git so we're just
// going to use upstream git for now.
// TODO
cmd := exec.CommandContext(session.ctx, "git", "hash-object", "-w", "-t", "blob", "--stdin")
cmd.Env = append(os.Environ(), "GIT_DIR="+fsPath)
cmd.Stdout = &hashBuf
cmd.Stdin = &patchedBuf
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to run git hash-object: %w", err)
}
newHashStr := strings.TrimSpace(hashBuf.String())
newHash, err := hex.DecodeString(newHashStr)
if err != nil {
return fmt.Errorf("failed to decode hex string from git: %w", err)
}
blobUpdates[diffFile.NewName] = newHash
if diffFile.NewName != diffFile.OldName {
blobUpdates[diffFile.OldName] = nil // Mark for deletion.
}
}
newTreeSha, err := buildTreeRecursive(session.ctx, fsPath, headTreeHash, blobUpdates)
if err != nil {
return fmt.Errorf("failed to recursively build a tree: %w", err)
}
commitMsg := header.Title
if header.Body != "" {
commitMsg += "\n\n" + header.Body
}
env := append(os.Environ(),
"GIT_DIR="+fsPath,
"GIT_AUTHOR_NAME="+header.Author.Name,
"GIT_AUTHOR_EMAIL="+header.Author.Email,
"GIT_AUTHOR_DATE="+header.AuthorDate.Format(time.RFC3339),
)
commitCmd := exec.CommandContext(session.ctx, "git", "commit-tree", newTreeSha, "-p", headCommit.Hash.String(), "-m", commitMsg)
commitCmd.Env = env
var commitOut bytes.Buffer
commitCmd.Stdout = &commitOut
if err := commitCmd.Run(); err != nil {
return fmt.Errorf("failed to commit tree: %w", err)
}
newCommitSha := strings.TrimSpace(commitOut.String())
newBranchName := rand.Text()
refCmd := exec.CommandContext(session.ctx, "git", "update-ref", "refs/heads/contrib/"+newBranchName, newCommitSha) //#nosec G204
refCmd.Env = append(os.Environ(), "GIT_DIR="+fsPath)
if err := refCmd.Run(); err != nil {
return fmt.Errorf("failed to update ref: %w", err)
}
return nil
}
|