knotserver/git: write change-id headers into git objects #642

merged
opened by oppi.li targeting master from push-rvtqynpmozzy

after applying a patch series, write the change-id headers into the git commit objects itself.

Signed-off-by: oppiliappan me@oppi.li

Changed files
+146 -33
knotserver
+144 -31
knotserver/git/merge.go
···
"bytes"
"crypto/sha256"
"fmt"
+
"log"
"os"
"os/exec"
"regexp"
···
"github.com/dgraph-io/ristretto"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
+
"tangled.org/core/patchutil"
+
"tangled.org/core/types"
)
type MergeCheckCache struct {
···
return nil
}
-
func (g *GitRepo) applyPatch(tmpDir, patchFile string, opts MergeOptions) error {
+
func (g *GitRepo) applyPatch(patchData, patchFile string, opts MergeOptions) error {
var stderr bytes.Buffer
var cmd *exec.Cmd
// configure default git user before merge
-
exec.Command("git", "-C", tmpDir, "config", "user.name", opts.CommitterName).Run()
-
exec.Command("git", "-C", tmpDir, "config", "user.email", opts.CommitterEmail).Run()
-
exec.Command("git", "-C", tmpDir, "config", "advice.mergeConflict", "false").Run()
+
exec.Command("git", "-C", g.path, "config", "user.name", opts.CommitterName).Run()
+
exec.Command("git", "-C", g.path, "config", "user.email", opts.CommitterEmail).Run()
+
exec.Command("git", "-C", g.path, "config", "advice.mergeConflict", "false").Run()
// if patch is a format-patch, apply using 'git am'
if opts.FormatPatch {
-
cmd = exec.Command("git", "-C", tmpDir, "am", patchFile)
-
} else {
-
// else, apply using 'git apply' and commit it manually
-
applyCmd := exec.Command("git", "-C", tmpDir, "apply", patchFile)
-
applyCmd.Stderr = &stderr
-
if err := applyCmd.Run(); err != nil {
-
return fmt.Errorf("patch application failed: %s", stderr.String())
-
}
+
return g.applyMailbox(patchData)
+
}
-
stageCmd := exec.Command("git", "-C", tmpDir, "add", ".")
-
if err := stageCmd.Run(); err != nil {
-
return fmt.Errorf("failed to stage changes: %w", err)
-
}
+
// else, apply using 'git apply' and commit it manually
+
applyCmd := exec.Command("git", "-C", g.path, "apply", patchFile)
+
applyCmd.Stderr = &stderr
+
if err := applyCmd.Run(); err != nil {
+
return fmt.Errorf("patch application failed: %s", stderr.String())
+
}
-
commitArgs := []string{"-C", tmpDir, "commit"}
+
stageCmd := exec.Command("git", "-C", g.path, "add", ".")
+
if err := stageCmd.Run(); err != nil {
+
return fmt.Errorf("failed to stage changes: %w", err)
+
}
-
// Set author if provided
-
authorName := opts.AuthorName
-
authorEmail := opts.AuthorEmail
+
commitArgs := []string{"-C", g.path, "commit"}
-
if authorName != "" && authorEmail != "" {
-
commitArgs = append(commitArgs, "--author", fmt.Sprintf("%s <%s>", authorName, authorEmail))
-
}
-
// else, will default to knot's global user.name & user.email configured via `KNOT_GIT_USER_*` env variables
+
// Set author if provided
+
authorName := opts.AuthorName
+
authorEmail := opts.AuthorEmail
-
commitArgs = append(commitArgs, "-m", opts.CommitMessage)
+
if authorName != "" && authorEmail != "" {
+
commitArgs = append(commitArgs, "--author", fmt.Sprintf("%s <%s>", authorName, authorEmail))
+
}
+
// else, will default to knot's global user.name & user.email configured via `KNOT_GIT_USER_*` env variables
-
if opts.CommitBody != "" {
-
commitArgs = append(commitArgs, "-m", opts.CommitBody)
-
}
+
commitArgs = append(commitArgs, "-m", opts.CommitMessage)
-
cmd = exec.Command("git", commitArgs...)
+
if opts.CommitBody != "" {
+
commitArgs = append(commitArgs, "-m", opts.CommitBody)
}
+
cmd = exec.Command("git", commitArgs...)
+
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
···
return nil
}
-
func (g *GitRepo) MergeCheck(patchData []byte, targetBranch string) error {
+
func (g *GitRepo) applyMailbox(patchData string) error {
+
fps, err := patchutil.ExtractPatches(patchData)
+
if err != nil {
+
return fmt.Errorf("failed to extract patches: %w", err)
+
}
+
+
// apply each patch one by one
+
// update the newly created commit object to add the change-id header
+
total := len(fps)
+
for i, p := range fps {
+
newCommit, err := g.applySingleMailbox(p)
+
if err != nil {
+
return err
+
}
+
+
log.Printf("applying mailbox patch %d/%d: committed %s\n", i+1, total, newCommit.String())
+
}
+
+
return nil
+
}
+
+
func (g *GitRepo) applySingleMailbox(singlePatch types.FormatPatch) (plumbing.Hash, error) {
+
tmpPatch, err := g.createTempFileWithPatch(singlePatch.Raw)
+
if err != nil {
+
return plumbing.ZeroHash, fmt.Errorf("failed to create temporary patch file for singluar mailbox patch: %w", err)
+
}
+
+
var stderr bytes.Buffer
+
cmd := exec.Command("git", "-C", g.path, "am", tmpPatch)
+
cmd.Stderr = &stderr
+
+
head, err := g.r.Head()
+
if err != nil {
+
return plumbing.ZeroHash, err
+
}
+
log.Println("head before apply", head.Hash().String())
+
+
if err := cmd.Run(); err != nil {
+
return plumbing.ZeroHash, fmt.Errorf("patch application failed: %s", stderr.String())
+
}
+
+
if err := g.Refresh(); err != nil {
+
return plumbing.ZeroHash, fmt.Errorf("failed to refresh repository state: %w", err)
+
}
+
+
head, err = g.r.Head()
+
if err != nil {
+
return plumbing.ZeroHash, err
+
}
+
log.Println("head after apply", head.Hash().String())
+
+
newHash := head.Hash()
+
if changeId, err := singlePatch.ChangeId(); err != nil {
+
// no change ID
+
} else if updatedHash, err := g.setChangeId(head.Hash(), changeId); err != nil {
+
return plumbing.ZeroHash, err
+
} else {
+
newHash = updatedHash
+
}
+
+
return newHash, nil
+
}
+
+
func (g *GitRepo) setChangeId(hash plumbing.Hash, changeId string) (plumbing.Hash, error) {
+
log.Printf("updating change ID of %s to %s\n", hash.String(), changeId)
+
obj, err := g.r.CommitObject(hash)
+
if err != nil {
+
return plumbing.ZeroHash, fmt.Errorf("failed to get commit object for hash %s: %w", hash.String(), err)
+
}
+
+
// write the change-id header
+
obj.ExtraHeaders["change-id"] = []byte(changeId)
+
+
// create a new object
+
dest := g.r.Storer.NewEncodedObject()
+
if err := obj.Encode(dest); err != nil {
+
return plumbing.ZeroHash, fmt.Errorf("failed to create new object: %w", err)
+
}
+
+
// store the new object
+
newHash, err := g.r.Storer.SetEncodedObject(dest)
+
if err != nil {
+
return plumbing.ZeroHash, fmt.Errorf("failed to store new object: %w", err)
+
}
+
+
log.Printf("hash changed from %s to %s\n", obj.Hash.String(), newHash.String())
+
+
// find the branch that HEAD is pointing to
+
ref, err := g.r.Head()
+
if err != nil {
+
return plumbing.ZeroHash, fmt.Errorf("failed to fetch HEAD: %w", err)
+
}
+
+
// and update that branch to point to new commit
+
if ref.Name().IsBranch() {
+
err = g.r.Storer.SetReference(plumbing.NewHashReference(ref.Name(), newHash))
+
if err != nil {
+
return plumbing.ZeroHash, fmt.Errorf("failed to update HEAD: %w", err)
+
}
+
}
+
+
// new hash of commit
+
return newHash, nil
+
}
+
+
func (g *GitRepo) MergeCheck(patchData string, targetBranch string) error {
if val, ok := mergeCheckCache.Get(g, patchData, targetBranch); ok {
return val
}
···
}
defer os.RemoveAll(tmpDir)
-
if err := g.applyPatch(tmpDir, patchFile, opts); err != nil {
+
tmpRepo, err := PlainOpen(tmpDir)
+
if err != nil {
+
return err
+
}
+
+
if err := tmpRepo.applyPatch(patchData, patchFile, opts); err != nil {
return err
}
+1 -1
knotserver/xrpc/merge.go
···
mo.CommitterEmail = x.Config.Git.UserEmail
mo.FormatPatch = patchutil.IsFormatPatch(data.Patch)
-
err = gr.MergeWithOptions([]byte(data.Patch), data.Branch, mo)
+
err = gr.MergeWithOptions(data.Patch, data.Branch, mo)
if err != nil {
var mergeErr *git.ErrMerge
if errors.As(err, &mergeErr) {
+1 -1
knotserver/xrpc/merge_check.go
···
return
}
-
err = gr.MergeCheck([]byte(data.Patch), data.Branch)
+
err = gr.MergeCheck(data.Patch, data.Branch)
response := tangled.RepoMergeCheck_Output{
Is_conflicted: false,