From 352fb70e52a9a863e0818ee350aac5d9fe048e3b Mon Sep 17 00:00:00 2001 From: oppiliappan Date: Wed, 1 Oct 2025 17:22:20 +0100 Subject: [PATCH] knotserver/git: write change-id headers into git objects Change-Id: mqqpssvxtxomtxouptrnlqrpyyoqukqs after applying a patch series, write the change-id headers into the git commit objects itself. Signed-off-by: oppiliappan --- knotserver/git/merge.go | 175 +++++++++++++++++++++++++++------ knotserver/xrpc/merge.go | 2 +- knotserver/xrpc/merge_check.go | 2 +- 3 files changed, 146 insertions(+), 33 deletions(-) diff --git a/knotserver/git/merge.go b/knotserver/git/merge.go index 1085ece2..ff6511b7 100644 --- a/knotserver/git/merge.go +++ b/knotserver/git/merge.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/sha256" "fmt" + "log" "os" "os/exec" "regexp" @@ -12,6 +13,8 @@ import ( "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 { @@ -162,51 +165,51 @@ func (g *GitRepo) checkPatch(tmpDir, patchFile string) error { 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 { @@ -216,7 +219,112 @@ func (g *GitRepo) applyPatch(tmpDir, patchFile string, opts MergeOptions) error 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 } @@ -263,7 +371,12 @@ func (g *GitRepo) MergeWithOptions(patchData string, targetBranch string, opts M } 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 } diff --git a/knotserver/xrpc/merge.go b/knotserver/xrpc/merge.go index b3fb2687..6bbf5464 100644 --- a/knotserver/xrpc/merge.go +++ b/knotserver/xrpc/merge.go @@ -85,7 +85,7 @@ func (x *Xrpc) Merge(w http.ResponseWriter, r *http.Request) { 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) { diff --git a/knotserver/xrpc/merge_check.go b/knotserver/xrpc/merge_check.go index 2994e92b..5a0f45ca 100644 --- a/knotserver/xrpc/merge_check.go +++ b/knotserver/xrpc/merge_check.go @@ -51,7 +51,7 @@ func (x *Xrpc) MergeCheck(w http.ResponseWriter, r *http.Request) { return } - err = gr.MergeCheck([]byte(data.Patch), data.Branch) + err = gr.MergeCheck(data.Patch, data.Branch) response := tangled.RepoMergeCheck_Output{ Is_conflicted: false, -- 2.43.0