1// Copyright 2017 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package integrations
6
7import (
8	"encoding/hex"
9	"fmt"
10	"math/rand"
11	"net/http"
12	"net/url"
13	"os"
14	"path"
15	"path/filepath"
16	"strconv"
17	"testing"
18	"time"
19
20	"code.gitea.io/gitea/models"
21	"code.gitea.io/gitea/models/perm"
22	repo_model "code.gitea.io/gitea/models/repo"
23	"code.gitea.io/gitea/models/unittest"
24	user_model "code.gitea.io/gitea/models/user"
25	"code.gitea.io/gitea/modules/git"
26	"code.gitea.io/gitea/modules/lfs"
27	"code.gitea.io/gitea/modules/setting"
28	api "code.gitea.io/gitea/modules/structs"
29	"code.gitea.io/gitea/modules/util"
30
31	"github.com/stretchr/testify/assert"
32)
33
34const (
35	littleSize = 1024              //1ko
36	bigSize    = 128 * 1024 * 1024 //128Mo
37)
38
39func TestGit(t *testing.T) {
40	onGiteaRun(t, testGit)
41}
42
43func testGit(t *testing.T, u *url.URL) {
44	username := "user2"
45	baseAPITestContext := NewAPITestContext(t, username, "repo1")
46
47	u.Path = baseAPITestContext.GitPath()
48
49	forkedUserCtx := NewAPITestContext(t, "user4", "repo1")
50
51	t.Run("HTTP", func(t *testing.T) {
52		defer PrintCurrentTest(t)()
53		ensureAnonymousClone(t, u)
54		httpContext := baseAPITestContext
55		httpContext.Reponame = "repo-tmp-17"
56		forkedUserCtx.Reponame = httpContext.Reponame
57
58		dstPath, err := os.MkdirTemp("", httpContext.Reponame)
59		assert.NoError(t, err)
60		defer util.RemoveAll(dstPath)
61
62		t.Run("CreateRepoInDifferentUser", doAPICreateRepository(forkedUserCtx, false))
63		t.Run("AddUserAsCollaborator", doAPIAddCollaborator(forkedUserCtx, httpContext.Username, perm.AccessModeRead))
64
65		t.Run("ForkFromDifferentUser", doAPIForkRepository(httpContext, forkedUserCtx.Username))
66
67		u.Path = httpContext.GitPath()
68		u.User = url.UserPassword(username, userPassword)
69
70		t.Run("Clone", doGitClone(dstPath, u))
71
72		dstPath2, err := os.MkdirTemp("", httpContext.Reponame)
73		assert.NoError(t, err)
74		defer util.RemoveAll(dstPath2)
75
76		t.Run("Partial Clone", doPartialGitClone(dstPath2, u))
77
78		little, big := standardCommitAndPushTest(t, dstPath)
79		littleLFS, bigLFS := lfsCommitAndPushTest(t, dstPath)
80		rawTest(t, &httpContext, little, big, littleLFS, bigLFS)
81		mediaTest(t, &httpContext, little, big, littleLFS, bigLFS)
82
83		t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "master", "test/head"))
84		t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath))
85		t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge"))
86		t.Run("MergeFork", func(t *testing.T) {
87			defer PrintCurrentTest(t)()
88			t.Run("CreatePRAndMerge", doMergeFork(httpContext, forkedUserCtx, "master", httpContext.Username+":master"))
89			rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
90			mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
91		})
92
93		t.Run("PushCreate", doPushCreate(httpContext, u))
94	})
95	t.Run("SSH", func(t *testing.T) {
96		defer PrintCurrentTest(t)()
97		sshContext := baseAPITestContext
98		sshContext.Reponame = "repo-tmp-18"
99		keyname := "my-testing-key"
100		forkedUserCtx.Reponame = sshContext.Reponame
101		t.Run("CreateRepoInDifferentUser", doAPICreateRepository(forkedUserCtx, false))
102		t.Run("AddUserAsCollaborator", doAPIAddCollaborator(forkedUserCtx, sshContext.Username, perm.AccessModeRead))
103		t.Run("ForkFromDifferentUser", doAPIForkRepository(sshContext, forkedUserCtx.Username))
104
105		//Setup key the user ssh key
106		withKeyFile(t, keyname, func(keyFile string) {
107			t.Run("CreateUserKey", doAPICreateUserKey(sshContext, "test-key", keyFile))
108
109			//Setup remote link
110			//TODO: get url from api
111			sshURL := createSSHUrl(sshContext.GitPath(), u)
112
113			//Setup clone folder
114			dstPath, err := os.MkdirTemp("", sshContext.Reponame)
115			assert.NoError(t, err)
116			defer util.RemoveAll(dstPath)
117
118			t.Run("Clone", doGitClone(dstPath, sshURL))
119
120			little, big := standardCommitAndPushTest(t, dstPath)
121			littleLFS, bigLFS := lfsCommitAndPushTest(t, dstPath)
122			rawTest(t, &sshContext, little, big, littleLFS, bigLFS)
123			mediaTest(t, &sshContext, little, big, littleLFS, bigLFS)
124
125			t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &sshContext, "master", "test/head2"))
126			t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath))
127			t.Run("MergeFork", func(t *testing.T) {
128				defer PrintCurrentTest(t)()
129				t.Run("CreatePRAndMerge", doMergeFork(sshContext, forkedUserCtx, "master", sshContext.Username+":master"))
130				rawTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
131				mediaTest(t, &forkedUserCtx, little, big, littleLFS, bigLFS)
132			})
133
134			t.Run("PushCreate", doPushCreate(sshContext, sshURL))
135		})
136	})
137}
138
139func ensureAnonymousClone(t *testing.T, u *url.URL) {
140	dstLocalPath, err := os.MkdirTemp("", "repo1")
141	assert.NoError(t, err)
142	defer util.RemoveAll(dstLocalPath)
143	t.Run("CloneAnonymous", doGitClone(dstLocalPath, u))
144
145}
146
147func standardCommitAndPushTest(t *testing.T, dstPath string) (little, big string) {
148	t.Run("Standard", func(t *testing.T) {
149		defer PrintCurrentTest(t)()
150		little, big = commitAndPushTest(t, dstPath, "data-file-")
151	})
152	return
153}
154
155func lfsCommitAndPushTest(t *testing.T, dstPath string) (littleLFS, bigLFS string) {
156	t.Run("LFS", func(t *testing.T) {
157		defer PrintCurrentTest(t)()
158		git.CheckLFSVersion()
159		if !setting.LFS.StartServer {
160			t.Skip()
161			return
162		}
163		prefix := "lfs-data-file-"
164		_, err := git.NewCommand("lfs").AddArguments("install").RunInDir(dstPath)
165		assert.NoError(t, err)
166		_, err = git.NewCommand("lfs").AddArguments("track", prefix+"*").RunInDir(dstPath)
167		assert.NoError(t, err)
168		err = git.AddChanges(dstPath, false, ".gitattributes")
169		assert.NoError(t, err)
170
171		err = git.CommitChangesWithArgs(dstPath, allowLFSFilters(), git.CommitChangesOptions{
172			Committer: &git.Signature{
173				Email: "user2@example.com",
174				Name:  "User Two",
175				When:  time.Now(),
176			},
177			Author: &git.Signature{
178				Email: "user2@example.com",
179				Name:  "User Two",
180				When:  time.Now(),
181			},
182			Message: fmt.Sprintf("Testing commit @ %v", time.Now()),
183		})
184		assert.NoError(t, err)
185
186		littleLFS, bigLFS = commitAndPushTest(t, dstPath, prefix)
187
188		t.Run("Locks", func(t *testing.T) {
189			defer PrintCurrentTest(t)()
190			lockTest(t, dstPath)
191		})
192	})
193	return
194}
195
196func commitAndPushTest(t *testing.T, dstPath, prefix string) (little, big string) {
197	t.Run("PushCommit", func(t *testing.T) {
198		defer PrintCurrentTest(t)()
199		t.Run("Little", func(t *testing.T) {
200			defer PrintCurrentTest(t)()
201			little = doCommitAndPush(t, littleSize, dstPath, prefix)
202		})
203		t.Run("Big", func(t *testing.T) {
204			if testing.Short() {
205				t.Skip("Skipping test in short mode.")
206				return
207			}
208			defer PrintCurrentTest(t)()
209			big = doCommitAndPush(t, bigSize, dstPath, prefix)
210		})
211	})
212	return
213}
214
215func rawTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) {
216	t.Run("Raw", func(t *testing.T) {
217		defer PrintCurrentTest(t)()
218		username := ctx.Username
219		reponame := ctx.Reponame
220
221		session := loginUser(t, username)
222
223		// Request raw paths
224		req := NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", little))
225		resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
226		assert.Equal(t, littleSize, resp.Length)
227
228		git.CheckLFSVersion()
229		if setting.LFS.StartServer {
230			req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", littleLFS))
231			resp := session.MakeRequest(t, req, http.StatusOK)
232			assert.NotEqual(t, littleSize, resp.Body.Len())
233			assert.LessOrEqual(t, resp.Body.Len(), 1024)
234			if resp.Body.Len() != littleSize && resp.Body.Len() <= 1024 {
235				assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier)
236			}
237		}
238
239		if !testing.Short() {
240			req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", big))
241			resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
242			assert.Equal(t, bigSize, resp.Length)
243
244			if setting.LFS.StartServer {
245				req = NewRequest(t, "GET", path.Join("/", username, reponame, "/raw/branch/master/", bigLFS))
246				resp := session.MakeRequest(t, req, http.StatusOK)
247				assert.NotEqual(t, bigSize, resp.Body.Len())
248				if resp.Body.Len() != bigSize && resp.Body.Len() <= 1024 {
249					assert.Contains(t, resp.Body.String(), lfs.MetaFileIdentifier)
250				}
251			}
252		}
253	})
254}
255
256func mediaTest(t *testing.T, ctx *APITestContext, little, big, littleLFS, bigLFS string) {
257	t.Run("Media", func(t *testing.T) {
258		defer PrintCurrentTest(t)()
259
260		username := ctx.Username
261		reponame := ctx.Reponame
262
263		session := loginUser(t, username)
264
265		// Request media paths
266		req := NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", little))
267		resp := session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
268		assert.Equal(t, littleSize, resp.Length)
269
270		git.CheckLFSVersion()
271		if setting.LFS.StartServer {
272			req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", littleLFS))
273			resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
274			assert.Equal(t, littleSize, resp.Length)
275		}
276
277		if !testing.Short() {
278			req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", big))
279			resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
280			assert.Equal(t, bigSize, resp.Length)
281
282			if setting.LFS.StartServer {
283				req = NewRequest(t, "GET", path.Join("/", username, reponame, "/media/branch/master/", bigLFS))
284				resp = session.MakeRequestNilResponseRecorder(t, req, http.StatusOK)
285				assert.Equal(t, bigSize, resp.Length)
286			}
287		}
288	})
289}
290
291func lockTest(t *testing.T, repoPath string) {
292	lockFileTest(t, "README.md", repoPath)
293}
294
295func lockFileTest(t *testing.T, filename, repoPath string) {
296	_, err := git.NewCommand("lfs").AddArguments("locks").RunInDir(repoPath)
297	assert.NoError(t, err)
298	_, err = git.NewCommand("lfs").AddArguments("lock", filename).RunInDir(repoPath)
299	assert.NoError(t, err)
300	_, err = git.NewCommand("lfs").AddArguments("locks").RunInDir(repoPath)
301	assert.NoError(t, err)
302	_, err = git.NewCommand("lfs").AddArguments("unlock", filename).RunInDir(repoPath)
303	assert.NoError(t, err)
304}
305
306func doCommitAndPush(t *testing.T, size int, repoPath, prefix string) string {
307	name, err := generateCommitWithNewData(size, repoPath, "user2@example.com", "User Two", prefix)
308	assert.NoError(t, err)
309	_, err = git.NewCommand("push", "origin", "master").RunInDir(repoPath) //Push
310	assert.NoError(t, err)
311	return name
312}
313
314func generateCommitWithNewData(size int, repoPath, email, fullName, prefix string) (string, error) {
315	//Generate random file
316	bufSize := 4 * 1024
317	if bufSize > size {
318		bufSize = size
319	}
320
321	buffer := make([]byte, bufSize)
322
323	tmpFile, err := os.CreateTemp(repoPath, prefix)
324	if err != nil {
325		return "", err
326	}
327	defer tmpFile.Close()
328	written := 0
329	for written < size {
330		n := size - written
331		if n > bufSize {
332			n = bufSize
333		}
334		_, err := rand.Read(buffer[:n])
335		if err != nil {
336			return "", err
337		}
338		n, err = tmpFile.Write(buffer[:n])
339		if err != nil {
340			return "", err
341		}
342		written += n
343	}
344	if err != nil {
345		return "", err
346	}
347
348	//Commit
349	// Now here we should explicitly allow lfs filters to run
350	globalArgs := allowLFSFilters()
351	err = git.AddChangesWithArgs(repoPath, globalArgs, false, filepath.Base(tmpFile.Name()))
352	if err != nil {
353		return "", err
354	}
355	err = git.CommitChangesWithArgs(repoPath, globalArgs, git.CommitChangesOptions{
356		Committer: &git.Signature{
357			Email: email,
358			Name:  fullName,
359			When:  time.Now(),
360		},
361		Author: &git.Signature{
362			Email: email,
363			Name:  fullName,
364			When:  time.Now(),
365		},
366		Message: fmt.Sprintf("Testing commit @ %v", time.Now()),
367	})
368	return filepath.Base(tmpFile.Name()), err
369}
370
371func doBranchProtectPRMerge(baseCtx *APITestContext, dstPath string) func(t *testing.T) {
372	return func(t *testing.T) {
373		defer PrintCurrentTest(t)()
374		t.Run("CreateBranchProtected", doGitCreateBranch(dstPath, "protected"))
375		t.Run("PushProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected"))
376
377		ctx := NewAPITestContext(t, baseCtx.Username, baseCtx.Reponame)
378		t.Run("ProtectProtectedBranchNoWhitelist", doProtectBranch(ctx, "protected", "", ""))
379		t.Run("GenerateCommit", func(t *testing.T) {
380			_, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
381			assert.NoError(t, err)
382		})
383		t.Run("FailToPushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "origin", "protected"))
384		t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected"))
385		var pr api.PullRequest
386		var err error
387		t.Run("CreatePullRequest", func(t *testing.T) {
388			pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "protected", "unprotected")(t)
389			assert.NoError(t, err)
390		})
391		t.Run("GenerateCommit", func(t *testing.T) {
392			_, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
393			assert.NoError(t, err)
394		})
395		t.Run("PushToUnprotectedBranch", doGitPushTestRepository(dstPath, "origin", "protected:unprotected-2"))
396		var pr2 api.PullRequest
397		t.Run("CreatePullRequest", func(t *testing.T) {
398			pr2, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, "unprotected", "unprotected-2")(t)
399			assert.NoError(t, err)
400		})
401		t.Run("MergePR2", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr2.Index))
402		t.Run("MergePR", doAPIMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, pr.Index))
403		t.Run("PullProtected", doGitPull(dstPath, "origin", "protected"))
404
405		t.Run("ProtectProtectedBranchUnprotectedFilePaths", doProtectBranch(ctx, "protected", "", "unprotected-file-*"))
406		t.Run("GenerateCommit", func(t *testing.T) {
407			_, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "unprotected-file-")
408			assert.NoError(t, err)
409		})
410		t.Run("PushUnprotectedFilesToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "protected"))
411
412		t.Run("ProtectProtectedBranchWhitelist", doProtectBranch(ctx, "protected", baseCtx.Username, ""))
413
414		t.Run("CheckoutMaster", doGitCheckoutBranch(dstPath, "master"))
415		t.Run("CreateBranchForced", doGitCreateBranch(dstPath, "toforce"))
416		t.Run("GenerateCommit", func(t *testing.T) {
417			_, err := generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-")
418			assert.NoError(t, err)
419		})
420		t.Run("FailToForcePushToProtectedBranch", doGitPushTestRepositoryFail(dstPath, "-f", "origin", "toforce:protected"))
421		t.Run("MergeProtectedToToforce", doGitMerge(dstPath, "protected"))
422		t.Run("PushToProtectedBranch", doGitPushTestRepository(dstPath, "origin", "toforce:protected"))
423		t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master"))
424	}
425}
426
427func doProtectBranch(ctx APITestContext, branch, userToWhitelist, unprotectedFilePatterns string) func(t *testing.T) {
428	// We are going to just use the owner to set the protection.
429	return func(t *testing.T) {
430		csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/settings/branches", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame)))
431
432		if userToWhitelist == "" {
433			// Change branch to protected
434			req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), url.PathEscape(branch)), map[string]string{
435				"_csrf":                     csrf,
436				"protected":                 "on",
437				"unprotected_file_patterns": unprotectedFilePatterns,
438			})
439			ctx.Session.MakeRequest(t, req, http.StatusFound)
440		} else {
441			user, err := user_model.GetUserByName(userToWhitelist)
442			assert.NoError(t, err)
443			// Change branch to protected
444			req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/settings/branches/%s", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), url.PathEscape(branch)), map[string]string{
445				"_csrf":                     csrf,
446				"protected":                 "on",
447				"enable_push":               "whitelist",
448				"enable_whitelist":          "on",
449				"whitelist_users":           strconv.FormatInt(user.ID, 10),
450				"unprotected_file_patterns": unprotectedFilePatterns,
451			})
452			ctx.Session.MakeRequest(t, req, http.StatusFound)
453		}
454		// Check if master branch has been locked successfully
455		flashCookie := ctx.Session.GetCookie("macaron_flash")
456		assert.NotNil(t, flashCookie)
457		assert.EqualValues(t, "success%3DBranch%2Bprotection%2Bfor%2Bbranch%2B%2527"+url.QueryEscape(branch)+"%2527%2Bhas%2Bbeen%2Bupdated.", flashCookie.Value)
458	}
459}
460
461func doMergeFork(ctx, baseCtx APITestContext, baseBranch, headBranch string) func(t *testing.T) {
462	return func(t *testing.T) {
463		defer PrintCurrentTest(t)()
464		var pr api.PullRequest
465		var err error
466
467		// Create a test pullrequest
468		t.Run("CreatePullRequest", func(t *testing.T) {
469			pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t)
470			assert.NoError(t, err)
471		})
472
473		// Ensure the PR page works
474		t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
475
476		// Then get the diff string
477		var diffHash string
478		var diffLength int
479		t.Run("GetDiff", func(t *testing.T) {
480			req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d.diff", url.PathEscape(baseCtx.Username), url.PathEscape(baseCtx.Reponame), pr.Index))
481			resp := ctx.Session.MakeRequestNilResponseHashSumRecorder(t, req, http.StatusOK)
482			diffHash = string(resp.Hash.Sum(nil))
483			diffLength = resp.Length
484		})
485
486		// Now: Merge the PR & make sure that doesn't break the PR page or change its diff
487		t.Run("MergePR", doAPIMergePullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index))
488		t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
489		t.Run("CheckPR", func(t *testing.T) {
490			oldMergeBase := pr.MergeBase
491			pr2, err := doAPIGetPullRequest(baseCtx, baseCtx.Username, baseCtx.Reponame, pr.Index)(t)
492			assert.NoError(t, err)
493			assert.Equal(t, oldMergeBase, pr2.MergeBase)
494		})
495		t.Run("EnsurDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength))
496
497		// Then: Delete the head branch & make sure that doesn't break the PR page or change its diff
498		t.Run("DeleteHeadBranch", doBranchDelete(baseCtx, baseCtx.Username, baseCtx.Reponame, headBranch))
499		t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
500		t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength))
501
502		// Delete the head repository & make sure that doesn't break the PR page or change its diff
503		t.Run("DeleteHeadRepository", doAPIDeleteRepository(ctx))
504		t.Run("EnsureCanSeePull", doEnsureCanSeePull(baseCtx, pr))
505		t.Run("EnsureDiffNoChange", doEnsureDiffNoChange(baseCtx, pr, diffHash, diffLength))
506	}
507}
508
509func doCreatePRAndSetManuallyMerged(ctx, baseCtx APITestContext, dstPath, baseBranch, headBranch string) func(t *testing.T) {
510	return func(t *testing.T) {
511		defer PrintCurrentTest(t)()
512		var (
513			pr           api.PullRequest
514			err          error
515			lastCommitID string
516		)
517
518		trueBool := true
519		falseBool := false
520
521		t.Run("AllowSetManuallyMergedAndSwitchOffAutodetectManualMerge", doAPIEditRepository(baseCtx, &api.EditRepoOption{
522			HasPullRequests:       &trueBool,
523			AllowManualMerge:      &trueBool,
524			AutodetectManualMerge: &falseBool,
525		}))
526
527		t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch))
528		t.Run("PushToHeadBranch", doGitPushTestRepository(dstPath, "origin", headBranch))
529		t.Run("CreateEmptyPullRequest", func(t *testing.T) {
530			pr, err = doAPICreatePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, baseBranch, headBranch)(t)
531			assert.NoError(t, err)
532		})
533		lastCommitID = pr.Base.Sha
534		t.Run("ManuallyMergePR", doAPIManuallyMergePullRequest(ctx, baseCtx.Username, baseCtx.Reponame, lastCommitID, pr.Index))
535	}
536}
537
538func doEnsureCanSeePull(ctx APITestContext, pr api.PullRequest) func(t *testing.T) {
539	return func(t *testing.T) {
540		req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
541		ctx.Session.MakeRequest(t, req, http.StatusOK)
542		req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/files", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
543		ctx.Session.MakeRequest(t, req, http.StatusOK)
544		req = NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d/commits", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
545		ctx.Session.MakeRequest(t, req, http.StatusOK)
546	}
547}
548
549func doEnsureDiffNoChange(ctx APITestContext, pr api.PullRequest, diffHash string, diffLength int) func(t *testing.T) {
550	return func(t *testing.T) {
551		req := NewRequest(t, "GET", fmt.Sprintf("/%s/%s/pulls/%d.diff", url.PathEscape(ctx.Username), url.PathEscape(ctx.Reponame), pr.Index))
552		resp := ctx.Session.MakeRequestNilResponseHashSumRecorder(t, req, http.StatusOK)
553		actual := string(resp.Hash.Sum(nil))
554		actualLength := resp.Length
555
556		equal := diffHash == actual
557		assert.True(t, equal, "Unexpected change in the diff string: expected hash: %s size: %d but was actually: %s size: %d", hex.EncodeToString([]byte(diffHash)), diffLength, hex.EncodeToString([]byte(actual)), actualLength)
558	}
559}
560
561func doPushCreate(ctx APITestContext, u *url.URL) func(t *testing.T) {
562	return func(t *testing.T) {
563		defer PrintCurrentTest(t)()
564
565		// create a context for a currently non-existent repository
566		ctx.Reponame = fmt.Sprintf("repo-tmp-push-create-%s", u.Scheme)
567		u.Path = ctx.GitPath()
568
569		// Create a temporary directory
570		tmpDir, err := os.MkdirTemp("", ctx.Reponame)
571		assert.NoError(t, err)
572		defer util.RemoveAll(tmpDir)
573
574		// Now create local repository to push as our test and set its origin
575		t.Run("InitTestRepository", doGitInitTestRepository(tmpDir))
576		t.Run("AddRemote", doGitAddRemote(tmpDir, "origin", u))
577
578		// Disable "Push To Create" and attempt to push
579		setting.Repository.EnablePushCreateUser = false
580		t.Run("FailToPushAndCreateTestRepository", doGitPushTestRepositoryFail(tmpDir, "origin", "master"))
581
582		// Enable "Push To Create"
583		setting.Repository.EnablePushCreateUser = true
584
585		// Assert that cloning from a non-existent repository does not create it and that it definitely wasn't create above
586		t.Run("FailToCloneFromNonExistentRepository", doGitCloneFail(u))
587
588		// Then "Push To Create"x
589		t.Run("SuccessfullyPushAndCreateTestRepository", doGitPushTestRepository(tmpDir, "origin", "master"))
590
591		// Finally, fetch repo from database and ensure the correct repository has been created
592		repo, err := repo_model.GetRepositoryByOwnerAndName(ctx.Username, ctx.Reponame)
593		assert.NoError(t, err)
594		assert.False(t, repo.IsEmpty)
595		assert.True(t, repo.IsPrivate)
596
597		// Now add a remote that is invalid to "Push To Create"
598		invalidCtx := ctx
599		invalidCtx.Reponame = fmt.Sprintf("invalid/repo-tmp-push-create-%s", u.Scheme)
600		u.Path = invalidCtx.GitPath()
601		t.Run("AddInvalidRemote", doGitAddRemote(tmpDir, "invalid", u))
602
603		// Fail to "Push To Create" the invalid
604		t.Run("FailToPushAndCreateInvalidTestRepository", doGitPushTestRepositoryFail(tmpDir, "invalid", "master"))
605	}
606}
607
608func doBranchDelete(ctx APITestContext, owner, repo, branch string) func(*testing.T) {
609	return func(t *testing.T) {
610		csrf := GetCSRF(t, ctx.Session, fmt.Sprintf("/%s/%s/branches", url.PathEscape(owner), url.PathEscape(repo)))
611
612		req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/%s/branches/delete?name=%s", url.PathEscape(owner), url.PathEscape(repo), url.QueryEscape(branch)), map[string]string{
613			"_csrf": csrf,
614		})
615		ctx.Session.MakeRequest(t, req, http.StatusOK)
616	}
617}
618
619func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headBranch string) func(t *testing.T) {
620	return func(t *testing.T) {
621		defer PrintCurrentTest(t)()
622
623		// skip this test if git version is low
624		if git.CheckGitVersionAtLeast("2.29") != nil {
625			return
626		}
627
628		gitRepo, err := git.OpenRepository(dstPath)
629		if !assert.NoError(t, err) {
630			return
631		}
632		defer gitRepo.Close()
633
634		var (
635			pr1, pr2 *models.PullRequest
636			commit   string
637		)
638		repo, err := repo_model.GetRepositoryByOwnerAndName(ctx.Username, ctx.Reponame)
639		if !assert.NoError(t, err) {
640			return
641		}
642
643		pullNum := unittest.GetCount(t, &models.PullRequest{})
644
645		t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch))
646
647		t.Run("AddCommit", func(t *testing.T) {
648			err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0666)
649			if !assert.NoError(t, err) {
650				return
651			}
652
653			err = git.AddChanges(dstPath, true)
654			assert.NoError(t, err)
655
656			err = git.CommitChanges(dstPath, git.CommitChangesOptions{
657				Committer: &git.Signature{
658					Email: "user2@example.com",
659					Name:  "user2",
660					When:  time.Now(),
661				},
662				Author: &git.Signature{
663					Email: "user2@example.com",
664					Name:  "user2",
665					When:  time.Now(),
666				},
667				Message: "Testing commit 1",
668			})
669			assert.NoError(t, err)
670			commit, err = gitRepo.GetRefCommitID("HEAD")
671			assert.NoError(t, err)
672		})
673
674		t.Run("Push", func(t *testing.T) {
675			_, err := git.NewCommand("push", "origin", "HEAD:refs/for/master", "-o", "topic="+headBranch).RunInDir(dstPath)
676			if !assert.NoError(t, err) {
677				return
678			}
679			unittest.AssertCount(t, &models.PullRequest{}, pullNum+1)
680			pr1 = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{
681				HeadRepoID: repo.ID,
682				Flow:       models.PullRequestFlowAGit,
683			}).(*models.PullRequest)
684			if !assert.NotEmpty(t, pr1) {
685				return
686			}
687			prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t)
688			if !assert.NoError(t, err) {
689				return
690			}
691			assert.Equal(t, "user2/"+headBranch, pr1.HeadBranch)
692			assert.Equal(t, false, prMsg.HasMerged)
693			assert.Contains(t, "Testing commit 1", prMsg.Body)
694			assert.Equal(t, commit, prMsg.Head.Sha)
695
696			_, err = git.NewCommand("push", "origin", "HEAD:refs/for/master/test/"+headBranch).RunInDir(dstPath)
697			if !assert.NoError(t, err) {
698				return
699			}
700			unittest.AssertCount(t, &models.PullRequest{}, pullNum+2)
701			pr2 = unittest.AssertExistsAndLoadBean(t, &models.PullRequest{
702				HeadRepoID: repo.ID,
703				Index:      pr1.Index + 1,
704				Flow:       models.PullRequestFlowAGit,
705			}).(*models.PullRequest)
706			if !assert.NotEmpty(t, pr2) {
707				return
708			}
709			prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t)
710			if !assert.NoError(t, err) {
711				return
712			}
713			assert.Equal(t, "user2/test/"+headBranch, pr2.HeadBranch)
714			assert.Equal(t, false, prMsg.HasMerged)
715		})
716
717		if pr1 == nil || pr2 == nil {
718			return
719		}
720
721		t.Run("AddCommit2", func(t *testing.T) {
722			err := os.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content \n ## test content 2"), 0666)
723			if !assert.NoError(t, err) {
724				return
725			}
726
727			err = git.AddChanges(dstPath, true)
728			assert.NoError(t, err)
729
730			err = git.CommitChanges(dstPath, git.CommitChangesOptions{
731				Committer: &git.Signature{
732					Email: "user2@example.com",
733					Name:  "user2",
734					When:  time.Now(),
735				},
736				Author: &git.Signature{
737					Email: "user2@example.com",
738					Name:  "user2",
739					When:  time.Now(),
740				},
741				Message: "Testing commit 2",
742			})
743			assert.NoError(t, err)
744			commit, err = gitRepo.GetRefCommitID("HEAD")
745			assert.NoError(t, err)
746		})
747
748		t.Run("Push2", func(t *testing.T) {
749			_, err := git.NewCommand("push", "origin", "HEAD:refs/for/master", "-o", "topic="+headBranch).RunInDir(dstPath)
750			if !assert.NoError(t, err) {
751				return
752			}
753			unittest.AssertCount(t, &models.PullRequest{}, pullNum+2)
754			prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t)
755			if !assert.NoError(t, err) {
756				return
757			}
758			assert.Equal(t, false, prMsg.HasMerged)
759			assert.Equal(t, commit, prMsg.Head.Sha)
760
761			_, err = git.NewCommand("push", "origin", "HEAD:refs/for/master/test/"+headBranch).RunInDir(dstPath)
762			if !assert.NoError(t, err) {
763				return
764			}
765			unittest.AssertCount(t, &models.PullRequest{}, pullNum+2)
766			prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t)
767			if !assert.NoError(t, err) {
768				return
769			}
770			assert.Equal(t, false, prMsg.HasMerged)
771			assert.Equal(t, commit, prMsg.Head.Sha)
772		})
773		t.Run("Merge", doAPIMergePullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index))
774		t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master"))
775	}
776}
777