Mirror of @tangled.org/core. Running on a Raspberry Pi Zero 2

knotserver/git: rework merge check to also use `git am`

we were using `git apply` in merge check and `git am` for the actual
merge, but in reality, there are slight behavior changes among the two.
this change switches out `git apply` in `mergeCheck` to use `git am`
when dealing with mailbox style patches.

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

authored by oppi.li and committed by tangled.org 86081872 0fd947f3

+37 -31
+30 -30
knotserver/git/merge.go
··· 107 107 return fmt.Sprintf("merge failed: %s", e.Message) 108 108 } 109 109 110 - func (g *GitRepo) createTempFileWithPatch(patchData string) (string, error) { 110 + func createTemp(data string) (string, error) { 111 111 tmpFile, err := os.CreateTemp("", "git-patch-*.patch") 112 112 if err != nil { 113 113 return "", fmt.Errorf("failed to create temporary patch file: %w", err) 114 114 } 115 115 116 - if _, err := tmpFile.Write([]byte(patchData)); err != nil { 116 + if _, err := tmpFile.Write([]byte(data)); err != nil { 117 117 tmpFile.Close() 118 118 os.Remove(tmpFile.Name()) 119 119 return "", fmt.Errorf("failed to write patch data to temporary file: %w", err) ··· 127 127 return tmpFile.Name(), nil 128 128 } 129 129 130 - func (g *GitRepo) cloneRepository(targetBranch string) (string, error) { 130 + func (g *GitRepo) cloneTemp(targetBranch string) (string, error) { 131 131 tmpDir, err := os.MkdirTemp("", "git-clone-") 132 132 if err != nil { 133 133 return "", fmt.Errorf("failed to create temporary directory: %w", err) ··· 147 147 return tmpDir, nil 148 148 } 149 149 150 - func (g *GitRepo) checkPatch(tmpDir, patchFile string) error { 151 - var stderr bytes.Buffer 152 - 153 - cmd := exec.Command("git", "-C", tmpDir, "apply", "--check", "-v", patchFile) 154 - cmd.Stderr = &stderr 155 - 156 - if err := cmd.Run(); err != nil { 157 - conflicts := parseGitApplyErrors(stderr.String()) 158 - return &ErrMerge{ 159 - Message: "patch cannot be applied cleanly", 160 - Conflicts: conflicts, 161 - HasConflict: len(conflicts) > 0, 162 - OtherError: err, 163 - } 164 - } 165 - return nil 166 - } 167 - 168 150 func (g *GitRepo) applyPatch(patchData, patchFile string, opts MergeOptions) error { 169 151 var stderr bytes.Buffer 170 152 var cmd *exec.Cmd ··· 155 173 exec.Command("git", "-C", g.path, "config", "user.name", opts.CommitterName).Run() 156 174 exec.Command("git", "-C", g.path, "config", "user.email", opts.CommitterEmail).Run() 157 175 exec.Command("git", "-C", g.path, "config", "advice.mergeConflict", "false").Run() 176 + exec.Command("git", "-C", g.path, "config", "advice.amWorkDir", "false").Run() 158 177 159 178 // if patch is a format-patch, apply using 'git am' 160 179 if opts.FormatPatch { ··· 196 213 cmd.Stderr = &stderr 197 214 198 215 if err := cmd.Run(); err != nil { 199 - return fmt.Errorf("patch application failed: %s", stderr.String()) 216 + conflicts := parseGitApplyErrors(stderr.String()) 217 + return &ErrMerge{ 218 + Message: "patch cannot be applied cleanly", 219 + Conflicts: conflicts, 220 + HasConflict: len(conflicts) > 0, 221 + OtherError: err, 222 + } 200 223 } 201 224 202 225 return nil ··· 230 241 } 231 242 232 243 func (g *GitRepo) applySingleMailbox(singlePatch types.FormatPatch) (plumbing.Hash, error) { 233 - tmpPatch, err := g.createTempFileWithPatch(singlePatch.Raw) 244 + tmpPatch, err := createTemp(singlePatch.Raw) 234 245 if err != nil { 235 246 return plumbing.ZeroHash, fmt.Errorf("failed to create temporary patch file for singluar mailbox patch: %w", err) 236 247 } ··· 246 257 log.Println("head before apply", head.Hash().String()) 247 258 248 259 if err := cmd.Run(); err != nil { 249 - return plumbing.ZeroHash, fmt.Errorf("patch application failed: %s", stderr.String()) 260 + conflicts := parseGitApplyErrors(stderr.String()) 261 + return plumbing.ZeroHash, &ErrMerge{ 262 + Message: "patch cannot be applied cleanly", 263 + Conflicts: conflicts, 264 + HasConflict: len(conflicts) > 0, 265 + OtherError: err, 266 + } 250 267 } 251 268 252 269 if err := g.Refresh(); err != nil { ··· 319 324 return newHash, nil 320 325 } 321 326 322 - func (g *GitRepo) MergeCheck(patchData string, targetBranch string) error { 327 + func (g *GitRepo) MergeCheckWithOptions(patchData string, targetBranch string, mo MergeOptions) error { 323 328 if val, ok := mergeCheckCache.Get(g, patchData, targetBranch); ok { 324 329 return val 325 330 } 326 331 327 - patchFile, err := g.createTempFileWithPatch(patchData) 332 + patchFile, err := createTemp(patchData) 328 333 if err != nil { 329 334 return &ErrMerge{ 330 335 Message: err.Error(), ··· 333 338 } 334 339 defer os.Remove(patchFile) 335 340 336 - tmpDir, err := g.cloneRepository(targetBranch) 341 + tmpDir, err := g.cloneTemp(targetBranch) 337 342 if err != nil { 338 343 return &ErrMerge{ 339 344 Message: err.Error(), ··· 342 347 } 343 348 defer os.RemoveAll(tmpDir) 344 349 345 - result := g.checkPatch(tmpDir, patchFile) 350 + tmpRepo, err := PlainOpen(tmpDir) 351 + if err != nil { 352 + return err 353 + } 354 + 355 + result := tmpRepo.applyPatch(patchData, patchFile, mo) 346 356 mergeCheckCache.Set(g, patchData, targetBranch, result) 347 357 return result 348 358 } 349 359 350 360 func (g *GitRepo) MergeWithOptions(patchData string, targetBranch string, opts MergeOptions) error { 351 - patchFile, err := g.createTempFileWithPatch(patchData) 361 + patchFile, err := createTemp(patchData) 352 362 if err != nil { 353 363 return &ErrMerge{ 354 364 Message: err.Error(), ··· 362 362 } 363 363 defer os.Remove(patchFile) 364 364 365 - tmpDir, err := g.cloneRepository(targetBranch) 365 + tmpDir, err := g.cloneTemp(targetBranch) 366 366 if err != nil { 367 367 return &ErrMerge{ 368 368 Message: err.Error(),
+7 -1
knotserver/xrpc/merge_check.go
··· 9 9 securejoin "github.com/cyphar/filepath-securejoin" 10 10 "tangled.org/core/api/tangled" 11 11 "tangled.org/core/knotserver/git" 12 + "tangled.org/core/patchutil" 12 13 xrpcerr "tangled.org/core/xrpc/errors" 13 14 ) 14 15 ··· 52 51 return 53 52 } 54 53 55 - err = gr.MergeCheck(data.Patch, data.Branch) 54 + mo := git.MergeOptions{} 55 + mo.CommitterName = x.Config.Git.UserName 56 + mo.CommitterEmail = x.Config.Git.UserEmail 57 + mo.FormatPatch = patchutil.IsFormatPatch(data.Patch) 58 + 59 + err = gr.MergeCheckWithOptions(data.Patch, data.Branch, mo) 56 60 57 61 response := tangled.RepoMergeCheck_Output{ 58 62 Is_conflicted: false,