Signed-off-by: oppiliappan me@oppi.li
+706
knotserver/git/merge_test.go
+706
knotserver/git/merge_test.go
···
1
+
package git
2
+
3
+
import (
4
+
"os"
5
+
"path/filepath"
6
+
"strings"
7
+
"testing"
8
+
9
+
"github.com/go-git/go-git/v5"
10
+
"github.com/go-git/go-git/v5/config"
11
+
"github.com/go-git/go-git/v5/plumbing"
12
+
"github.com/go-git/go-git/v5/plumbing/object"
13
+
"github.com/stretchr/testify/assert"
14
+
"github.com/stretchr/testify/require"
15
+
)
16
+
17
+
type Helper struct {
18
+
t *testing.T
19
+
tempDir string
20
+
repo *GitRepo
21
+
}
22
+
23
+
func helper(t *testing.T) *Helper {
24
+
tempDir, err := os.MkdirTemp("", "git-merge-test-*")
25
+
require.NoError(t, err)
26
+
27
+
return &Helper{
28
+
t: t,
29
+
tempDir: tempDir,
30
+
}
31
+
}
32
+
33
+
func (h *Helper) cleanup() {
34
+
if h.tempDir != "" {
35
+
os.RemoveAll(h.tempDir)
36
+
}
37
+
}
38
+
39
+
// initRepo initializes a git repository with an initial commit
40
+
func (h *Helper) initRepo() *GitRepo {
41
+
repoPath := filepath.Join(h.tempDir, "test-repo")
42
+
43
+
// initialize repository
44
+
r, err := git.PlainInit(repoPath, false)
45
+
require.NoError(h.t, err)
46
+
47
+
// configure git user
48
+
cfg, err := r.Config()
49
+
require.NoError(h.t, err)
50
+
cfg.User.Name = "Test User"
51
+
cfg.User.Email = "test@example.com"
52
+
err = r.SetConfig(cfg)
53
+
require.NoError(h.t, err)
54
+
55
+
// create initial commit with a file
56
+
w, err := r.Worktree()
57
+
require.NoError(h.t, err)
58
+
59
+
// create initial file
60
+
initialFile := filepath.Join(repoPath, "README.md")
61
+
err = os.WriteFile(initialFile, []byte("# Test Repository\n\nInitial content.\n"), 0644)
62
+
require.NoError(h.t, err)
63
+
64
+
_, err = w.Add("README.md")
65
+
require.NoError(h.t, err)
66
+
67
+
_, err = w.Commit("Initial commit", &git.CommitOptions{
68
+
Author: &object.Signature{
69
+
Name: "Test User",
70
+
Email: "test@example.com",
71
+
},
72
+
})
73
+
require.NoError(h.t, err)
74
+
75
+
gitRepo, err := PlainOpen(repoPath)
76
+
require.NoError(h.t, err)
77
+
78
+
h.repo = gitRepo
79
+
return gitRepo
80
+
}
81
+
82
+
// addFile creates a file in the repository
83
+
func (h *Helper) addFile(filename, content string) {
84
+
filePath := filepath.Join(h.repo.path, filename)
85
+
dir := filepath.Dir(filePath)
86
+
87
+
err := os.MkdirAll(dir, 0755)
88
+
require.NoError(h.t, err)
89
+
90
+
err = os.WriteFile(filePath, []byte(content), 0644)
91
+
require.NoError(h.t, err)
92
+
}
93
+
94
+
// commitFile adds and commits a file
95
+
func (h *Helper) commitFile(filename, content, message string) plumbing.Hash {
96
+
h.addFile(filename, content)
97
+
98
+
w, err := h.repo.r.Worktree()
99
+
require.NoError(h.t, err)
100
+
101
+
_, err = w.Add(filename)
102
+
require.NoError(h.t, err)
103
+
104
+
hash, err := w.Commit(message, &git.CommitOptions{
105
+
Author: &object.Signature{
106
+
Name: "Test User",
107
+
Email: "test@example.com",
108
+
},
109
+
})
110
+
require.NoError(h.t, err)
111
+
112
+
return hash
113
+
}
114
+
115
+
// readFile reads a file from the repository
116
+
func (h *Helper) readFile(filename string) string {
117
+
content, err := os.ReadFile(filepath.Join(h.repo.path, filename))
118
+
require.NoError(h.t, err)
119
+
return string(content)
120
+
}
121
+
122
+
// fileExists checks if a file exists in the repository
123
+
func (h *Helper) fileExists(filename string) bool {
124
+
_, err := os.Stat(filepath.Join(h.repo.path, filename))
125
+
return err == nil
126
+
}
127
+
128
+
func TestApplyPatch_Success(t *testing.T) {
129
+
h := helper(t)
130
+
defer h.cleanup()
131
+
132
+
repo := h.initRepo()
133
+
134
+
// modify README.md
135
+
patch := `diff --git a/README.md b/README.md
136
+
index 1234567..abcdefg 100644
137
+
--- a/README.md
138
+
+++ b/README.md
139
+
@@ -1,3 +1,3 @@
140
+
# Test Repository
141
+
142
+
-Initial content.
143
+
+Modified content.
144
+
`
145
+
146
+
patchFile, err := createTempFileWithPatch(patch)
147
+
require.NoError(t, err)
148
+
defer os.Remove(patchFile)
149
+
150
+
opts := MergeOptions{
151
+
CommitMessage: "Apply test patch",
152
+
CommitterName: "Test Committer",
153
+
CommitterEmail: "committer@example.com",
154
+
FormatPatch: false,
155
+
}
156
+
157
+
err = repo.applyPatch(patch, patchFile, opts)
158
+
assert.NoError(t, err)
159
+
160
+
// verify the file was modified
161
+
content := h.readFile("README.md")
162
+
assert.Contains(t, content, "Modified content.")
163
+
}
164
+
165
+
func TestApplyPatch_AddNewFile(t *testing.T) {
166
+
h := helper(t)
167
+
defer h.cleanup()
168
+
169
+
repo := h.initRepo()
170
+
171
+
// add a new file
172
+
patch := `diff --git a/newfile.txt b/newfile.txt
173
+
new file mode 100644
174
+
index 0000000..ce01362
175
+
--- /dev/null
176
+
+++ b/newfile.txt
177
+
@@ -0,0 +1 @@
178
+
+hello
179
+
`
180
+
181
+
patchFile, err := createTempFileWithPatch(patch)
182
+
require.NoError(t, err)
183
+
defer os.Remove(patchFile)
184
+
185
+
opts := MergeOptions{
186
+
CommitMessage: "Add new file",
187
+
CommitterName: "Test Committer",
188
+
CommitterEmail: "committer@example.com",
189
+
FormatPatch: false,
190
+
}
191
+
192
+
err = repo.applyPatch(patch, patchFile, opts)
193
+
assert.NoError(t, err)
194
+
195
+
assert.True(t, h.fileExists("newfile.txt"))
196
+
content := h.readFile("newfile.txt")
197
+
assert.Equal(t, "hello\n", content)
198
+
}
199
+
200
+
func TestApplyPatch_DeleteFile(t *testing.T) {
201
+
h := helper(t)
202
+
defer h.cleanup()
203
+
204
+
repo := h.initRepo()
205
+
206
+
// add a file
207
+
h.commitFile("deleteme.txt", "content to delete\n", "Add file to delete")
208
+
209
+
// delete the file
210
+
patch := `diff --git a/deleteme.txt b/deleteme.txt
211
+
deleted file mode 100644
212
+
index 1234567..0000000
213
+
--- a/deleteme.txt
214
+
+++ /dev/null
215
+
@@ -1 +0,0 @@
216
+
-content to delete
217
+
`
218
+
219
+
patchFile, err := createTempFileWithPatch(patch)
220
+
require.NoError(t, err)
221
+
defer os.Remove(patchFile)
222
+
223
+
opts := MergeOptions{
224
+
CommitMessage: "Delete file",
225
+
CommitterName: "Test Committer",
226
+
CommitterEmail: "committer@example.com",
227
+
FormatPatch: false,
228
+
}
229
+
230
+
err = repo.applyPatch(patch, patchFile, opts)
231
+
assert.NoError(t, err)
232
+
233
+
assert.False(t, h.fileExists("deleteme.txt"))
234
+
}
235
+
236
+
func TestApplyPatch_WithAuthor(t *testing.T) {
237
+
h := helper(t)
238
+
defer h.cleanup()
239
+
240
+
repo := h.initRepo()
241
+
242
+
patch := `diff --git a/README.md b/README.md
243
+
index 1234567..abcdefg 100644
244
+
--- a/README.md
245
+
+++ b/README.md
246
+
@@ -1,3 +1,4 @@
247
+
# Test Repository
248
+
249
+
Initial content.
250
+
+New line.
251
+
`
252
+
253
+
patchFile, err := createTempFileWithPatch(patch)
254
+
require.NoError(t, err)
255
+
defer os.Remove(patchFile)
256
+
257
+
opts := MergeOptions{
258
+
CommitMessage: "Patch with author",
259
+
AuthorName: "Patch Author",
260
+
AuthorEmail: "author@example.com",
261
+
CommitterName: "Test Committer",
262
+
CommitterEmail: "committer@example.com",
263
+
FormatPatch: false,
264
+
}
265
+
266
+
err = repo.applyPatch(patch, patchFile, opts)
267
+
assert.NoError(t, err)
268
+
269
+
head, err := repo.r.Head()
270
+
require.NoError(t, err)
271
+
272
+
commit, err := repo.r.CommitObject(head.Hash())
273
+
require.NoError(t, err)
274
+
275
+
assert.Equal(t, "Patch Author", commit.Author.Name)
276
+
assert.Equal(t, "author@example.com", commit.Author.Email)
277
+
}
278
+
279
+
func TestApplyPatch_MissingFile(t *testing.T) {
280
+
h := helper(t)
281
+
defer h.cleanup()
282
+
283
+
repo := h.initRepo()
284
+
285
+
// patch that modifies a non-existent file
286
+
patch := `diff --git a/nonexistent.txt b/nonexistent.txt
287
+
index 1234567..abcdefg 100644
288
+
--- a/nonexistent.txt
289
+
+++ b/nonexistent.txt
290
+
@@ -1 +1 @@
291
+
-old content
292
+
+new content
293
+
`
294
+
295
+
patchFile, err := createTempFileWithPatch(patch)
296
+
require.NoError(t, err)
297
+
defer os.Remove(patchFile)
298
+
299
+
opts := MergeOptions{
300
+
CommitMessage: "Should fail",
301
+
CommitterName: "Test Committer",
302
+
CommitterEmail: "committer@example.com",
303
+
FormatPatch: false,
304
+
}
305
+
306
+
err = repo.applyPatch(patch, patchFile, opts)
307
+
assert.Error(t, err)
308
+
assert.Contains(t, err.Error(), "patch application failed")
309
+
}
310
+
311
+
func TestApplyPatch_Conflict(t *testing.T) {
312
+
h := helper(t)
313
+
defer h.cleanup()
314
+
315
+
repo := h.initRepo()
316
+
317
+
// modify the file to create a conflict
318
+
h.commitFile("README.md", "# Test Repository\n\nDifferent content.\n", "Modify README")
319
+
320
+
// patch that expects different content
321
+
patch := `diff --git a/README.md b/README.md
322
+
index 1234567..abcdefg 100644
323
+
--- a/README.md
324
+
+++ b/README.md
325
+
@@ -1,3 +1,3 @@
326
+
# Test Repository
327
+
328
+
-Initial content.
329
+
+Modified content.
330
+
`
331
+
332
+
patchFile, err := createTempFileWithPatch(patch)
333
+
require.NoError(t, err)
334
+
defer os.Remove(patchFile)
335
+
336
+
opts := MergeOptions{
337
+
CommitMessage: "Should conflict",
338
+
CommitterName: "Test Committer",
339
+
CommitterEmail: "committer@example.com",
340
+
FormatPatch: false,
341
+
}
342
+
343
+
err = repo.applyPatch(patch, patchFile, opts)
344
+
assert.Error(t, err)
345
+
}
346
+
347
+
func TestApplyPatch_MissingDirectory(t *testing.T) {
348
+
h := helper(t)
349
+
defer h.cleanup()
350
+
351
+
repo := h.initRepo()
352
+
353
+
// patch that adds a file in a non-existent directory
354
+
patch := `diff --git a/subdir/newfile.txt b/subdir/newfile.txt
355
+
new file mode 100644
356
+
index 0000000..ce01362
357
+
--- /dev/null
358
+
+++ b/subdir/newfile.txt
359
+
@@ -0,0 +1 @@
360
+
+content
361
+
`
362
+
363
+
patchFile, err := createTempFileWithPatch(patch)
364
+
require.NoError(t, err)
365
+
defer os.Remove(patchFile)
366
+
367
+
opts := MergeOptions{
368
+
CommitMessage: "Add file in subdir",
369
+
CommitterName: "Test Committer",
370
+
CommitterEmail: "committer@example.com",
371
+
FormatPatch: false,
372
+
}
373
+
374
+
// git apply should create the directory automatically
375
+
err = repo.applyPatch(patch, patchFile, opts)
376
+
assert.NoError(t, err)
377
+
378
+
// Verify the file and directory were created
379
+
assert.True(t, h.fileExists("subdir/newfile.txt"))
380
+
}
381
+
382
+
func TestApplyMailbox_Single(t *testing.T) {
383
+
h := helper(t)
384
+
defer h.cleanup()
385
+
386
+
repo := h.initRepo()
387
+
388
+
// format-patch mailbox format
389
+
patch := `From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
390
+
From: Patch Author <author@example.com>
391
+
Date: Mon, 1 Jan 2024 12:00:00 +0000
392
+
Subject: [PATCH] Add new feature
393
+
394
+
This is a test patch.
395
+
---
396
+
newfile.txt | 1 +
397
+
1 file changed, 1 insertion(+)
398
+
create mode 100644 newfile.txt
399
+
400
+
diff --git a/newfile.txt b/newfile.txt
401
+
new file mode 100644
402
+
index 0000000..ce01362
403
+
--- /dev/null
404
+
+++ b/newfile.txt
405
+
@@ -0,0 +1 @@
406
+
+hello
407
+
--
408
+
2.40.0
409
+
`
410
+
411
+
err := repo.applyMailbox(patch)
412
+
assert.NoError(t, err)
413
+
414
+
assert.True(t, h.fileExists("newfile.txt"))
415
+
content := h.readFile("newfile.txt")
416
+
assert.Equal(t, "hello\n", content)
417
+
}
418
+
419
+
func TestApplyMailbox_Multiple(t *testing.T) {
420
+
h := helper(t)
421
+
defer h.cleanup()
422
+
423
+
repo := h.initRepo()
424
+
425
+
// multiple patches in mailbox format
426
+
patch := `From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
427
+
From: Patch Author <author@example.com>
428
+
Date: Mon, 1 Jan 2024 12:00:00 +0000
429
+
Subject: [PATCH 1/2] Add first file
430
+
431
+
---
432
+
file1.txt | 1 +
433
+
1 file changed, 1 insertion(+)
434
+
create mode 100644 file1.txt
435
+
436
+
diff --git a/file1.txt b/file1.txt
437
+
new file mode 100644
438
+
index 0000000..ce01362
439
+
--- /dev/null
440
+
+++ b/file1.txt
441
+
@@ -0,0 +1 @@
442
+
+first
443
+
--
444
+
2.40.0
445
+
446
+
From 1111111111111111111111111111111111111111 Mon Sep 17 00:00:00 2001
447
+
From: Patch Author <author@example.com>
448
+
Date: Mon, 1 Jan 2024 12:01:00 +0000
449
+
Subject: [PATCH 2/2] Add second file
450
+
451
+
---
452
+
file2.txt | 1 +
453
+
1 file changed, 1 insertion(+)
454
+
create mode 100644 file2.txt
455
+
456
+
diff --git a/file2.txt b/file2.txt
457
+
new file mode 100644
458
+
index 0000000..ce01362
459
+
--- /dev/null
460
+
+++ b/file2.txt
461
+
@@ -0,0 +1 @@
462
+
+second
463
+
--
464
+
2.40.0
465
+
`
466
+
467
+
err := repo.applyMailbox(patch)
468
+
assert.NoError(t, err)
469
+
470
+
assert.True(t, h.fileExists("file1.txt"))
471
+
assert.True(t, h.fileExists("file2.txt"))
472
+
473
+
content1 := h.readFile("file1.txt")
474
+
assert.Equal(t, "first\n", content1)
475
+
476
+
content2 := h.readFile("file2.txt")
477
+
assert.Equal(t, "second\n", content2)
478
+
}
479
+
480
+
func TestApplyMailbox_Conflict(t *testing.T) {
481
+
h := helper(t)
482
+
defer h.cleanup()
483
+
484
+
repo := h.initRepo()
485
+
486
+
h.commitFile("README.md", "# Test Repository\n\nConflicting content.\n", "Create conflict")
487
+
488
+
patch := `From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
489
+
From: Patch Author <author@example.com>
490
+
Date: Mon, 1 Jan 2024 12:00:00 +0000
491
+
Subject: [PATCH] Modify README
492
+
493
+
---
494
+
README.md | 2 +-
495
+
1 file changed, 1 insertion(+), 1 deletion(-)
496
+
497
+
diff --git a/README.md b/README.md
498
+
index 1234567..abcdefg 100644
499
+
--- a/README.md
500
+
+++ b/README.md
501
+
@@ -1,3 +1,3 @@
502
+
# Test Repository
503
+
504
+
-Initial content.
505
+
+Different content.
506
+
--
507
+
2.40.0
508
+
`
509
+
510
+
err := repo.applyMailbox(patch)
511
+
assert.Error(t, err)
512
+
513
+
var mergeErr *ErrMerge
514
+
assert.ErrorAs(t, err, &mergeErr)
515
+
}
516
+
517
+
func TestParseGitApplyErrors(t *testing.T) {
518
+
tests := []struct {
519
+
name string
520
+
errorOutput string
521
+
expectedCount int
522
+
expectedReason string
523
+
}{
524
+
{
525
+
name: "file already exists",
526
+
errorOutput: `error: path/to/file.txt: already exists in working directory`,
527
+
expectedCount: 1,
528
+
expectedReason: "file already exists",
529
+
},
530
+
{
531
+
name: "file does not exist",
532
+
errorOutput: `error: path/to/file.txt: does not exist in working tree`,
533
+
expectedCount: 1,
534
+
expectedReason: "file does not exist",
535
+
},
536
+
{
537
+
name: "patch does not apply",
538
+
errorOutput: `error: patch failed: file.txt:10
539
+
error: file.txt: patch does not apply`,
540
+
expectedCount: 1,
541
+
expectedReason: "patch does not apply",
542
+
},
543
+
{
544
+
name: "multiple conflicts",
545
+
errorOutput: `error: patch failed: file1.txt:5
546
+
error: file1.txt:5: some error
547
+
error: patch failed: file2.txt:10
548
+
error: file2.txt:10: another error`,
549
+
expectedCount: 2,
550
+
},
551
+
}
552
+
553
+
for _, tt := range tests {
554
+
t.Run(tt.name, func(t *testing.T) {
555
+
conflicts := parseGitApplyErrors(tt.errorOutput)
556
+
assert.Len(t, conflicts, tt.expectedCount)
557
+
558
+
if tt.expectedReason != "" && len(conflicts) > 0 {
559
+
assert.Equal(t, tt.expectedReason, conflicts[0].Reason)
560
+
}
561
+
})
562
+
}
563
+
}
564
+
565
+
func TestErrMerge_Error(t *testing.T) {
566
+
tests := []struct {
567
+
name string
568
+
err ErrMerge
569
+
expectedMsg string
570
+
}{
571
+
{
572
+
name: "with conflicts",
573
+
err: ErrMerge{
574
+
Message: "test merge failed",
575
+
HasConflict: true,
576
+
Conflicts: []ConflictInfo{
577
+
{Filename: "file1.txt", Reason: "conflict 1"},
578
+
{Filename: "file2.txt", Reason: "conflict 2"},
579
+
},
580
+
},
581
+
expectedMsg: "merge failed due to conflicts: test merge failed (2 conflicts)",
582
+
},
583
+
{
584
+
name: "with other error",
585
+
err: ErrMerge{
586
+
Message: "command failed",
587
+
OtherError: assert.AnError,
588
+
},
589
+
expectedMsg: "merge failed: command failed:",
590
+
},
591
+
{
592
+
name: "message only",
593
+
err: ErrMerge{
594
+
Message: "simple failure",
595
+
},
596
+
expectedMsg: "merge failed: simple failure",
597
+
},
598
+
}
599
+
600
+
for _, tt := range tests {
601
+
t.Run(tt.name, func(t *testing.T) {
602
+
errMsg := tt.err.Error()
603
+
assert.Contains(t, errMsg, tt.expectedMsg)
604
+
})
605
+
}
606
+
}
607
+
608
+
func TestMergeWithOptions_Integration(t *testing.T) {
609
+
h := helper(t)
610
+
defer h.cleanup()
611
+
612
+
// create a repository first with initial content
613
+
workRepoPath := filepath.Join(h.tempDir, "work-repo")
614
+
workRepo, err := git.PlainInit(workRepoPath, false)
615
+
require.NoError(t, err)
616
+
617
+
// configure git user
618
+
cfg, err := workRepo.Config()
619
+
require.NoError(t, err)
620
+
cfg.User.Name = "Test User"
621
+
cfg.User.Email = "test@example.com"
622
+
err = workRepo.SetConfig(cfg)
623
+
require.NoError(t, err)
624
+
625
+
// Create initial commit
626
+
w, err := workRepo.Worktree()
627
+
require.NoError(t, err)
628
+
629
+
err = os.WriteFile(filepath.Join(workRepoPath, "README.md"), []byte("# Initial\n"), 0644)
630
+
require.NoError(t, err)
631
+
632
+
_, err = w.Add("README.md")
633
+
require.NoError(t, err)
634
+
635
+
_, err = w.Commit("Initial commit", &git.CommitOptions{
636
+
Author: &object.Signature{
637
+
Name: "Test User",
638
+
Email: "test@example.com",
639
+
},
640
+
})
641
+
require.NoError(t, err)
642
+
643
+
// create a bare repository (like production)
644
+
bareRepoPath := filepath.Join(h.tempDir, "bare-repo")
645
+
err = InitBare(bareRepoPath, "main")
646
+
require.NoError(t, err)
647
+
648
+
// add bare repo as remote and push to it
649
+
_, err = workRepo.CreateRemote(&config.RemoteConfig{
650
+
Name: "origin",
651
+
URLs: []string{"file://" + bareRepoPath},
652
+
})
653
+
require.NoError(t, err)
654
+
655
+
err = workRepo.Push(&git.PushOptions{
656
+
RemoteName: "origin",
657
+
RefSpecs: []config.RefSpec{"refs/heads/master:refs/heads/main"},
658
+
})
659
+
require.NoError(t, err)
660
+
661
+
// now merge a patch into the bare repo
662
+
gitRepo, err := PlainOpen(bareRepoPath)
663
+
require.NoError(t, err)
664
+
665
+
patch := `diff --git a/feature.txt b/feature.txt
666
+
new file mode 100644
667
+
index 0000000..5e1c309
668
+
--- /dev/null
669
+
+++ b/feature.txt
670
+
@@ -0,0 +1 @@
671
+
+Hello World
672
+
`
673
+
674
+
opts := MergeOptions{
675
+
CommitMessage: "Add feature",
676
+
CommitterName: "Test Committer",
677
+
CommitterEmail: "committer@example.com",
678
+
FormatPatch: false,
679
+
}
680
+
681
+
err = gitRepo.MergeWithOptions(patch, "main", opts)
682
+
assert.NoError(t, err)
683
+
684
+
// Clone again and verify the changes were merged
685
+
verifyRepoPath := filepath.Join(h.tempDir, "verify-repo")
686
+
verifyRepo, err := git.PlainClone(verifyRepoPath, false, &git.CloneOptions{
687
+
URL: "file://" + bareRepoPath,
688
+
})
689
+
require.NoError(t, err)
690
+
691
+
// check that feature.txt exists
692
+
featureFile := filepath.Join(verifyRepoPath, "feature.txt")
693
+
assert.FileExists(t, featureFile)
694
+
695
+
content, err := os.ReadFile(featureFile)
696
+
require.NoError(t, err)
697
+
assert.Equal(t, "Hello World\n", string(content))
698
+
699
+
// verify commit message
700
+
head, err := verifyRepo.Head()
701
+
require.NoError(t, err)
702
+
703
+
commit, err := verifyRepo.CommitObject(head.Hash())
704
+
require.NoError(t, err)
705
+
assert.Equal(t, "Add feature", strings.TrimSpace(commit.Message))
706
+
}
History
2 rounds
0 comments
1 commit
expand
collapse
knotserver/git: add tests for applyPatch
Signed-off-by: oppiliappan <me@oppi.li>
2/3 timeout, 1/3 success
expand
collapse
expand 0 comments
pull request successfully merged
oppi.li
submitted
#0
1 commit
expand
collapse
knotserver/git: add tests for applyPatch
Signed-off-by: oppiliappan <me@oppi.li>