aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRunxi Yu <me@runxiyu.org>2025-04-02 08:49:03 +0800
committerRunxi Yu <me@runxiyu.org>2025-04-02 09:24:37 +0800
commitc32389d7d54f3fe66d32f849c02c5e75b7d476c8 (patch)
tree6a28b81c35e6ac602cb10ca24869b8bc2342134c
parentLMTP: Fix mistake in command arguments (diff)
downloadforge-c32389d7d54f3fe66d32f849c02c5e75b7d476c8.tar.gz
forge-c32389d7d54f3fe66d32f849c02c5e75b7d476c8.tar.zst
forge-c32389d7d54f3fe66d32f849c02c5e75b7d476c8.zip
LMTP: Actually apply patches from email
-rw-r--r--git_plumbing.go173
-rw-r--r--lmtp_handle_patch.go108
2 files changed, 247 insertions, 34 deletions
diff --git a/git_plumbing.go b/git_plumbing.go
new file mode 100644
index 0000000..36acb90
--- /dev/null
+++ b/git_plumbing.go
@@ -0,0 +1,173 @@
+// 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
+}
diff --git a/lmtp_handle_patch.go b/lmtp_handle_patch.go
index 138b592..d69424b 100644
--- a/lmtp_handle_patch.go
+++ b/lmtp_handle_patch.go
@@ -5,16 +5,16 @@ 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) {
@@ -24,6 +24,11 @@ func lmtpHandlePatch(session *lmtpSession, groupPath []string, repoName string,
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)
@@ -31,61 +36,96 @@ func lmtpHandlePatch(session *lmtpSession, groupPath []string, repoName string,
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
+ }
+
+ 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
- // TODO: Store the branch
+ var commitOut bytes.Buffer
+ commitCmd.Stdout = &commitOut
+ if err := commitCmd.Run(); err != nil {
+ return err
+ }
+ newCommitSha := strings.TrimSpace(commitOut.String())
- // fmt.Println(repo, diffFiles, preamble)
- _ = preamble
+ 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
}