+57
-4
knotserver/git/merge.go
+57
-4
knotserver/git/merge.go
···
24
Reason string
25
}
26
27
func (e ErrMerge) Error() string {
28
if e.HasConflict {
29
return fmt.Sprintf("merge failed due to conflicts: %s (%d conflicts)", e.Message, len(e.Conflicts))
···
74
return tmpDir, nil
75
}
76
77
-
func (g *GitRepo) applyPatch(tmpDir, patchFile string, checkOnly bool) error {
78
var stderr bytes.Buffer
79
var cmd *exec.Cmd
80
···
82
cmd = exec.Command("git", "-C", tmpDir, "apply", "--check", "-v", patchFile)
83
} else {
84
exec.Command("git", "-C", tmpDir, "config", "advice.mergeConflict", "false").Run()
85
-
cmd = exec.Command("git", "-C", tmpDir, "am", patchFile)
86
}
87
88
cmd.Stderr = &stderr
···
122
}
123
defer os.RemoveAll(tmpDir)
124
125
-
return g.applyPatch(tmpDir, patchFile, true)
126
}
127
128
func (g *GitRepo) Merge(patchData []byte, targetBranch string) error {
129
patchFile, err := g.createTempFileWithPatch(patchData)
130
if err != nil {
131
return &ErrMerge{
···
144
}
145
defer os.RemoveAll(tmpDir)
146
147
-
if err := g.applyPatch(tmpDir, patchFile, false); err != nil {
148
return err
149
}
150
···
24
Reason string
25
}
26
27
+
// MergeOptions specifies the configuration for a merge operation
28
+
type MergeOptions struct {
29
+
CommitMessage string
30
+
CommitBody string
31
+
AuthorName string
32
+
AuthorEmail string
33
+
}
34
+
35
func (e ErrMerge) Error() string {
36
if e.HasConflict {
37
return fmt.Sprintf("merge failed due to conflicts: %s (%d conflicts)", e.Message, len(e.Conflicts))
···
82
return tmpDir, nil
83
}
84
85
+
func (g *GitRepo) applyPatch(tmpDir, patchFile string, checkOnly bool, opts *MergeOptions) error {
86
var stderr bytes.Buffer
87
var cmd *exec.Cmd
88
···
90
cmd = exec.Command("git", "-C", tmpDir, "apply", "--check", "-v", patchFile)
91
} else {
92
exec.Command("git", "-C", tmpDir, "config", "advice.mergeConflict", "false").Run()
93
+
94
+
if opts != nil {
95
+
applyCmd := exec.Command("git", "-C", tmpDir, "apply", patchFile)
96
+
applyCmd.Stderr = &stderr
97
+
if err := applyCmd.Run(); err != nil {
98
+
return fmt.Errorf("patch application failed: %s", stderr.String())
99
+
}
100
+
101
+
stageCmd := exec.Command("git", "-C", tmpDir, "add", ".")
102
+
if err := stageCmd.Run(); err != nil {
103
+
return fmt.Errorf("failed to stage changes: %w", err)
104
+
}
105
+
106
+
commitArgs := []string{"-C", tmpDir, "commit"}
107
+
108
+
// Set author if provided
109
+
authorName := opts.AuthorName
110
+
authorEmail := opts.AuthorEmail
111
+
112
+
if authorEmail == "" {
113
+
authorEmail = "noreply@tangled.sh"
114
+
}
115
+
116
+
if authorName == "" {
117
+
authorName = "Tangled"
118
+
}
119
+
120
+
if authorName != "" {
121
+
commitArgs = append(commitArgs, "--author", fmt.Sprintf("%s <%s>", authorName, authorEmail))
122
+
}
123
+
124
+
commitArgs = append(commitArgs, "-m", opts.CommitMessage)
125
+
126
+
if opts.CommitBody != "" {
127
+
commitArgs = append(commitArgs, "-m", opts.CommitBody)
128
+
}
129
+
130
+
cmd = exec.Command("git", commitArgs...)
131
+
} else {
132
+
// If no commit message specified, use git-am which automatically creates a commit
133
+
cmd = exec.Command("git", "-C", tmpDir, "am", patchFile)
134
+
}
135
}
136
137
cmd.Stderr = &stderr
···
171
}
172
defer os.RemoveAll(tmpDir)
173
174
+
return g.applyPatch(tmpDir, patchFile, true, nil)
175
}
176
177
func (g *GitRepo) Merge(patchData []byte, targetBranch string) error {
178
+
return g.MergeWithOptions(patchData, targetBranch, nil)
179
+
}
180
+
181
+
func (g *GitRepo) MergeWithOptions(patchData []byte, targetBranch string, opts *MergeOptions) error {
182
patchFile, err := g.createTempFileWithPatch(patchData)
183
if err != nil {
184
return &ErrMerge{
···
197
}
198
defer os.RemoveAll(tmpDir)
199
200
+
if err := g.applyPatch(tmpDir, patchFile, false, opts); err != nil {
201
return err
202
}
203
+9
-5
knotserver/routes.go
+9
-5
knotserver/routes.go
···
559
func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) {
560
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
561
562
-
var data struct {
563
-
Patch string `json:"patch"`
564
-
Branch string `json:"branch"`
565
-
}
566
567
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
568
writeError(w, err.Error(), http.StatusBadRequest)
···
570
return
571
}
572
573
patch := data.Patch
574
branch := data.Branch
575
gr, err := git.Open(path, branch)
···
577
notFound(w)
578
return
579
}
580
-
if err := gr.Merge([]byte(patch), branch); err != nil {
581
var mergeErr *git.ErrMerge
582
if errors.As(err, &mergeErr) {
583
conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
···
559
func (h *Handle) Merge(w http.ResponseWriter, r *http.Request) {
560
path, _ := securejoin.SecureJoin(h.c.Repo.ScanPath, didPath(r))
561
562
+
data := types.MergeRequest{}
563
564
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
565
writeError(w, err.Error(), http.StatusBadRequest)
···
567
return
568
}
569
570
+
mo := &git.MergeOptions{
571
+
AuthorName: data.AuthorName,
572
+
AuthorEmail: data.AuthorEmail,
573
+
CommitBody: data.CommitBody,
574
+
CommitMessage: data.CommitMessage,
575
+
}
576
+
577
patch := data.Patch
578
branch := data.Branch
579
gr, err := git.Open(path, branch)
···
581
notFound(w)
582
return
583
}
584
+
if err := gr.MergeWithOptions([]byte(patch), branch, mo); err != nil {
585
var mergeErr *git.ErrMerge
586
if errors.As(err, &mergeErr) {
587
conflicts := make([]types.ConflictInfo, len(mergeErr.Conflicts))
+9
types/merge.go
+9
types/merge.go
···
11
Message string `json:"message"`
12
Error string `json:"error"`
13
}
14
+
15
+
type MergeRequest struct {
16
+
Patch string `json:"patch"`
17
+
AuthorName string `json:"authorName,omitempty"`
18
+
AuthorEmail string `json:"authorEmail,omitempty"`
19
+
CommitBody string `json:"commitBody,omitempty"`
20
+
CommitMessage string `json:"commitMessage,omitempty"`
21
+
Branch string `json:"branch"`
22
+
}