From c32389d7d54f3fe66d32f849c02c5e75b7d476c8 Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Wed, 02 Apr 2025 08:49:03 +0800 Subject: [PATCH] LMTP: Actually apply patches from email --- git_plumbing.go | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++ lmtp_handle_patch.go | 108 ++++++++++++++++++++++++++++++++++++----------------- diff --git a/git_plumbing.go b/git_plumbing.go new file mode 100644 index 0000000000000000000000000000000000000000..36acb902b8993b1180169f4481b9513f067c4387 --- /dev/null +++ b/git_plumbing.go @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// SPDX-FileCopyrightText: Copyright (c) 2025 Runxi Yu + +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 138b5927e593e37b48d29075aa4f229fe30b7c84..d69424b512c3ec2a890be0a03707f018e36b0800 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 @@ 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) @@ -31,61 +36,96 @@ 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 } -- 2.48.1