Files
Gitea/tests/integration/api_repo_files_change_test.go
T
wxiaoguang 9dc04289aa refactor: make git package handle all git operations (#38543)
before: gitrepo vs git packages
after: git package fully handle all git operations

by the way, use `WithRepo(repo)` instead of `WithDir(repo.Path)` to hide
path details.

benefits:

1. remove all unnecessary wrappers, developers no need to struggle with
"which package should be used"
2. simplify code, RepositoryFacade can (will) be used everywhere, all
"path" details are (will be) hidden
2026-07-20 16:07:38 +00:00

392 lines
19 KiB
Go

// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package integration
import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"testing"
auth_model "gitea.dev/models/auth"
repo_model "gitea.dev/models/repo"
"gitea.dev/models/unittest"
user_model "gitea.dev/models/user"
"gitea.dev/modules/git"
"gitea.dev/modules/setting"
api "gitea.dev/modules/structs"
"gitea.dev/services/context"
"github.com/stretchr/testify/assert"
)
func getChangeFilesOptions() *api.ChangeFilesOptions {
newContent := "This is new text"
updateContent := "This is updated text"
newContentEncoded := base64.StdEncoding.EncodeToString([]byte(newContent))
updateContentEncoded := base64.StdEncoding.EncodeToString([]byte(updateContent))
return &api.ChangeFilesOptions{
FileOptions: api.FileOptions{
BranchName: "master",
NewBranchName: "master",
Message: "My update of new/file.txt",
Author: api.Identity{
Name: "Anne Doe",
Email: "annedoe@example.com",
},
Committer: api.Identity{
Name: "John Doe",
Email: "johndoe@example.com",
},
},
Files: []*api.ChangeFileOperation{
{
Operation: "create",
ContentBase64: newContentEncoded,
},
{
Operation: "update",
ContentBase64: updateContentEncoded,
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
},
{
Operation: "delete",
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
},
},
}
}
func TestAPIChangeFiles(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) // owner of the repo1 & repo16
org3 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 3}) // owner of the repo3, is an org
user4 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 4}) // owner of neither repos
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) // public repo
repo3 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3}) // public repo
repo16 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 16}) // private repo
fileID := 0
// Get user2's token
session := loginUser(t, user2.Name)
token2 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
// Get user4's token
session = loginUser(t, user4.Name)
token4 := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
// Test changing files in repo1 which user2 owns, try both with branch and empty branch
for _, branch := range []string{"master", ""} {
t.Run("Branch-"+branch, func(t *testing.T) {
fileID++
createTreePath := fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath := fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath := fmt.Sprintf("delete/file%d.txt", fileID)
_, _ = createFile(user2, repo1, updateTreePath)
_, _ = createFile(user2, repo1, deleteTreePath)
changeFilesOptions := getChangeFilesOptions()
changeFilesOptions.BranchName = branch
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
req := NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name), &changeFilesOptions).
AddTokenAuth(token2)
resp := MakeRequest(t, req, http.StatusCreated)
gitRepo, _ := git.OpenRepository(repo1)
defer gitRepo.Close()
commitID, _ := gitRepo.GetBranchCommitID(t.Context(), changeFilesOptions.NewBranchName)
createLasCommit, _ := gitRepo.GetCommitByPath(t.Context(), createTreePath)
updateLastCommit, _ := gitRepo.GetCommitByPath(t.Context(), updateTreePath)
expectedCreateFileResponse := getExpectedFileResponseForCreate(apiFileResponseInfo{
repoFullName: fmt.Sprintf("%s/%s", user2.Name, repo1.Name),
commitID: commitID,
treePath: createTreePath,
lastCommitSHA: createLasCommit.ID.String(),
lastCommitterWhen: createLasCommit.Committer.When,
lastAuthorWhen: createLasCommit.Author.When,
})
expectedUpdateFileResponse := getExpectedFileResponseForUpdate(apiFileResponseInfo{
commitID: commitID,
treePath: updateTreePath,
lastCommitSHA: updateLastCommit.ID.String(),
lastCommitterWhen: updateLastCommit.Committer.When,
lastAuthorWhen: updateLastCommit.Author.When,
})
filesResponse := DecodeJSON(t, resp, &api.FilesResponse{})
normalizeFileContentResponseCommitTime(filesResponse.Files[0])
normalizeFileContentResponseCommitTime(filesResponse.Files[1])
assert.Equal(t, expectedCreateFileResponse.Content, filesResponse.Files[0]) // check create file
assert.Equal(t, expectedUpdateFileResponse.Content, filesResponse.Files[1]) // check update file
assert.Equal(t, expectedCreateFileResponse.Commit.SHA, filesResponse.Commit.SHA)
assert.Equal(t, expectedCreateFileResponse.Commit.HTMLURL, filesResponse.Commit.HTMLURL)
assert.Equal(t, expectedCreateFileResponse.Commit.Author.Email, filesResponse.Commit.Author.Email)
assert.Equal(t, expectedCreateFileResponse.Commit.Author.Name, filesResponse.Commit.Author.Name)
assert.Equal(t, expectedCreateFileResponse.Commit.Committer.Email, filesResponse.Commit.Committer.Email)
assert.Equal(t, expectedCreateFileResponse.Commit.Committer.Name, filesResponse.Commit.Committer.Name)
assert.Nil(t, filesResponse.Files[2]) // test delete file
})
}
// Test changing files in a new branch
changeFilesOptions := getChangeFilesOptions()
changeFilesOptions.BranchName = repo1.DefaultBranch
changeFilesOptions.NewBranchName = "new_branch"
fileID++
createTreePath := fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath := fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath := fmt.Sprintf("delete/file%d.txt", fileID)
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
createFile(user2, repo1, updateTreePath)
createFile(user2, repo1, deleteTreePath)
url := fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name)
req := NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
AddTokenAuth(token2)
resp := MakeRequest(t, req, http.StatusCreated)
filesResponse := DecodeJSON(t, resp, &api.FilesResponse{})
expectedCreateSHA := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
expectedCreateHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/new_branch/new/file%d.txt", fileID)
expectedCreateDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/new/file%d.txt", fileID)
expectedUpdateSHA := "08bd14b2e2852529157324de9c226b3364e76136"
expectedUpdateHTMLURL := fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/new_branch/update/file%d.txt", fileID)
expectedUpdateDownloadURL := fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/new_branch/update/file%d.txt", fileID)
assert.Equal(t, expectedCreateSHA, filesResponse.Files[0].SHA)
assert.Equal(t, expectedCreateHTMLURL, *filesResponse.Files[0].HTMLURL)
assert.Equal(t, expectedCreateDownloadURL, *filesResponse.Files[0].DownloadURL)
assert.Equal(t, expectedUpdateSHA, filesResponse.Files[1].SHA)
assert.Equal(t, expectedUpdateHTMLURL, *filesResponse.Files[1].HTMLURL)
assert.Equal(t, expectedUpdateDownloadURL, *filesResponse.Files[1].DownloadURL)
assert.Nil(t, filesResponse.Files[2])
assert.Equal(t, changeFilesOptions.Message+"\n", filesResponse.Commit.Message)
// Test fails creating a file in a branch that already exists without force
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.BranchName = repo1.DefaultBranch
changeFilesOptions.NewBranchName = "develop"
changeFilesOptions.ForcePush = false
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
createFile(user2, repo1, updateTreePath)
createFile(user2, repo1, deleteTreePath)
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name)
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusUnprocessableEntity)
assert.Contains(t, resp.Body.String(), `"message":"branch already exists [name: develop]"`)
// Test succeeds creating a file in a branch that already exists with force
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.BranchName = repo1.DefaultBranch
changeFilesOptions.NewBranchName = "develop"
changeFilesOptions.ForcePush = true
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
createFile(user2, repo1, updateTreePath)
createFile(user2, repo1, deleteTreePath)
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name)
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusCreated)
filesResponse = DecodeJSON(t, resp, &api.FilesResponse{})
expectedCreateHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/develop/new/file%d.txt", fileID)
expectedCreateDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/develop/new/file%d.txt", fileID)
expectedUpdateHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/develop/update/file%d.txt", fileID)
expectedUpdateDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/develop/update/file%d.txt", fileID)
assert.Equal(t, expectedCreateSHA, filesResponse.Files[0].SHA)
assert.Equal(t, expectedCreateHTMLURL, *filesResponse.Files[0].HTMLURL)
assert.Equal(t, expectedCreateDownloadURL, *filesResponse.Files[0].DownloadURL)
assert.Equal(t, expectedUpdateSHA, filesResponse.Files[1].SHA)
assert.Equal(t, expectedUpdateHTMLURL, *filesResponse.Files[1].HTMLURL)
assert.Equal(t, expectedUpdateDownloadURL, *filesResponse.Files[1].DownloadURL)
assert.Nil(t, filesResponse.Files[2])
assert.Equal(t, changeFilesOptions.Message+"\n", filesResponse.Commit.Message)
// Test fails creating a file in a branch that already exists with force and branch protection enabled
protectionReq := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/branch_protections", &api.BranchProtection{
RuleName: "develop",
BranchName: "develop",
Priority: 1,
EnablePush: true,
EnableForcePush: false,
}).AddTokenAuth(token2)
MakeRequest(t, protectionReq, http.StatusCreated)
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.BranchName = repo1.DefaultBranch
changeFilesOptions.NewBranchName = "develop"
changeFilesOptions.ForcePush = true
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
createFile(user2, repo1, updateTreePath)
createFile(user2, repo1, deleteTreePath)
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name)
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusForbidden)
assert.Contains(t, resp.Body.String(), `"message":"branch develop is protected from force push"`)
// Test updating a file and renaming it
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.BranchName = repo1.DefaultBranch
fileID++
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
createFile(user2, repo1, updateTreePath)
changeFilesOptions.Files = []*api.ChangeFileOperation{changeFilesOptions.Files[1]}
changeFilesOptions.Files[0].FromPath = updateTreePath
changeFilesOptions.Files[0].Path = "rename/" + updateTreePath
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusCreated)
filesResponse = DecodeJSON(t, resp, &api.FilesResponse{})
expectedUpdateSHA = "08bd14b2e2852529157324de9c226b3364e76136"
expectedUpdateHTMLURL = fmt.Sprintf(setting.AppURL+"user2/repo1/src/branch/master/rename/update/file%d.txt", fileID)
expectedUpdateDownloadURL = fmt.Sprintf(setting.AppURL+"user2/repo1/raw/branch/master/rename/update/file%d.txt", fileID)
assert.Equal(t, expectedUpdateSHA, filesResponse.Files[0].SHA)
assert.Equal(t, expectedUpdateHTMLURL, *filesResponse.Files[0].HTMLURL)
assert.Equal(t, expectedUpdateDownloadURL, *filesResponse.Files[0].DownloadURL)
// Test updating a file without a message
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.Message = ""
changeFilesOptions.BranchName = repo1.DefaultBranch
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
createFile(user2, repo1, updateTreePath)
createFile(user2, repo1, deleteTreePath)
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusCreated)
filesResponse = DecodeJSON(t, resp, &api.FilesResponse{})
expectedMessage := fmt.Sprintf("Add %v\nUpdate %v\nDelete %v\n", createTreePath, updateTreePath, deleteTreePath)
assert.Equal(t, expectedMessage, filesResponse.Commit.Message)
// Test updating a file with the wrong SHA
fileID++
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
createFile(user2, repo1, updateTreePath)
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.Files = []*api.ChangeFileOperation{changeFilesOptions.Files[1]}
changeFilesOptions.Files[0].Path = updateTreePath
correctSHA := changeFilesOptions.Files[0].SHA
changeFilesOptions.Files[0].SHA = "badsha"
req = NewRequestWithJSON(t, "POST", url, &changeFilesOptions).
AddTokenAuth(token2)
resp = MakeRequest(t, req, http.StatusUnprocessableEntity)
expectedAPIError := &context.APIError{
Message: "sha does not match [given: " + changeFilesOptions.Files[0].SHA + ", expected: " + correctSHA + "]",
URL: setting.API.SwaggerURL,
}
apiError := DecodeJSON(t, resp, &context.APIError{})
assert.Equal(t, expectedAPIError, apiError)
// Test creating a file in repo1 by user4 who does not have write access
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
createFile(user2, repo16, updateTreePath)
createFile(user2, repo16, deleteTreePath)
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo16.Name), &changeFilesOptions).
AddTokenAuth(token4)
MakeRequest(t, req, http.StatusNotFound)
// Tests a repo with no token given so will fail
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
createFile(user2, repo16, updateTreePath)
createFile(user2, repo16, deleteTreePath)
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo16.Name), &changeFilesOptions)
MakeRequest(t, req, http.StatusNotFound)
// Test using access token for a private repo that the user of the token owns
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
createFile(user2, repo16, updateTreePath)
createFile(user2, repo16, deleteTreePath)
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo16.Name), &changeFilesOptions).
AddTokenAuth(token2)
MakeRequest(t, req, http.StatusCreated)
// Test using org repo "org3/repo3" where user2 is a collaborator
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
createFile(org3, repo3, updateTreePath)
createFile(org3, repo3, deleteTreePath)
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents", org3.Name, repo3.Name), &changeFilesOptions).
AddTokenAuth(token2)
MakeRequest(t, req, http.StatusCreated)
// Test using org repo "org3/repo3" with no user token
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
createFile(org3, repo3, updateTreePath)
createFile(org3, repo3, deleteTreePath)
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents", org3.Name, repo3.Name), &changeFilesOptions)
MakeRequest(t, req, http.StatusNotFound)
// Test using repo "user2/repo1" where user4 is a NOT collaborator
fileID++
createTreePath = fmt.Sprintf("new/file%d.txt", fileID)
updateTreePath = fmt.Sprintf("update/file%d.txt", fileID)
deleteTreePath = fmt.Sprintf("delete/file%d.txt", fileID)
createFile(user2, repo1, updateTreePath)
createFile(user2, repo1, deleteTreePath)
changeFilesOptions = getChangeFilesOptions()
changeFilesOptions.Files[0].Path = createTreePath
changeFilesOptions.Files[1].Path = updateTreePath
changeFilesOptions.Files[2].Path = deleteTreePath
req = NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/contents", user2.Name, repo1.Name), &changeFilesOptions).
AddTokenAuth(token4)
MakeRequest(t, req, http.StatusForbidden)
})
}