Hi… I am well aware that this diff view is very suboptimal. It will be fixed when the refactored server comes along!
LMTP: Actually apply patches from email
// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org>
package main
import (
"bytes"
"context"
"encoding/hex"
"errors"
"os"
"os/exec"
"path"
"sort"
"strings"
)
func writeTree(ctx context.Context, repoPath string, entries []treeEntry) (string, error) {
var buf bytes.Buffer
// Must
sort.Slice(entries, func(i, j int) bool {
return entries[i].name < entries[j].name
})
for _, e := range entries {
buf.WriteString(e.mode)
buf.WriteByte(' ')
buf.WriteString(e.name)
buf.WriteByte(0)
buf.Write(e.sha)
}
cmd := exec.CommandContext(ctx, "git", "hash-object", "-w", "-t", "tree", "--stdin")
cmd.Env = append(os.Environ(), "GIT_DIR="+repoPath)
cmd.Stdin = &buf
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return "", err
}
return strings.TrimSpace(out.String()), nil
}
func buildTreeRecursive(ctx context.Context, repoPath string, baseTree string, updates map[string][]byte) (string, error) {
treeCache := make(map[string][]treeEntry)
var walk func(string, string) error
walk = func(prefix, sha string) error {
cmd := exec.CommandContext(ctx, "git", "cat-file", "tree", sha)
cmd.Env = append(os.Environ(), "GIT_DIR="+repoPath)
var out bytes.Buffer
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
return err
}
data := out.Bytes()
i := 0
var entries []treeEntry
for i < len(data) {
modeEnd := bytes.IndexByte(data[i:], ' ')
if modeEnd < 0 {
return errors.New("invalid tree format")
}
mode := string(data[i : i+modeEnd])
i += modeEnd + 1
nameEnd := bytes.IndexByte(data[i:], 0)
if nameEnd < 0 {
return errors.New("missing null after filename")
}
name := string(data[i : i+nameEnd])
i += nameEnd + 1
if i+20 > len(data) {
return errors.New("unexpected EOF in SHA")
}
shaBytes := data[i : i+20]
i += 20
entries = append(entries, treeEntry{
mode: mode,
name: name,
sha: shaBytes,
})
if mode == "40000" {
subPrefix := path.Join(prefix, name)
if err := walk(subPrefix, hex.EncodeToString(shaBytes)); err != nil {
return err
}
}
}
treeCache[prefix] = entries
return nil
}
if err := walk("", baseTree); err != nil {
return "", err
}
for filePath, blobSha := range updates {
parts := strings.Split(filePath, "/")
dir := strings.Join(parts[:len(parts)-1], "/")
name := parts[len(parts)-1]
entries := treeCache[dir]
found := false
for i, e := range entries {
if e.name == name {
if blobSha == nil {
// Remove TODO
entries = append(entries[:i], entries[i+1:]...)
} else {
entries[i].sha = blobSha
}
found = true
break
}
}
if !found && blobSha != nil {
entries = append(entries, treeEntry{
mode: "100644",
name: name,
sha: blobSha,
})
}
treeCache[dir] = entries
}
built := make(map[string][]byte)
var build func(string) ([]byte, error)
build = func(prefix string) ([]byte, error) {
entries := treeCache[prefix]
for i, e := range entries {
if e.mode == "40000" {
subPrefix := path.Join(prefix, e.name)
if sha, ok := built[subPrefix]; ok {
entries[i].sha = sha
continue
}
newShaStr, err := build(subPrefix)
if err != nil {
return nil, err
}
entries[i].sha = newShaStr
}
}
shaStr, err := writeTree(ctx, repoPath, entries)
if err != nil {
return nil, err
}
shaBytes, err := hex.DecodeString(shaStr)
if err != nil {
return nil, err
}
built[prefix] = shaBytes
return shaBytes, nil
}
rootShaBytes, err := build("")
if err != nil {
return "", err
}
return hex.EncodeToString(rootShaBytes), nil
}
type treeEntry struct {
mode string // like "100644"
name string // individual name
sha []byte
}
// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu <https://runxiyu.org> package main import ( "bytes"
// "crypto/rand" // "fmt"
"crypto/rand" "encoding/hex"
"os" "os/exec"
"strings" "time"
"github.com/bluekeyes/go-gitdiff/gitdiff" "github.com/emersion/go-message" "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object"
)
func lmtpHandlePatch(session *lmtpSession, groupPath []string, repoName string, email *message.Entity) (err error) {
var diffFiles []*gitdiff.File
var preamble string
if diffFiles, preamble, err = gitdiff.Parse(email.Body); err != nil {
return
}
var header *gitdiff.PatchHeader
if header, err = gitdiff.ParsePatchHeader(preamble); err != nil {
return
}
var repo *git.Repository
var fsPath string
repo, _, _, fsPath, err = openRepo(session.ctx, groupPath, repoName)
if err != nil {
return
}
var headRef *plumbing.Reference
if headRef, err = repo.Head(); err != nil {
headRef, err := repo.Head()
if err != nil {
return }
var headCommit *object.Commit
if headCommit, err = repo.CommitObject(headRef.Hash()); err != nil {
headCommit, err := repo.CommitObject(headRef.Hash())
if err != nil {
return }
var headTree *object.Tree
if headTree, err = headCommit.Tree(); err != nil {
headTree, err := headCommit.Tree()
if err != nil {
return }
// TODO: Try to not shell out
headTreeHash := headTree.Hash.String()
blobUpdates := make(map[string][]byte)
for _, diffFile := range diffFiles {
var sourceFile *object.File
if sourceFile, err = headTree.File(diffFile.OldName); err != nil {
sourceFile, err := headTree.File(diffFile.OldName)
if err != nil {
return err }
var sourceString string
if sourceString, err = sourceFile.Contents(); err != nil {
sourceString, err := sourceFile.Contents()
if err != nil {
return err }
hashBuf := bytes.Buffer{}
patchedBuf := bytes.Buffer{}
sourceBuf := bytes.NewReader(stringToBytes(sourceString))
if err = gitdiff.Apply(&patchedBuf, sourceBuf, diffFile); err != nil {
var patchedBuf bytes.Buffer
if err := gitdiff.Apply(&patchedBuf, sourceBuf, diffFile); err != nil {
return err }
proc := exec.CommandContext(session.ctx, "git", "hash-object", "-w", "-t", "blob", "--stdin")
proc.Env = append(os.Environ(), "GIT_DIR="+fsPath)
proc.Stdout = &hashBuf
proc.Stdin = &patchedBuf
if err = proc.Start(); err != nil {
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 err }
if err = proc.Wait(); err != nil {
newHashStr := strings.TrimSpace(hashBuf.String())
newHash, err := hex.DecodeString(newHashStr)
if err != nil {
return err }
newHash := hashBuf.Bytes()
if len(newHash) != 20*2+1 { // TODO: Hardcoded from the size of plumbing.Hash
panic("unexpected hash size")
blobUpdates[diffFile.NewName] = newHash
if diffFile.NewName != diffFile.OldName {
blobUpdates[diffFile.OldName] = nil // Mark for deletion.
}
// TODO: Add to tree
}
// contribBranchName := rand.Text()
newTreeSha, err := buildTreeRecursive(session.ctx, fsPath, headTreeHash, blobUpdates)
if err != nil {
return err
}
// TODO: Store the branch
commitMsg := header.Title
if header.Body != "" {
commitMsg += "\n\n" + header.Body
}
// fmt.Println(repo, diffFiles, preamble) _ = preamble
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 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 err
}
return nil }