code.gitea.io/gitea@v1.22.3/tests/integration/git_test.go (about)

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