Fixes 4762 - Content API for Creating, Updating, Deleting Files (#6314)
parent
059195b127
commit
2262811e40
@ -0,0 +1,114 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package integrations |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"path/filepath" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/base" |
||||||
|
"code.gitea.io/gitea/modules/context" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
api "code.gitea.io/sdk/gitea" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func getExpectedFileContentResponseForFileContents(branch string) *api.FileContentResponse { |
||||||
|
treePath := "README.md" |
||||||
|
sha := "4b4851ad51df6a7d9f25c979345979eaeb5b349f" |
||||||
|
return &api.FileContentResponse{ |
||||||
|
Name: filepath.Base(treePath), |
||||||
|
Path: treePath, |
||||||
|
SHA: sha, |
||||||
|
Size: 30, |
||||||
|
URL: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath, |
||||||
|
HTMLURL: setting.AppURL + "user2/repo1/blob/" + branch + "/" + treePath, |
||||||
|
GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha, |
||||||
|
DownloadURL: setting.AppURL + "user2/repo1/raw/branch/" + branch + "/" + treePath, |
||||||
|
Type: "blob", |
||||||
|
Links: &api.FileLinksResponse{ |
||||||
|
Self: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath, |
||||||
|
GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha, |
||||||
|
HTMLURL: setting.AppURL + "user2/repo1/blob/" + branch + "/" + treePath, |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestAPIGetFileContents(t *testing.T) { |
||||||
|
prepareTestEnv(t) |
||||||
|
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16
|
||||||
|
user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3, is an org
|
||||||
|
user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos
|
||||||
|
repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo
|
||||||
|
repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo
|
||||||
|
repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
|
||||||
|
treePath := "README.md" |
||||||
|
|
||||||
|
// Get user2's token
|
||||||
|
session := loginUser(t, user2.Name) |
||||||
|
token2 := getTokenForLoggedInUser(t, session) |
||||||
|
session = emptyTestSession(t) |
||||||
|
// Get user4's token
|
||||||
|
session = loginUser(t, user4.Name) |
||||||
|
token4 := getTokenForLoggedInUser(t, session) |
||||||
|
session = emptyTestSession(t) |
||||||
|
|
||||||
|
// Make a second master branch in repo1
|
||||||
|
repo1.CreateNewBranch(user2, repo1.DefaultBranch, "master2") |
||||||
|
|
||||||
|
// ref is default branch
|
||||||
|
branch := repo1.DefaultBranch |
||||||
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, branch) |
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK) |
||||||
|
var fileContentResponse api.FileContentResponse |
||||||
|
DecodeJSON(t, resp, &fileContentResponse) |
||||||
|
assert.NotNil(t, fileContentResponse) |
||||||
|
expectedFileContentResponse := getExpectedFileContentResponseForFileContents(branch) |
||||||
|
assert.EqualValues(t, *expectedFileContentResponse, fileContentResponse) |
||||||
|
|
||||||
|
// No ref
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath) |
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK) |
||||||
|
DecodeJSON(t, resp, &fileContentResponse) |
||||||
|
assert.NotNil(t, fileContentResponse) |
||||||
|
expectedFileContentResponse = getExpectedFileContentResponseForFileContents(repo1.DefaultBranch) |
||||||
|
assert.EqualValues(t, *expectedFileContentResponse, fileContentResponse) |
||||||
|
|
||||||
|
// ref is master2
|
||||||
|
branch = "master2" |
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, branch) |
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK) |
||||||
|
DecodeJSON(t, resp, &fileContentResponse) |
||||||
|
assert.NotNil(t, fileContentResponse) |
||||||
|
expectedFileContentResponse = getExpectedFileContentResponseForFileContents("master2") |
||||||
|
assert.EqualValues(t, *expectedFileContentResponse, fileContentResponse) |
||||||
|
|
||||||
|
// Test file contents a file with the wrong branch
|
||||||
|
branch = "badbranch" |
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, branch) |
||||||
|
resp = session.MakeRequest(t, req, http.StatusInternalServerError) |
||||||
|
expectedAPIError := context.APIError{ |
||||||
|
Message: "object does not exist [id: " + branch + ", rel_path: ]", |
||||||
|
URL: base.DocURL, |
||||||
|
} |
||||||
|
var apiError context.APIError |
||||||
|
DecodeJSON(t, resp, &apiError) |
||||||
|
assert.Equal(t, expectedAPIError, apiError) |
||||||
|
|
||||||
|
// Test accessing private branch with user token that does not have access - should fail
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
|
||||||
|
// Test access private branch of owner of token
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/readme.md?token=%s", user2.Name, repo16.Name, token2) |
||||||
|
session.MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
// Test access of org user3 private repo file by owner user2
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2) |
||||||
|
session.MakeRequest(t, req, http.StatusOK) |
||||||
|
} |
@ -0,0 +1,215 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package integrations |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/base64" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"path/filepath" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/base" |
||||||
|
"code.gitea.io/gitea/modules/context" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
api "code.gitea.io/sdk/gitea" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func getCreateFileOptions() api.CreateFileOptions { |
||||||
|
content := "This is new text" |
||||||
|
contentEncoded := base64.StdEncoding.EncodeToString([]byte(content)) |
||||||
|
return api.CreateFileOptions{ |
||||||
|
FileOptions: api.FileOptions{ |
||||||
|
BranchName: "master", |
||||||
|
NewBranchName: "master", |
||||||
|
Message: "Creates new/file.txt", |
||||||
|
Author: api.Identity{ |
||||||
|
Name: "John Doe", |
||||||
|
Email: "johndoe@example.com", |
||||||
|
}, |
||||||
|
Committer: api.Identity{ |
||||||
|
Name: "Jane Doe", |
||||||
|
Email: "janedoe@example.com", |
||||||
|
}, |
||||||
|
}, |
||||||
|
Content: contentEncoded, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func getExpectedFileResponseForCreate(commitID, treePath string) *api.FileResponse { |
||||||
|
sha := "a635aa942442ddfdba07468cf9661c08fbdf0ebf" |
||||||
|
return &api.FileResponse{ |
||||||
|
Content: &api.FileContentResponse{ |
||||||
|
Name: filepath.Base(treePath), |
||||||
|
Path: treePath, |
||||||
|
SHA: sha, |
||||||
|
Size: 16, |
||||||
|
URL: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath, |
||||||
|
HTMLURL: setting.AppURL + "user2/repo1/blob/master/" + treePath, |
||||||
|
GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha, |
||||||
|
DownloadURL: setting.AppURL + "user2/repo1/raw/branch/master/" + treePath, |
||||||
|
Type: "blob", |
||||||
|
Links: &api.FileLinksResponse{ |
||||||
|
Self: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath, |
||||||
|
GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha, |
||||||
|
HTMLURL: setting.AppURL + "user2/repo1/blob/master/" + treePath, |
||||||
|
}, |
||||||
|
}, |
||||||
|
Commit: &api.FileCommitResponse{ |
||||||
|
CommitMeta: api.CommitMeta{ |
||||||
|
URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID, |
||||||
|
SHA: commitID, |
||||||
|
}, |
||||||
|
HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID, |
||||||
|
Author: &api.CommitUser{ |
||||||
|
Identity: api.Identity{ |
||||||
|
Name: "Jane Doe", |
||||||
|
Email: "janedoe@example.com", |
||||||
|
}, |
||||||
|
}, |
||||||
|
Committer: &api.CommitUser{ |
||||||
|
Identity: api.Identity{ |
||||||
|
Name: "John Doe", |
||||||
|
Email: "johndoe@example.com", |
||||||
|
}, |
||||||
|
}, |
||||||
|
Message: "Updates README.md\n", |
||||||
|
}, |
||||||
|
Verification: &api.PayloadCommitVerification{ |
||||||
|
Verified: false, |
||||||
|
Reason: "unsigned", |
||||||
|
Signature: "", |
||||||
|
Payload: "", |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestAPICreateFile(t *testing.T) { |
||||||
|
prepareTestEnv(t) |
||||||
|
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16
|
||||||
|
user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3, is an org
|
||||||
|
user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos
|
||||||
|
repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo
|
||||||
|
repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo
|
||||||
|
repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
|
||||||
|
fileID := 0 |
||||||
|
|
||||||
|
// Get user2's token
|
||||||
|
session := loginUser(t, user2.Name) |
||||||
|
token2 := getTokenForLoggedInUser(t, session) |
||||||
|
session = emptyTestSession(t) |
||||||
|
// Get user4's token
|
||||||
|
session = loginUser(t, user4.Name) |
||||||
|
token4 := getTokenForLoggedInUser(t, session) |
||||||
|
session = emptyTestSession(t) |
||||||
|
|
||||||
|
// Test creating a file in repo1 which user2 owns, try both with branch and empty branch
|
||||||
|
for _, branch := range [...]string{ |
||||||
|
"master", // Branch
|
||||||
|
"", // Empty branch
|
||||||
|
} { |
||||||
|
createFileOptions := getCreateFileOptions() |
||||||
|
createFileOptions.BranchName = branch |
||||||
|
fileID++ |
||||||
|
treePath := fmt.Sprintf("new/file%d.txt", fileID) |
||||||
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2) |
||||||
|
req := NewRequestWithJSON(t, "POST", url, &createFileOptions) |
||||||
|
resp := session.MakeRequest(t, req, http.StatusCreated) |
||||||
|
gitRepo, _ := git.OpenRepository(repo1.RepoPath()) |
||||||
|
commitID, _ := gitRepo.GetBranchCommitID(createFileOptions.NewBranchName) |
||||||
|
expectedFileResponse := getExpectedFileResponseForCreate(commitID, treePath) |
||||||
|
var fileResponse api.FileResponse |
||||||
|
DecodeJSON(t, resp, &fileResponse) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name) |
||||||
|
} |
||||||
|
|
||||||
|
// Test creating a file in a new branch
|
||||||
|
createFileOptions := getCreateFileOptions() |
||||||
|
createFileOptions.BranchName = repo1.DefaultBranch |
||||||
|
createFileOptions.NewBranchName = "new_branch" |
||||||
|
fileID++ |
||||||
|
treePath := fmt.Sprintf("new/file%d.txt", fileID) |
||||||
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2) |
||||||
|
req := NewRequestWithJSON(t, "POST", url, &createFileOptions) |
||||||
|
resp := session.MakeRequest(t, req, http.StatusCreated) |
||||||
|
var fileResponse api.FileResponse |
||||||
|
DecodeJSON(t, resp, &fileResponse) |
||||||
|
expectedSHA := "a635aa942442ddfdba07468cf9661c08fbdf0ebf" |
||||||
|
expectedHTMLURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/blob/new_branch/new/file%d.txt", fileID) |
||||||
|
expectedDownloadURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/raw/branch/new_branch/new/file%d.txt", fileID) |
||||||
|
assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA) |
||||||
|
assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL) |
||||||
|
assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL) |
||||||
|
|
||||||
|
// Test trying to create a file that already exists, should fail
|
||||||
|
createFileOptions = getCreateFileOptions() |
||||||
|
treePath = "README.md" |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2) |
||||||
|
req = NewRequestWithJSON(t, "POST", url, &createFileOptions) |
||||||
|
resp = session.MakeRequest(t, req, http.StatusInternalServerError) |
||||||
|
expectedAPIError := context.APIError{ |
||||||
|
Message: "repository file already exists [path: " + treePath + "]", |
||||||
|
URL: base.DocURL, |
||||||
|
} |
||||||
|
var apiError context.APIError |
||||||
|
DecodeJSON(t, resp, &apiError) |
||||||
|
assert.Equal(t, expectedAPIError, apiError) |
||||||
|
|
||||||
|
// Test creating a file in repo1 by user4 who does not have write access
|
||||||
|
createFileOptions = getCreateFileOptions() |
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("new/file%d.txt", fileID) |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4) |
||||||
|
req = NewRequestWithJSON(t, "POST", url, &createFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
|
||||||
|
// Tests a repo with no token given so will fail
|
||||||
|
createFileOptions = getCreateFileOptions() |
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("new/file%d.txt", fileID) |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath) |
||||||
|
req = NewRequestWithJSON(t, "POST", url, &createFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
|
||||||
|
// Test using access token for a private repo that the user of the token owns
|
||||||
|
createFileOptions = getCreateFileOptions() |
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("new/file%d.txt", fileID) |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token2) |
||||||
|
req = NewRequestWithJSON(t, "POST", url, &createFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusCreated) |
||||||
|
|
||||||
|
// Test using org repo "user3/repo3" where user2 is a collaborator
|
||||||
|
createFileOptions = getCreateFileOptions() |
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("new/file%d.txt", fileID) |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2) |
||||||
|
req = NewRequestWithJSON(t, "POST", url, &createFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusCreated) |
||||||
|
|
||||||
|
// Test using org repo "user3/repo3" with no user token
|
||||||
|
createFileOptions = getCreateFileOptions() |
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("new/file%d.txt", fileID) |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user3.Name, repo3.Name, treePath) |
||||||
|
req = NewRequestWithJSON(t, "POST", url, &createFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
|
||||||
|
// Test using repo "user2/repo1" where user4 is a NOT collaborator
|
||||||
|
createFileOptions = getCreateFileOptions() |
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("new/file%d.txt", fileID) |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token4) |
||||||
|
req = NewRequestWithJSON(t, "POST", url, &createFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusForbidden) |
||||||
|
} |
@ -0,0 +1,163 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package integrations |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/base" |
||||||
|
"code.gitea.io/gitea/modules/context" |
||||||
|
api "code.gitea.io/sdk/gitea" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func getDeleteFileOptions() *api.DeleteFileOptions { |
||||||
|
return &api.DeleteFileOptions{ |
||||||
|
FileOptions: api.FileOptions{ |
||||||
|
BranchName: "master", |
||||||
|
NewBranchName: "master", |
||||||
|
Message: "Updates new/file.txt", |
||||||
|
Author: api.Identity{ |
||||||
|
Name: "John Doe", |
||||||
|
Email: "johndoe@example.com", |
||||||
|
}, |
||||||
|
Committer: api.Identity{ |
||||||
|
Name: "Jane Doe", |
||||||
|
Email: "janedoe@example.com", |
||||||
|
}, |
||||||
|
}, |
||||||
|
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestAPIDeleteFile(t *testing.T) { |
||||||
|
prepareTestEnv(t) |
||||||
|
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16
|
||||||
|
user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3, is an org
|
||||||
|
user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos
|
||||||
|
repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo
|
||||||
|
repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo
|
||||||
|
repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
|
||||||
|
fileID := 0 |
||||||
|
|
||||||
|
// Get user2's token
|
||||||
|
session := loginUser(t, user2.Name) |
||||||
|
token2 := getTokenForLoggedInUser(t, session) |
||||||
|
session = emptyTestSession(t) |
||||||
|
// Get user4's token
|
||||||
|
session = loginUser(t, user4.Name) |
||||||
|
token4 := getTokenForLoggedInUser(t, session) |
||||||
|
session = emptyTestSession(t) |
||||||
|
|
||||||
|
// Test deleting a file in repo1 which user2 owns, try both with branch and empty branch
|
||||||
|
for _, branch := range [...]string{ |
||||||
|
"master", // Branch
|
||||||
|
"", // Empty branch
|
||||||
|
} { |
||||||
|
fileID++ |
||||||
|
treePath := fmt.Sprintf("delete/file%d.txt", fileID) |
||||||
|
createFile(user2, repo1, treePath) |
||||||
|
deleteFileOptions := getDeleteFileOptions() |
||||||
|
deleteFileOptions.BranchName = branch |
||||||
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2) |
||||||
|
req := NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions) |
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK) |
||||||
|
var fileResponse api.FileResponse |
||||||
|
DecodeJSON(t, resp, &fileResponse) |
||||||
|
assert.NotNil(t, fileResponse) |
||||||
|
assert.Nil(t, fileResponse.Content) |
||||||
|
} |
||||||
|
|
||||||
|
// Test deleting file and making the delete in a new branch
|
||||||
|
fileID++ |
||||||
|
treePath := fmt.Sprintf("delete/file%d.txt", fileID) |
||||||
|
createFile(user2, repo1, treePath) |
||||||
|
deleteFileOptions := getDeleteFileOptions() |
||||||
|
deleteFileOptions.BranchName = repo1.DefaultBranch |
||||||
|
deleteFileOptions.NewBranchName = "new_branch" |
||||||
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2) |
||||||
|
req := NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions) |
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK) |
||||||
|
var fileResponse api.FileResponse |
||||||
|
DecodeJSON(t, resp, &fileResponse) |
||||||
|
assert.NotNil(t, fileResponse) |
||||||
|
assert.Nil(t, fileResponse.Content) |
||||||
|
|
||||||
|
// Test deleting a file with the wrong SHA
|
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("delete/file%d.txt", fileID) |
||||||
|
createFile(user2, repo1, treePath) |
||||||
|
deleteFileOptions = getDeleteFileOptions() |
||||||
|
correctSHA := deleteFileOptions.SHA |
||||||
|
deleteFileOptions.SHA = "badsha" |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2) |
||||||
|
req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions) |
||||||
|
resp = session.MakeRequest(t, req, http.StatusInternalServerError) |
||||||
|
expectedAPIError := context.APIError{ |
||||||
|
Message: "sha does not match [given: " + deleteFileOptions.SHA + ", expected: " + correctSHA + "]", |
||||||
|
URL: base.DocURL, |
||||||
|
} |
||||||
|
var apiError context.APIError |
||||||
|
DecodeJSON(t, resp, &apiError) |
||||||
|
assert.Equal(t, expectedAPIError, apiError) |
||||||
|
|
||||||
|
// Test creating a file in repo1 by user4 who does not have write access
|
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("delete/file%d.txt", fileID) |
||||||
|
createFile(user2, repo16, treePath) |
||||||
|
deleteFileOptions = getDeleteFileOptions() |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4) |
||||||
|
req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
|
||||||
|
// Tests a repo with no token given so will fail
|
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("delete/file%d.txt", fileID) |
||||||
|
createFile(user2, repo16, treePath) |
||||||
|
deleteFileOptions = getDeleteFileOptions() |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath) |
||||||
|
req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
|
||||||
|
// Test using access token for a private repo that the user of the token owns
|
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("delete/file%d.txt", fileID) |
||||||
|
createFile(user2, repo16, treePath) |
||||||
|
deleteFileOptions = getDeleteFileOptions() |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token2) |
||||||
|
req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
// Test using org repo "user3/repo3" where user2 is a collaborator
|
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("delete/file%d.txt", fileID) |
||||||
|
createFile(user3, repo3, treePath) |
||||||
|
deleteFileOptions = getDeleteFileOptions() |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2) |
||||||
|
req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
// Test using org repo "user3/repo3" with no user token
|
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("delete/file%d.txt", fileID) |
||||||
|
createFile(user3, repo3, treePath) |
||||||
|
deleteFileOptions = getDeleteFileOptions() |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user3.Name, repo3.Name, treePath) |
||||||
|
req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
|
||||||
|
// Test using repo "user2/repo1" where user4 is a NOT collaborator
|
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("delete/file%d.txt", fileID) |
||||||
|
createFile(user2, repo1, treePath) |
||||||
|
deleteFileOptions = getDeleteFileOptions() |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token4) |
||||||
|
req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusForbidden) |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package integrations |
||||||
|
|
||||||
|
import ( |
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/repofiles" |
||||||
|
api "code.gitea.io/sdk/gitea" |
||||||
|
) |
||||||
|
|
||||||
|
func createFileInBranch(user *models.User, repo *models.Repository, treePath, branchName string) (*api.FileResponse, error) { |
||||||
|
opts := &repofiles.UpdateRepoFileOptions{ |
||||||
|
OldBranch: branchName, |
||||||
|
TreePath: treePath, |
||||||
|
Content: "This is a NEW file", |
||||||
|
IsNewFile: true, |
||||||
|
Author: nil, |
||||||
|
Committer: nil, |
||||||
|
} |
||||||
|
return repofiles.CreateOrUpdateRepoFile(repo, user, opts) |
||||||
|
} |
||||||
|
|
||||||
|
func createFile(user *models.User, repo *models.Repository, treePath string) (*api.FileResponse, error) { |
||||||
|
return createFileInBranch(user, repo, treePath, repo.DefaultBranch) |
||||||
|
} |
@ -0,0 +1,234 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package integrations |
||||||
|
|
||||||
|
import ( |
||||||
|
"encoding/base64" |
||||||
|
"fmt" |
||||||
|
"net/http" |
||||||
|
"path/filepath" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/base" |
||||||
|
"code.gitea.io/gitea/modules/context" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
api "code.gitea.io/sdk/gitea" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func getUpdateFileOptions() *api.UpdateFileOptions { |
||||||
|
content := "This is updated text" |
||||||
|
contentEncoded := base64.StdEncoding.EncodeToString([]byte(content)) |
||||||
|
return &api.UpdateFileOptions{ |
||||||
|
DeleteFileOptions: *getDeleteFileOptions(), |
||||||
|
Content: contentEncoded, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func getExpectedFileResponseForUpdate(commitID, treePath string) *api.FileResponse { |
||||||
|
sha := "08bd14b2e2852529157324de9c226b3364e76136" |
||||||
|
return &api.FileResponse{ |
||||||
|
Content: &api.FileContentResponse{ |
||||||
|
Name: filepath.Base(treePath), |
||||||
|
Path: treePath, |
||||||
|
SHA: sha, |
||||||
|
Size: 20, |
||||||
|
URL: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath, |
||||||
|
HTMLURL: setting.AppURL + "user2/repo1/blob/master/" + treePath, |
||||||
|
GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha, |
||||||
|
DownloadURL: setting.AppURL + "user2/repo1/raw/branch/master/" + treePath, |
||||||
|
Type: "blob", |
||||||
|
Links: &api.FileLinksResponse{ |
||||||
|
Self: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath, |
||||||
|
GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha, |
||||||
|
HTMLURL: setting.AppURL + "user2/repo1/blob/master/" + treePath, |
||||||
|
}, |
||||||
|
}, |
||||||
|
Commit: &api.FileCommitResponse{ |
||||||
|
CommitMeta: api.CommitMeta{ |
||||||
|
URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID, |
||||||
|
SHA: commitID, |
||||||
|
}, |
||||||
|
HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID, |
||||||
|
Author: &api.CommitUser{ |
||||||
|
Identity: api.Identity{ |
||||||
|
Name: "Jane Doe", |
||||||
|
Email: "janedoe@example.com", |
||||||
|
}, |
||||||
|
}, |
||||||
|
Committer: &api.CommitUser{ |
||||||
|
Identity: api.Identity{ |
||||||
|
Name: "John Doe", |
||||||
|
Email: "johndoe@example.com", |
||||||
|
}, |
||||||
|
}, |
||||||
|
Message: "Updates README.md\n", |
||||||
|
}, |
||||||
|
Verification: &api.PayloadCommitVerification{ |
||||||
|
Verified: false, |
||||||
|
Reason: "unsigned", |
||||||
|
Signature: "", |
||||||
|
Payload: "", |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestAPIUpdateFile(t *testing.T) { |
||||||
|
prepareTestEnv(t) |
||||||
|
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16
|
||||||
|
user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3, is an org
|
||||||
|
user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos
|
||||||
|
repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo
|
||||||
|
repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo
|
||||||
|
repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
|
||||||
|
fileID := 0 |
||||||
|
|
||||||
|
// Get user2's token
|
||||||
|
session := loginUser(t, user2.Name) |
||||||
|
token2 := getTokenForLoggedInUser(t, session) |
||||||
|
session = emptyTestSession(t) |
||||||
|
// Get user4's token
|
||||||
|
session = loginUser(t, user4.Name) |
||||||
|
token4 := getTokenForLoggedInUser(t, session) |
||||||
|
session = emptyTestSession(t) |
||||||
|
|
||||||
|
// Test updating a file in repo1 which user2 owns, try both with branch and empty branch
|
||||||
|
for _, branch := range [...]string{ |
||||||
|
"master", // Branch
|
||||||
|
"", // Empty branch
|
||||||
|
} { |
||||||
|
fileID++ |
||||||
|
treePath := fmt.Sprintf("update/file%d.txt", fileID) |
||||||
|
createFile(user2, repo1, treePath) |
||||||
|
updateFileOptions := getUpdateFileOptions() |
||||||
|
updateFileOptions.BranchName = branch |
||||||
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2) |
||||||
|
req := NewRequestWithJSON(t, "PUT", url, &updateFileOptions) |
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK) |
||||||
|
gitRepo, _ := git.OpenRepository(repo1.RepoPath()) |
||||||
|
commitID, _ := gitRepo.GetBranchCommitID(updateFileOptions.NewBranchName) |
||||||
|
expectedFileResponse := getExpectedFileResponseForUpdate(commitID, treePath) |
||||||
|
var fileResponse api.FileResponse |
||||||
|
DecodeJSON(t, resp, &fileResponse) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name) |
||||||
|
} |
||||||
|
|
||||||
|
// Test updating a file in a new branch
|
||||||
|
updateFileOptions := getUpdateFileOptions() |
||||||
|
updateFileOptions.BranchName = repo1.DefaultBranch |
||||||
|
updateFileOptions.NewBranchName = "new_branch" |
||||||
|
fileID++ |
||||||
|
treePath := fmt.Sprintf("update/file%d.txt", fileID) |
||||||
|
createFile(user2, repo1, treePath) |
||||||
|
url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2) |
||||||
|
req := NewRequestWithJSON(t, "PUT", url, &updateFileOptions) |
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK) |
||||||
|
var fileResponse api.FileResponse |
||||||
|
DecodeJSON(t, resp, &fileResponse) |
||||||
|
expectedSHA := "08bd14b2e2852529157324de9c226b3364e76136" |
||||||
|
expectedHTMLURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/blob/new_branch/update/file%d.txt", fileID) |
||||||
|
expectedDownloadURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/raw/branch/new_branch/update/file%d.txt", fileID) |
||||||
|
assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA) |
||||||
|
assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL) |
||||||
|
assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL) |
||||||
|
|
||||||
|
// Test updating a file and renaming it
|
||||||
|
updateFileOptions = getUpdateFileOptions() |
||||||
|
updateFileOptions.BranchName = repo1.DefaultBranch |
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("update/file%d.txt", fileID) |
||||||
|
createFile(user2, repo1, treePath) |
||||||
|
updateFileOptions.FromPath = treePath |
||||||
|
treePath = "rename/" + treePath |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2) |
||||||
|
req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions) |
||||||
|
resp = session.MakeRequest(t, req, http.StatusOK) |
||||||
|
DecodeJSON(t, resp, &fileResponse) |
||||||
|
expectedSHA = "08bd14b2e2852529157324de9c226b3364e76136" |
||||||
|
expectedHTMLURL = fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/blob/master/rename/update/file%d.txt", fileID) |
||||||
|
expectedDownloadURL = fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/raw/branch/master/rename/update/file%d.txt", fileID) |
||||||
|
assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA) |
||||||
|
assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL) |
||||||
|
assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL) |
||||||
|
|
||||||
|
// Test updating a file with the wrong SHA
|
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("update/file%d.txt", fileID) |
||||||
|
createFile(user2, repo1, treePath) |
||||||
|
updateFileOptions = getUpdateFileOptions() |
||||||
|
correctSHA := updateFileOptions.SHA |
||||||
|
updateFileOptions.SHA = "badsha" |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2) |
||||||
|
req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions) |
||||||
|
resp = session.MakeRequest(t, req, http.StatusInternalServerError) |
||||||
|
expectedAPIError := context.APIError{ |
||||||
|
Message: "sha does not match [given: " + updateFileOptions.SHA + ", expected: " + correctSHA + "]", |
||||||
|
URL: base.DocURL, |
||||||
|
} |
||||||
|
var apiError context.APIError |
||||||
|
DecodeJSON(t, resp, &apiError) |
||||||
|
assert.Equal(t, expectedAPIError, apiError) |
||||||
|
|
||||||
|
// Test creating a file in repo1 by user4 who does not have write access
|
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("update/file%d.txt", fileID) |
||||||
|
createFile(user2, repo16, treePath) |
||||||
|
updateFileOptions = getUpdateFileOptions() |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4) |
||||||
|
req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
|
||||||
|
// Tests a repo with no token given so will fail
|
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("update/file%d.txt", fileID) |
||||||
|
createFile(user2, repo16, treePath) |
||||||
|
updateFileOptions = getUpdateFileOptions() |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath) |
||||||
|
req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
|
||||||
|
// Test using access token for a private repo that the user of the token owns
|
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("update/file%d.txt", fileID) |
||||||
|
createFile(user2, repo16, treePath) |
||||||
|
updateFileOptions = getUpdateFileOptions() |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token2) |
||||||
|
req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
// Test using org repo "user3/repo3" where user2 is a collaborator
|
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("update/file%d.txt", fileID) |
||||||
|
createFile(user3, repo3, treePath) |
||||||
|
updateFileOptions = getUpdateFileOptions() |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2) |
||||||
|
req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
// Test using org repo "user3/repo3" with no user token
|
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("update/file%d.txt", fileID) |
||||||
|
createFile(user3, repo3, treePath) |
||||||
|
updateFileOptions = getUpdateFileOptions() |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user3.Name, repo3.Name, treePath) |
||||||
|
req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
|
||||||
|
// Test using repo "user2/repo1" where user4 is a NOT collaborator
|
||||||
|
fileID++ |
||||||
|
treePath = fmt.Sprintf("update/file%d.txt", fileID) |
||||||
|
createFile(user2, repo1, treePath) |
||||||
|
updateFileOptions = getUpdateFileOptions() |
||||||
|
url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token4) |
||||||
|
req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions) |
||||||
|
session.MakeRequest(t, req, http.StatusForbidden) |
||||||
|
} |
@ -0,0 +1,76 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package integrations |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
api "code.gitea.io/sdk/gitea" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestAPIReposGitBlobs(t *testing.T) { |
||||||
|
prepareTestEnv(t) |
||||||
|
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16
|
||||||
|
user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3
|
||||||
|
user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos
|
||||||
|
repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo
|
||||||
|
repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo
|
||||||
|
repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
|
||||||
|
repo1ReadmeSHA := "65f1bf27bc3bf70f64657658635e66094edbcb4d" |
||||||
|
repo3ReadmeSHA := "d56a3073c1dbb7b15963110a049d50cdb5db99fc" |
||||||
|
repo16ReadmeSHA := "f90451c72ef61a7645293d17b47be7a8e983da57" |
||||||
|
badSHA := "0000000000000000000000000000000000000000" |
||||||
|
|
||||||
|
// Login as User2.
|
||||||
|
session := loginUser(t, user2.Name) |
||||||
|
token := getTokenForLoggedInUser(t, session) |
||||||
|
session = emptyTestSession(t) // don't want anyone logged in for this
|
||||||
|
|
||||||
|
// Test a public repo that anyone can GET the blob of
|
||||||
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo1.Name, repo1ReadmeSHA) |
||||||
|
resp := session.MakeRequest(t, req, http.StatusOK) |
||||||
|
var gitBlobResponse api.GitBlobResponse |
||||||
|
DecodeJSON(t, resp, &gitBlobResponse) |
||||||
|
assert.NotNil(t, gitBlobResponse) |
||||||
|
expectedContent := "Y29tbWl0IDY1ZjFiZjI3YmMzYmY3MGY2NDY1NzY1ODYzNWU2NjA5NGVkYmNiNGQKQXV0aG9yOiB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+CkRhdGU6ICAgU3VuIE1hciAxOSAxNjo0Nzo1OSAyMDE3IC0wNDAwCgogICAgSW5pdGlhbCBjb21taXQKCmRpZmYgLS1naXQgYS9SRUFETUUubWQgYi9SRUFETUUubWQKbmV3IGZpbGUgbW9kZSAxMDA2NDQKaW5kZXggMDAwMDAwMC4uNGI0ODUxYQotLS0gL2Rldi9udWxsCisrKyBiL1JFQURNRS5tZApAQCAtMCwwICsxLDMgQEAKKyMgcmVwbzEKKworRGVzY3JpcHRpb24gZm9yIHJlcG8xClwgTm8gbmV3bGluZSBhdCBlbmQgb2YgZmlsZQo=" |
||||||
|
assert.Equal(t, expectedContent, gitBlobResponse.Content) |
||||||
|
|
||||||
|
// Tests a private repo with no token so will fail
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo16.Name, repo16ReadmeSHA) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
|
||||||
|
// Test using access token for a private repo that the user of the token owns
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s?token=%s", user2.Name, repo16.Name, repo16ReadmeSHA, token) |
||||||
|
session.MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
// Test using bad sha
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo1.Name, badSHA) |
||||||
|
session.MakeRequest(t, req, http.StatusBadRequest) |
||||||
|
|
||||||
|
// Test using org repo "user3/repo3" where user2 is a collaborator
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s?token=%s", user3.Name, repo3.Name, repo3ReadmeSHA, token) |
||||||
|
session.MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
// Test using org repo "user3/repo3" where user2 is a collaborator
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s?token=%s", user3.Name, repo3.Name, repo3ReadmeSHA, token) |
||||||
|
session.MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
// Test using org repo "user3/repo3" with no user token
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user3.Name, repo3ReadmeSHA, repo3.Name) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
|
||||||
|
// Login as User4.
|
||||||
|
session = loginUser(t, user4.Name) |
||||||
|
token4 := getTokenForLoggedInUser(t, session) |
||||||
|
session = emptyTestSession(t) // don't want anyone logged in for this
|
||||||
|
|
||||||
|
// Test using org repo "user3/repo3" where user4 is a NOT collaborator
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", user3.Name, repo3.Name, token4) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
} |
@ -0,0 +1,74 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package integrations |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
) |
||||||
|
|
||||||
|
func TestAPIReposGitTrees(t *testing.T) { |
||||||
|
prepareTestEnv(t) |
||||||
|
user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16
|
||||||
|
user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3
|
||||||
|
user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos
|
||||||
|
repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo
|
||||||
|
repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo
|
||||||
|
repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
|
||||||
|
repo1TreeSHA := "65f1bf27bc3bf70f64657658635e66094edbcb4d" |
||||||
|
repo3TreeSHA := "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6" |
||||||
|
repo16TreeSHA := "69554a64c1e6030f051e5c3f94bfbd773cd6a324" |
||||||
|
badSHA := "0000000000000000000000000000000000000000" |
||||||
|
|
||||||
|
// Login as User2.
|
||||||
|
session := loginUser(t, user2.Name) |
||||||
|
token := getTokenForLoggedInUser(t, session) |
||||||
|
session = emptyTestSession(t) // don't want anyone logged in for this
|
||||||
|
|
||||||
|
// Test a public repo that anyone can GET the tree of
|
||||||
|
for _, ref := range [...]string{ |
||||||
|
"master", // Branch
|
||||||
|
repo1TreeSHA, // Tree SHA
|
||||||
|
} { |
||||||
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user2.Name, repo1.Name, ref) |
||||||
|
session.MakeRequest(t, req, http.StatusOK) |
||||||
|
} |
||||||
|
|
||||||
|
// Tests a private repo with no token so will fail
|
||||||
|
for _, ref := range [...]string{ |
||||||
|
"master", // Branch
|
||||||
|
repo1TreeSHA, // Tag
|
||||||
|
} { |
||||||
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user2.Name, repo16.Name, ref) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
} |
||||||
|
|
||||||
|
// Test using access token for a private repo that the user of the token owns
|
||||||
|
req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s?token=%s", user2.Name, repo16.Name, repo16TreeSHA, token) |
||||||
|
session.MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
// Test using bad sha
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user2.Name, repo1.Name, badSHA) |
||||||
|
session.MakeRequest(t, req, http.StatusBadRequest) |
||||||
|
|
||||||
|
// Test using org repo "user3/repo3" where user2 is a collaborator
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s?token=%s", user3.Name, repo3.Name, repo3TreeSHA, token) |
||||||
|
session.MakeRequest(t, req, http.StatusOK) |
||||||
|
|
||||||
|
// Test using org repo "user3/repo3" with no user token
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user3.Name, repo3TreeSHA, repo3.Name) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
|
||||||
|
// Login as User4.
|
||||||
|
session = loginUser(t, user4.Name) |
||||||
|
token4 := getTokenForLoggedInUser(t, session) |
||||||
|
session = emptyTestSession(t) // don't want anyone logged in for this
|
||||||
|
|
||||||
|
// Test using org repo "user3/repo3" where user4 is a NOT collaborator
|
||||||
|
req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", user3.Name, repo3.Name, token4) |
||||||
|
session.MakeRequest(t, req, http.StatusNotFound) |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repofiles |
||||||
|
|
||||||
|
import ( |
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
api "code.gitea.io/sdk/gitea" |
||||||
|
) |
||||||
|
|
||||||
|
// GetBlobBySHA get the GitBlobResponse of a repository using a sha hash.
|
||||||
|
func GetBlobBySHA(repo *models.Repository, sha string) (*api.GitBlobResponse, error) { |
||||||
|
gitRepo, err := git.OpenRepository(repo.RepoPath()) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
gitBlob, err := gitRepo.GetBlob(sha) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
content := "" |
||||||
|
if gitBlob.Size() <= setting.API.DefaultMaxBlobSize { |
||||||
|
content, err = gitBlob.GetBlobContentBase64() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
return &api.GitBlobResponse{ |
||||||
|
SHA: gitBlob.ID.String(), |
||||||
|
URL: repo.APIURL() + "/git/blobs/" + gitBlob.ID.String(), |
||||||
|
Size: gitBlob.Size(), |
||||||
|
Encoding: "base64", |
||||||
|
Content: content, |
||||||
|
}, nil |
||||||
|
} |
@ -0,0 +1,38 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repofiles |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/test" |
||||||
|
api "code.gitea.io/sdk/gitea" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestGetBlobBySHA(t *testing.T) { |
||||||
|
models.PrepareTestEnv(t) |
||||||
|
ctx := test.MockContext(t, "user2/repo1") |
||||||
|
test.LoadRepo(t, ctx, 1) |
||||||
|
test.LoadRepoCommit(t, ctx) |
||||||
|
test.LoadUser(t, ctx, 2) |
||||||
|
test.LoadGitRepo(t, ctx) |
||||||
|
sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d" |
||||||
|
ctx.SetParams(":id", "1") |
||||||
|
ctx.SetParams(":sha", sha) |
||||||
|
|
||||||
|
gbr, err := GetBlobBySHA(ctx.Repo.Repository, ctx.Params(":sha")) |
||||||
|
expectedGBR := &api.GitBlobResponse{ |
||||||
|
Content: "Y29tbWl0IDY1ZjFiZjI3YmMzYmY3MGY2NDY1NzY1ODYzNWU2NjA5NGVkYmNiNGQKQXV0aG9yOiB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+CkRhdGU6ICAgU3VuIE1hciAxOSAxNjo0Nzo1OSAyMDE3IC0wNDAwCgogICAgSW5pdGlhbCBjb21taXQKCmRpZmYgLS1naXQgYS9SRUFETUUubWQgYi9SRUFETUUubWQKbmV3IGZpbGUgbW9kZSAxMDA2NDQKaW5kZXggMDAwMDAwMC4uNGI0ODUxYQotLS0gL2Rldi9udWxsCisrKyBiL1JFQURNRS5tZApAQCAtMCwwICsxLDMgQEAKKyMgcmVwbzEKKworRGVzY3JpcHRpb24gZm9yIHJlcG8xClwgTm8gbmV3bGluZSBhdCBlbmQgb2YgZmlsZQo=", |
||||||
|
Encoding: "base64", |
||||||
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/65f1bf27bc3bf70f64657658635e66094edbcb4d", |
||||||
|
SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", |
||||||
|
Size: 180, |
||||||
|
} |
||||||
|
assert.Nil(t, err) |
||||||
|
assert.Equal(t, expectedGBR, gbr) |
||||||
|
} |
@ -0,0 +1,73 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repofiles |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/url" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
api "code.gitea.io/sdk/gitea" |
||||||
|
) |
||||||
|
|
||||||
|
// GetFileContents gets the meta data on a file's contents
|
||||||
|
func GetFileContents(repo *models.Repository, treePath, ref string) (*api.FileContentResponse, error) { |
||||||
|
if ref == "" { |
||||||
|
ref = repo.DefaultBranch |
||||||
|
} |
||||||
|
|
||||||
|
// Check that the path given in opts.treePath is valid (not a git path)
|
||||||
|
treePath = CleanUploadFileName(treePath) |
||||||
|
if treePath == "" { |
||||||
|
return nil, models.ErrFilenameInvalid{ |
||||||
|
Path: treePath, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
gitRepo, err := git.OpenRepository(repo.RepoPath()) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Get the commit object for the ref
|
||||||
|
commit, err := gitRepo.GetCommit(ref) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
entry, err := commit.GetTreeEntryByPath(treePath) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
urlRef := ref |
||||||
|
if _, err := gitRepo.GetBranchCommit(ref); err == nil { |
||||||
|
urlRef = "branch/" + ref |
||||||
|
} |
||||||
|
|
||||||
|
selfURL, _ := url.Parse(repo.APIURL() + "/contents/" + treePath) |
||||||
|
gitURL, _ := url.Parse(repo.APIURL() + "/git/blobs/" + entry.ID.String()) |
||||||
|
downloadURL, _ := url.Parse(repo.HTMLURL() + "/raw/" + urlRef + "/" + treePath) |
||||||
|
htmlURL, _ := url.Parse(repo.HTMLURL() + "/blob/" + ref + "/" + treePath) |
||||||
|
|
||||||
|
fileContent := &api.FileContentResponse{ |
||||||
|
Name: entry.Name(), |
||||||
|
Path: treePath, |
||||||
|
SHA: entry.ID.String(), |
||||||
|
Size: entry.Size(), |
||||||
|
URL: selfURL.String(), |
||||||
|
HTMLURL: htmlURL.String(), |
||||||
|
GitURL: gitURL.String(), |
||||||
|
DownloadURL: downloadURL.String(), |
||||||
|
Type: string(entry.Type), |
||||||
|
Links: &api.FileLinksResponse{ |
||||||
|
Self: selfURL.String(), |
||||||
|
GitURL: gitURL.String(), |
||||||
|
HTMLURL: htmlURL.String(), |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
return fileContent, nil |
||||||
|
} |
@ -0,0 +1,90 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repofiles |
||||||
|
|
||||||
|
import ( |
||||||
|
"path/filepath" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/test" |
||||||
|
"code.gitea.io/sdk/gitea" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestMain(m *testing.M) { |
||||||
|
models.MainTest(m, filepath.Join("..", "..")) |
||||||
|
} |
||||||
|
|
||||||
|
func TestGetFileContents(t *testing.T) { |
||||||
|
models.PrepareTestEnv(t) |
||||||
|
ctx := test.MockContext(t, "user2/repo1") |
||||||
|
ctx.SetParams(":id", "1") |
||||||
|
test.LoadRepo(t, ctx, 1) |
||||||
|
test.LoadRepoCommit(t, ctx) |
||||||
|
test.LoadUser(t, ctx, 2) |
||||||
|
test.LoadGitRepo(t, ctx) |
||||||
|
treePath := "README.md" |
||||||
|
ref := ctx.Repo.Repository.DefaultBranch |
||||||
|
|
||||||
|
expectedFileContentResponse := &gitea.FileContentResponse{ |
||||||
|
Name: treePath, |
||||||
|
Path: treePath, |
||||||
|
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f", |
||||||
|
Size: 30, |
||||||
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md", |
||||||
|
HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/README.md", |
||||||
|
GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/4b4851ad51df6a7d9f25c979345979eaeb5b349f", |
||||||
|
DownloadURL: "https://try.gitea.io/user2/repo1/raw/branch/master/README.md", |
||||||
|
Type: "blob", |
||||||
|
Links: &gitea.FileLinksResponse{ |
||||||
|
Self: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md", |
||||||
|
GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/4b4851ad51df6a7d9f25c979345979eaeb5b349f", |
||||||
|
HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/README.md", |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
t.Run("Get README.md contents", func(t *testing.T) { |
||||||
|
fileContentResponse, err := GetFileContents(ctx.Repo.Repository, treePath, ref) |
||||||
|
assert.EqualValues(t, expectedFileContentResponse, fileContentResponse) |
||||||
|
assert.Nil(t, err) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("Get REAMDE.md contents with ref as empty string (should then use the repo's default branch)", func(t *testing.T) { |
||||||
|
fileContentResponse, err := GetFileContents(ctx.Repo.Repository, treePath, "") |
||||||
|
assert.EqualValues(t, expectedFileContentResponse, fileContentResponse) |
||||||
|
assert.Nil(t, err) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestGetFileContentsErrors(t *testing.T) { |
||||||
|
models.PrepareTestEnv(t) |
||||||
|
ctx := test.MockContext(t, "user2/repo1") |
||||||
|
ctx.SetParams(":id", "1") |
||||||
|
test.LoadRepo(t, ctx, 1) |
||||||
|
test.LoadRepoCommit(t, ctx) |
||||||
|
test.LoadUser(t, ctx, 2) |
||||||
|
test.LoadGitRepo(t, ctx) |
||||||
|
repo := ctx.Repo.Repository |
||||||
|
treePath := "README.md" |
||||||
|
ref := repo.DefaultBranch |
||||||
|
|
||||||
|
t.Run("bad treePath", func(t *testing.T) { |
||||||
|
badTreePath := "bad/tree.md" |
||||||
|
fileContentResponse, err := GetFileContents(repo, badTreePath, ref) |
||||||
|
assert.Error(t, err) |
||||||
|
assert.EqualError(t, err, "object does not exist [id: , rel_path: bad]") |
||||||
|
assert.Nil(t, fileContentResponse) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("bad ref", func(t *testing.T) { |
||||||
|
badRef := "bad_ref" |
||||||
|
fileContentResponse, err := GetFileContents(repo, treePath, badRef) |
||||||
|
assert.Error(t, err) |
||||||
|
assert.EqualError(t, err, "object does not exist [id: "+badRef+", rel_path: ]") |
||||||
|
assert.Nil(t, fileContentResponse) |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,209 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repofiles |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
api "code.gitea.io/sdk/gitea" |
||||||
|
) |
||||||
|
|
||||||
|
// DeleteRepoFileOptions holds the repository delete file options
|
||||||
|
type DeleteRepoFileOptions struct { |
||||||
|
LastCommitID string |
||||||
|
OldBranch string |
||||||
|
NewBranch string |
||||||
|
TreePath string |
||||||
|
Message string |
||||||
|
SHA string |
||||||
|
Author *IdentityOptions |
||||||
|
Committer *IdentityOptions |
||||||
|
} |
||||||
|
|
||||||
|
// DeleteRepoFile deletes a file in the given repository
|
||||||
|
func DeleteRepoFile(repo *models.Repository, doer *models.User, opts *DeleteRepoFileOptions) (*api.FileResponse, error) { |
||||||
|
// If no branch name is set, assume the repo's default branch
|
||||||
|
if opts.OldBranch == "" { |
||||||
|
opts.OldBranch = repo.DefaultBranch |
||||||
|
} |
||||||
|
if opts.NewBranch == "" { |
||||||
|
opts.NewBranch = opts.OldBranch |
||||||
|
} |
||||||
|
|
||||||
|
// oldBranch must exist for this operation
|
||||||
|
if _, err := repo.GetBranch(opts.OldBranch); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// A NewBranch can be specified for the file to be created/updated in a new branch.
|
||||||
|
// Check to make sure the branch does not already exist, otherwise we can't proceed.
|
||||||
|
// If we aren't branching to a new branch, make sure user can commit to the given branch
|
||||||
|
if opts.NewBranch != opts.OldBranch { |
||||||
|
newBranch, err := repo.GetBranch(opts.NewBranch) |
||||||
|
if git.IsErrNotExist(err) { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if newBranch != nil { |
||||||
|
return nil, models.ErrBranchAlreadyExists{ |
||||||
|
BranchName: opts.NewBranch, |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
if protected, _ := repo.IsProtectedBranchForPush(opts.OldBranch, doer); protected { |
||||||
|
return nil, models.ErrUserCannotCommit{ |
||||||
|
UserName: doer.LowerName, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Check that the path given in opts.treeName is valid (not a git path)
|
||||||
|
treePath := CleanUploadFileName(opts.TreePath) |
||||||
|
if treePath == "" { |
||||||
|
return nil, models.ErrFilenameInvalid{ |
||||||
|
Path: opts.TreePath, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
message := strings.TrimSpace(opts.Message) |
||||||
|
|
||||||
|
author, committer := GetAuthorAndCommitterUsers(opts.Committer, opts.Author, doer) |
||||||
|
|
||||||
|
t, err := NewTemporaryUploadRepository(repo) |
||||||
|
defer t.Close() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if err := t.Clone(opts.OldBranch); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if err := t.SetDefaultIndex(); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Get the commit of the original branch
|
||||||
|
commit, err := t.GetBranchCommit(opts.OldBranch) |
||||||
|
if err != nil { |
||||||
|
return nil, err // Couldn't get a commit for the branch
|
||||||
|
} |
||||||
|
|
||||||
|
// Assigned LastCommitID in opts if it hasn't been set
|
||||||
|
if opts.LastCommitID == "" { |
||||||
|
opts.LastCommitID = commit.ID.String() |
||||||
|
} |
||||||
|
|
||||||
|
// Get the files in the index
|
||||||
|
filesInIndex, err := t.LsFiles(opts.TreePath) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("DeleteRepoFile: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Find the file we want to delete in the index
|
||||||
|
inFilelist := false |
||||||
|
for _, file := range filesInIndex { |
||||||
|
if file == opts.TreePath { |
||||||
|
inFilelist = true |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
if !inFilelist { |
||||||
|
return nil, models.ErrRepoFileDoesNotExist{ |
||||||
|
Path: opts.TreePath, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Get the entry of treePath and check if the SHA given is the same as the file
|
||||||
|
entry, err := commit.GetTreeEntryByPath(treePath) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if opts.SHA != "" { |
||||||
|
// If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error
|
||||||
|
if opts.SHA != entry.ID.String() { |
||||||
|
return nil, models.ErrSHADoesNotMatch{ |
||||||
|
Path: treePath, |
||||||
|
GivenSHA: opts.SHA, |
||||||
|
CurrentSHA: entry.ID.String(), |
||||||
|
} |
||||||
|
} |
||||||
|
} else if opts.LastCommitID != "" { |
||||||
|
// If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw
|
||||||
|
// an error, but only if we aren't creating a new branch.
|
||||||
|
if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch { |
||||||
|
// CommitIDs don't match, but we don't want to throw a ErrCommitIDDoesNotMatch unless
|
||||||
|
// this specific file has been edited since opts.LastCommitID
|
||||||
|
if changed, err := commit.FileChangedSinceCommit(treePath, opts.LastCommitID); err != nil { |
||||||
|
return nil, err |
||||||
|
} else if changed { |
||||||
|
return nil, models.ErrCommitIDDoesNotMatch{ |
||||||
|
GivenCommitID: opts.LastCommitID, |
||||||
|
CurrentCommitID: opts.LastCommitID, |
||||||
|
} |
||||||
|
} |
||||||
|
// The file wasn't modified, so we are good to delete it
|
||||||
|
} |
||||||
|
} else { |
||||||
|
// When deleting a file, a lastCommitID or SHA needs to be given to make sure other commits haven't been
|
||||||
|
// made. We throw an error if one wasn't provided.
|
||||||
|
return nil, models.ErrSHAOrCommitIDNotProvided{} |
||||||
|
} |
||||||
|
|
||||||
|
// Remove the file from the index
|
||||||
|
if err := t.RemoveFilesFromIndex(opts.TreePath); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Now write the tree
|
||||||
|
treeHash, err := t.WriteTree() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Now commit the tree
|
||||||
|
commitHash, err := t.CommitTree(author, committer, treeHash, message) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Then push this tree to NewBranch
|
||||||
|
if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Simulate push event.
|
||||||
|
oldCommitID := opts.LastCommitID |
||||||
|
if opts.NewBranch != opts.OldBranch { |
||||||
|
oldCommitID = git.EmptySHA |
||||||
|
} |
||||||
|
|
||||||
|
if err = repo.GetOwner(); err != nil { |
||||||
|
return nil, fmt.Errorf("GetOwner: %v", err) |
||||||
|
} |
||||||
|
err = models.PushUpdate( |
||||||
|
opts.NewBranch, |
||||||
|
models.PushUpdateOptions{ |
||||||
|
PusherID: doer.ID, |
||||||
|
PusherName: doer.Name, |
||||||
|
RepoUserName: repo.Owner.Name, |
||||||
|
RepoName: repo.Name, |
||||||
|
RefFullName: git.BranchPrefix + opts.NewBranch, |
||||||
|
OldCommitID: oldCommitID, |
||||||
|
NewCommitID: commitHash, |
||||||
|
}, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("PushUpdate: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// FIXME: Should we UpdateRepoIndexer(repo) here?
|
||||||
|
|
||||||
|
file, err := GetFileResponseFromCommit(repo, commit, opts.NewBranch, treePath) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return file, nil |
||||||
|
} |
@ -0,0 +1,183 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repofiles |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/test" |
||||||
|
api "code.gitea.io/sdk/gitea" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func getDeleteRepoFileOptions(repo *models.Repository) *DeleteRepoFileOptions { |
||||||
|
return &DeleteRepoFileOptions{ |
||||||
|
LastCommitID: "", |
||||||
|
OldBranch: repo.DefaultBranch, |
||||||
|
NewBranch: repo.DefaultBranch, |
||||||
|
TreePath: "README.md", |
||||||
|
Message: "Deletes README.md", |
||||||
|
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f", |
||||||
|
Author: nil, |
||||||
|
Committer: nil, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func getExpectedDeleteFileResponse() *api.FileResponse { |
||||||
|
return &api.FileResponse{ |
||||||
|
Content: nil, |
||||||
|
Commit: &api.FileCommitResponse{ |
||||||
|
CommitMeta: api.CommitMeta{ |
||||||
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d", |
||||||
|
SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", |
||||||
|
}, |
||||||
|
HTMLURL: "https://try.gitea.io/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d", |
||||||
|
Author: &api.CommitUser{ |
||||||
|
Identity: api.Identity{ |
||||||
|
Name: "user1", |
||||||
|
Email: "address1@example.com", |
||||||
|
}, |
||||||
|
Date: "2017-03-19T20:47:59Z", |
||||||
|
}, |
||||||
|
Committer: &api.CommitUser{ |
||||||
|
Identity: api.Identity{ |
||||||
|
Name: "Ethan Koenig", |
||||||
|
Email: "ethantkoenig@gmail.com", |
||||||
|
}, |
||||||
|
Date: "2017-03-19T20:47:59Z", |
||||||
|
}, |
||||||
|
Parents: []*api.CommitMeta{}, |
||||||
|
Message: "Initial commit\n", |
||||||
|
Tree: &api.CommitMeta{ |
||||||
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/2a2f1d4670728a2e10049e345bd7a276468beab6", |
||||||
|
SHA: "2a2f1d4670728a2e10049e345bd7a276468beab6", |
||||||
|
}, |
||||||
|
}, |
||||||
|
Verification: &api.PayloadCommitVerification{ |
||||||
|
Verified: false, |
||||||
|
Reason: "", |
||||||
|
Signature: "", |
||||||
|
Payload: "", |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestDeleteRepoFile(t *testing.T) { |
||||||
|
// setup
|
||||||
|
models.PrepareTestEnv(t) |
||||||
|
ctx := test.MockContext(t, "user2/repo1") |
||||||
|
ctx.SetParams(":id", "1") |
||||||
|
test.LoadRepo(t, ctx, 1) |
||||||
|
test.LoadRepoCommit(t, ctx) |
||||||
|
test.LoadUser(t, ctx, 2) |
||||||
|
test.LoadGitRepo(t, ctx) |
||||||
|
repo := ctx.Repo.Repository |
||||||
|
doer := ctx.User |
||||||
|
opts := getDeleteRepoFileOptions(repo) |
||||||
|
|
||||||
|
t.Run("Delete README.md file", func(t *testing.T) { |
||||||
|
fileResponse, err := DeleteRepoFile(repo, doer, opts) |
||||||
|
assert.Nil(t, err) |
||||||
|
expectedFileResponse := getExpectedDeleteFileResponse() |
||||||
|
assert.EqualValues(t, expectedFileResponse, fileResponse) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("Verify README.md has been deleted", func(t *testing.T) { |
||||||
|
fileResponse, err := DeleteRepoFile(repo, doer, opts) |
||||||
|
assert.Nil(t, fileResponse) |
||||||
|
expectedError := "repository file does not exist [path: " + opts.TreePath + "]" |
||||||
|
assert.EqualError(t, err, expectedError) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// Test opts with branch names removed, same results
|
||||||
|
func TestDeleteRepoFileWithoutBranchNames(t *testing.T) { |
||||||
|
// setup
|
||||||
|
models.PrepareTestEnv(t) |
||||||
|
ctx := test.MockContext(t, "user2/repo1") |
||||||
|
ctx.SetParams(":id", "1") |
||||||
|
test.LoadRepo(t, ctx, 1) |
||||||
|
test.LoadRepoCommit(t, ctx) |
||||||
|
test.LoadUser(t, ctx, 2) |
||||||
|
test.LoadGitRepo(t, ctx) |
||||||
|
repo := ctx.Repo.Repository |
||||||
|
doer := ctx.User |
||||||
|
opts := getDeleteRepoFileOptions(repo) |
||||||
|
opts.OldBranch = "" |
||||||
|
opts.NewBranch = "" |
||||||
|
|
||||||
|
t.Run("Delete README.md without Branch Name", func(t *testing.T) { |
||||||
|
fileResponse, err := DeleteRepoFile(repo, doer, opts) |
||||||
|
assert.Nil(t, err) |
||||||
|
expectedFileResponse := getExpectedDeleteFileResponse() |
||||||
|
assert.EqualValues(t, expectedFileResponse, fileResponse) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestDeleteRepoFileErrors(t *testing.T) { |
||||||
|
// setup
|
||||||
|
models.PrepareTestEnv(t) |
||||||
|
ctx := test.MockContext(t, "user2/repo1") |
||||||
|
ctx.SetParams(":id", "1") |
||||||
|
test.LoadRepo(t, ctx, 1) |
||||||
|
test.LoadRepoCommit(t, ctx) |
||||||
|
test.LoadUser(t, ctx, 2) |
||||||
|
test.LoadGitRepo(t, ctx) |
||||||
|
repo := ctx.Repo.Repository |
||||||
|
doer := ctx.User |
||||||
|
|
||||||
|
t.Run("Bad branch", func(t *testing.T) { |
||||||
|
opts := getDeleteRepoFileOptions(repo) |
||||||
|
opts.OldBranch = "bad_branch" |
||||||
|
fileResponse, err := DeleteRepoFile(repo, doer, opts) |
||||||
|
assert.Error(t, err) |
||||||
|
assert.Nil(t, fileResponse) |
||||||
|
expectedError := "branch does not exist [name: " + opts.OldBranch + "]" |
||||||
|
assert.EqualError(t, err, expectedError) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("Bad SHA", func(t *testing.T) { |
||||||
|
opts := getDeleteRepoFileOptions(repo) |
||||||
|
origSHA := opts.SHA |
||||||
|
opts.SHA = "bad_sha" |
||||||
|
fileResponse, err := DeleteRepoFile(repo, doer, opts) |
||||||
|
assert.Nil(t, fileResponse) |
||||||
|
assert.Error(t, err) |
||||||
|
expectedError := "sha does not match [given: " + opts.SHA + ", expected: " + origSHA + "]" |
||||||
|
assert.EqualError(t, err, expectedError) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("New branch already exists", func(t *testing.T) { |
||||||
|
opts := getDeleteRepoFileOptions(repo) |
||||||
|
opts.NewBranch = "develop" |
||||||
|
fileResponse, err := DeleteRepoFile(repo, doer, opts) |
||||||
|
assert.Nil(t, fileResponse) |
||||||
|
assert.Error(t, err) |
||||||
|
expectedError := "branch already exists [name: " + opts.NewBranch + "]" |
||||||
|
assert.EqualError(t, err, expectedError) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("TreePath is empty:", func(t *testing.T) { |
||||||
|
opts := getDeleteRepoFileOptions(repo) |
||||||
|
opts.TreePath = "" |
||||||
|
fileResponse, err := DeleteRepoFile(repo, doer, opts) |
||||||
|
assert.Nil(t, fileResponse) |
||||||
|
assert.Error(t, err) |
||||||
|
expectedError := "path contains a malformed path component [path: ]" |
||||||
|
assert.EqualError(t, err, expectedError) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("TreePath is a git directory:", func(t *testing.T) { |
||||||
|
opts := getDeleteRepoFileOptions(repo) |
||||||
|
opts.TreePath = ".git" |
||||||
|
fileResponse, err := DeleteRepoFile(repo, doer, opts) |
||||||
|
assert.Nil(t, fileResponse) |
||||||
|
assert.Error(t, err) |
||||||
|
expectedError := "path contains a malformed path component [path: " + opts.TreePath + "]" |
||||||
|
assert.EqualError(t, err, expectedError) |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,143 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repofiles |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/test" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestGetDiffPreview(t *testing.T) { |
||||||
|
models.PrepareTestEnv(t) |
||||||
|
ctx := test.MockContext(t, "user2/repo1") |
||||||
|
ctx.SetParams(":id", "1") |
||||||
|
test.LoadRepo(t, ctx, 1) |
||||||
|
test.LoadRepoCommit(t, ctx) |
||||||
|
test.LoadUser(t, ctx, 2) |
||||||
|
test.LoadGitRepo(t, ctx) |
||||||
|
branch := ctx.Repo.Repository.DefaultBranch |
||||||
|
treePath := "README.md" |
||||||
|
content := "# repo1\n\nDescription for repo1\nthis is a new line" |
||||||
|
|
||||||
|
expectedDiff := &models.Diff{ |
||||||
|
TotalAddition: 2, |
||||||
|
TotalDeletion: 1, |
||||||
|
Files: []*models.DiffFile{ |
||||||
|
{ |
||||||
|
Name: "README.md", |
||||||
|
OldName: "README.md", |
||||||
|
Index: 1, |
||||||
|
Addition: 2, |
||||||
|
Deletion: 1, |
||||||
|
Type: 2, |
||||||
|
IsCreated: false, |
||||||
|
IsDeleted: false, |
||||||
|
IsBin: false, |
||||||
|
IsLFSFile: false, |
||||||
|
IsRenamed: false, |
||||||
|
IsSubmodule: false, |
||||||
|
Sections: []*models.DiffSection{ |
||||||
|
{ |
||||||
|
Name: "", |
||||||
|
Lines: []*models.DiffLine{ |
||||||
|
{ |
||||||
|
LeftIdx: 0, |
||||||
|
RightIdx: 0, |
||||||
|
Type: 4, |
||||||
|
Content: "@@ -1,3 +1,4 @@", |
||||||
|
Comments: nil, |
||||||
|
}, |
||||||
|
{ |
||||||
|
LeftIdx: 1, |
||||||
|
RightIdx: 1, |
||||||
|
Type: 1, |
||||||
|
Content: " # repo1", |
||||||
|
Comments: nil, |
||||||
|
}, |
||||||
|
{ |
||||||
|
LeftIdx: 2, |
||||||
|
RightIdx: 2, |
||||||
|
Type: 1, |
||||||
|
Content: " ", |
||||||
|
Comments: nil, |
||||||
|
}, |
||||||
|
{ |
||||||
|
LeftIdx: 3, |
||||||
|
RightIdx: 0, |
||||||
|
Type: 3, |
||||||
|
Content: "-Description for repo1", |
||||||
|
Comments: nil, |
||||||
|
}, |
||||||
|
{ |
||||||
|
LeftIdx: 0, |
||||||
|
RightIdx: 3, |
||||||
|
Type: 2, |
||||||
|
Content: "+Description for repo1", |
||||||
|
Comments: nil, |
||||||
|
}, |
||||||
|
{ |
||||||
|
LeftIdx: 0, |
||||||
|
RightIdx: 4, |
||||||
|
Type: 2, |
||||||
|
Content: "+this is a new line", |
||||||
|
Comments: nil, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
IsIncomplete: false, |
||||||
|
}, |
||||||
|
}, |
||||||
|
IsIncomplete: false, |
||||||
|
} |
||||||
|
|
||||||
|
t.Run("with given branch", func(t *testing.T) { |
||||||
|
diff, err := GetDiffPreview(ctx.Repo.Repository, branch, treePath, content) |
||||||
|
assert.Nil(t, err) |
||||||
|
assert.EqualValues(t, expectedDiff, diff) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("empty branch, same results", func(t *testing.T) { |
||||||
|
diff, err := GetDiffPreview(ctx.Repo.Repository, "", treePath, content) |
||||||
|
assert.Nil(t, err) |
||||||
|
assert.EqualValues(t, expectedDiff, diff) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func TestGetDiffPreviewErrors(t *testing.T) { |
||||||
|
models.PrepareTestEnv(t) |
||||||
|
ctx := test.MockContext(t, "user2/repo1") |
||||||
|
ctx.SetParams(":id", "1") |
||||||
|
test.LoadRepo(t, ctx, 1) |
||||||
|
test.LoadRepoCommit(t, ctx) |
||||||
|
test.LoadUser(t, ctx, 2) |
||||||
|
test.LoadGitRepo(t, ctx) |
||||||
|
branch := ctx.Repo.Repository.DefaultBranch |
||||||
|
treePath := "README.md" |
||||||
|
content := "# repo1\n\nDescription for repo1\nthis is a new line" |
||||||
|
|
||||||
|
t.Run("empty repo", func(t *testing.T) { |
||||||
|
diff, err := GetDiffPreview(&models.Repository{}, branch, treePath, content) |
||||||
|
assert.Nil(t, diff) |
||||||
|
assert.EqualError(t, err, "repository does not exist [id: 0, uid: 0, owner_name: , name: ]") |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("bad branch", func(t *testing.T) { |
||||||
|
badBranch := "bad_branch" |
||||||
|
diff, err := GetDiffPreview(ctx.Repo.Repository, badBranch, treePath, content) |
||||||
|
assert.Nil(t, diff) |
||||||
|
assert.EqualError(t, err, "branch does not exist [name: "+badBranch+"]") |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("empty treePath", func(t *testing.T) { |
||||||
|
diff, err := GetDiffPreview(ctx.Repo.Repository, branch, "", content) |
||||||
|
assert.Nil(t, diff) |
||||||
|
assert.EqualError(t, err, "path is invalid [path: ]") |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,125 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repofiles |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"net/url" |
||||||
|
"strings" |
||||||
|
"time" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
api "code.gitea.io/sdk/gitea" |
||||||
|
) |
||||||
|
|
||||||
|
// GetFileResponseFromCommit Constructs a FileResponse from a Commit object
|
||||||
|
func GetFileResponseFromCommit(repo *models.Repository, commit *git.Commit, branch, treeName string) (*api.FileResponse, error) { |
||||||
|
fileContents, _ := GetFileContents(repo, treeName, branch) // ok if fails, then will be nil
|
||||||
|
fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil
|
||||||
|
verification := GetPayloadCommitVerification(commit) |
||||||
|
fileResponse := &api.FileResponse{ |
||||||
|
Content: fileContents, |
||||||
|
Commit: fileCommitResponse, |
||||||
|
Verification: verification, |
||||||
|
} |
||||||
|
return fileResponse, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetFileCommitResponse Constructs a FileCommitResponse from a Commit object
|
||||||
|
func GetFileCommitResponse(repo *models.Repository, commit *git.Commit) (*api.FileCommitResponse, error) { |
||||||
|
if repo == nil { |
||||||
|
return nil, fmt.Errorf("repo cannot be nil") |
||||||
|
} |
||||||
|
if commit == nil { |
||||||
|
return nil, fmt.Errorf("commit cannot be nil") |
||||||
|
} |
||||||
|
commitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + commit.ID.String()) |
||||||
|
commitTreeURL, _ := url.Parse(repo.APIURL() + "/git/trees/" + commit.Tree.ID.String()) |
||||||
|
parents := make([]*api.CommitMeta, commit.ParentCount()) |
||||||
|
for i := 0; i <= commit.ParentCount(); i++ { |
||||||
|
if parent, err := commit.Parent(i); err == nil && parent != nil { |
||||||
|
parentCommitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + parent.ID.String()) |
||||||
|
parents[i] = &api.CommitMeta{ |
||||||
|
SHA: parent.ID.String(), |
||||||
|
URL: parentCommitURL.String(), |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
commitHTMLURL, _ := url.Parse(repo.HTMLURL() + "/commit/" + commit.ID.String()) |
||||||
|
fileCommit := &api.FileCommitResponse{ |
||||||
|
CommitMeta: api.CommitMeta{ |
||||||
|
SHA: commit.ID.String(), |
||||||
|
URL: commitURL.String(), |
||||||
|
}, |
||||||
|
HTMLURL: commitHTMLURL.String(), |
||||||
|
Author: &api.CommitUser{ |
||||||
|
Identity: api.Identity{ |
||||||
|
Name: commit.Author.Name, |
||||||
|
Email: commit.Author.Email, |
||||||
|
}, |
||||||
|
Date: commit.Author.When.UTC().Format(time.RFC3339), |
||||||
|
}, |
||||||
|
Committer: &api.CommitUser{ |
||||||
|
Identity: api.Identity{ |
||||||
|
Name: commit.Committer.Name, |
||||||
|
Email: commit.Committer.Email, |
||||||
|
}, |
||||||
|
Date: commit.Committer.When.UTC().Format(time.RFC3339), |
||||||
|
}, |
||||||
|
Message: commit.Message(), |
||||||
|
Tree: &api.CommitMeta{ |
||||||
|
URL: commitTreeURL.String(), |
||||||
|
SHA: commit.Tree.ID.String(), |
||||||
|
}, |
||||||
|
Parents: parents, |
||||||
|
} |
||||||
|
return fileCommit, nil |
||||||
|
} |
||||||
|
|
||||||
|
// GetAuthorAndCommitterUsers Gets the author and committer user objects from the IdentityOptions
|
||||||
|
func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *models.User) (committerUser, authorUser *models.User) { |
||||||
|
// Committer and author are optional. If they are not the doer (not same email address)
|
||||||
|
// then we use bogus User objects for them to store their FullName and Email.
|
||||||
|
// If only one of the two are provided, we set both of them to it.
|
||||||
|
// If neither are provided, both are the doer.
|
||||||
|
if committer != nil && committer.Email != "" { |
||||||
|
if doer != nil && strings.ToLower(doer.Email) == strings.ToLower(committer.Email) { |
||||||
|
committerUser = doer // the committer is the doer, so will use their user object
|
||||||
|
if committer.Name != "" { |
||||||
|
committerUser.FullName = committer.Name |
||||||
|
} |
||||||
|
} else { |
||||||
|
committerUser = &models.User{ |
||||||
|
FullName: committer.Name, |
||||||
|
Email: committer.Email, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if author != nil && author.Email != "" { |
||||||
|
if doer != nil && strings.ToLower(doer.Email) == strings.ToLower(author.Email) { |
||||||
|
authorUser = doer // the author is the doer, so will use their user object
|
||||||
|
if authorUser.Name != "" { |
||||||
|
authorUser.FullName = author.Name |
||||||
|
} |
||||||
|
} else { |
||||||
|
authorUser = &models.User{ |
||||||
|
FullName: author.Name, |
||||||
|
Email: author.Email, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if authorUser == nil { |
||||||
|
if committerUser != nil { |
||||||
|
authorUser = committerUser // No valid author was given so use the committer
|
||||||
|
} else if doer != nil { |
||||||
|
authorUser = doer // No valid author was given and no valid committer so use the doer
|
||||||
|
} |
||||||
|
} |
||||||
|
if committerUser == nil { |
||||||
|
committerUser = authorUser // No valid committer so use the author as the committer (was set to a valid user above)
|
||||||
|
} |
||||||
|
return authorUser, committerUser |
||||||
|
} |
@ -0,0 +1,90 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repofiles |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
"code.gitea.io/gitea/modules/test" |
||||||
|
api "code.gitea.io/sdk/gitea" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func getExpectedFileResponse() *api.FileResponse { |
||||||
|
return &api.FileResponse{ |
||||||
|
Content: &api.FileContentResponse{ |
||||||
|
Name: "README.md", |
||||||
|
Path: "README.md", |
||||||
|
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f", |
||||||
|
Size: 30, |
||||||
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md", |
||||||
|
HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/README.md", |
||||||
|
GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/4b4851ad51df6a7d9f25c979345979eaeb5b349f", |
||||||
|
DownloadURL: "https://try.gitea.io/user2/repo1/raw/branch/master/README.md", |
||||||
|
Type: "blob", |
||||||
|
Links: &api.FileLinksResponse{ |
||||||
|
Self: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md", |
||||||
|
GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/4b4851ad51df6a7d9f25c979345979eaeb5b349f", |
||||||
|
HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/README.md", |
||||||
|
}, |
||||||
|
}, |
||||||
|
Commit: &api.FileCommitResponse{ |
||||||
|
CommitMeta: api.CommitMeta{ |
||||||
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d", |
||||||
|
SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", |
||||||
|
}, |
||||||
|
HTMLURL: "https://try.gitea.io/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d", |
||||||
|
Author: &api.CommitUser{ |
||||||
|
Identity: api.Identity{ |
||||||
|
Name: "user1", |
||||||
|
Email: "address1@example.com", |
||||||
|
}, |
||||||
|
Date: "2017-03-19T20:47:59Z", |
||||||
|
}, |
||||||
|
Committer: &api.CommitUser{ |
||||||
|
Identity: api.Identity{ |
||||||
|
Name: "Ethan Koenig", |
||||||
|
Email: "ethantkoenig@gmail.com", |
||||||
|
}, |
||||||
|
Date: "2017-03-19T20:47:59Z", |
||||||
|
}, |
||||||
|
Parents: []*api.CommitMeta{}, |
||||||
|
Message: "Initial commit\n", |
||||||
|
Tree: &api.CommitMeta{ |
||||||
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/2a2f1d4670728a2e10049e345bd7a276468beab6", |
||||||
|
SHA: "2a2f1d4670728a2e10049e345bd7a276468beab6", |
||||||
|
}, |
||||||
|
}, |
||||||
|
Verification: &api.PayloadCommitVerification{ |
||||||
|
Verified: false, |
||||||
|
Reason: "", |
||||||
|
Signature: "", |
||||||
|
Payload: "", |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestGetFileResponseFromCommit(t *testing.T) { |
||||||
|
models.PrepareTestEnv(t) |
||||||
|
ctx := test.MockContext(t, "user2/repo1") |
||||||
|
ctx.SetParams(":id", "1") |
||||||
|
test.LoadRepo(t, ctx, 1) |
||||||
|
test.LoadRepoCommit(t, ctx) |
||||||
|
test.LoadUser(t, ctx, 2) |
||||||
|
test.LoadGitRepo(t, ctx) |
||||||
|
repo := ctx.Repo.Repository |
||||||
|
branch := repo.DefaultBranch |
||||||
|
treePath := "README.md" |
||||||
|
gitRepo, _ := git.OpenRepository(repo.RepoPath()) |
||||||
|
commit, _ := gitRepo.GetBranchCommit(branch) |
||||||
|
expectedFileResponse := getExpectedFileResponse() |
||||||
|
|
||||||
|
fileResponse, err := GetFileResponseFromCommit(repo, commit, branch, treePath) |
||||||
|
assert.Nil(t, err) |
||||||
|
assert.EqualValues(t, expectedFileResponse, fileResponse) |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.package repofiles
|
||||||
|
|
||||||
|
package repofiles |
||||||
|
|
||||||
|
import ( |
||||||
|
"path" |
||||||
|
"strings" |
||||||
|
) |
||||||
|
|
||||||
|
// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory
|
||||||
|
func CleanUploadFileName(name string) string { |
||||||
|
// Rebase the filename
|
||||||
|
name = strings.Trim(path.Clean("/"+name), " /") |
||||||
|
// Git disallows any filenames to have a .git directory in them.
|
||||||
|
for _, part := range strings.Split(name, "/") { |
||||||
|
if strings.ToLower(part) == ".git" { |
||||||
|
return "" |
||||||
|
} |
||||||
|
} |
||||||
|
return name |
||||||
|
} |
@ -0,0 +1,27 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repofiles |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func TestCleanUploadFileName(t *testing.T) { |
||||||
|
t.Run("Clean regular file", func(t *testing.T) { |
||||||
|
name := "this/is/test" |
||||||
|
cleanName := CleanUploadFileName(name) |
||||||
|
expectedCleanName := name |
||||||
|
assert.EqualValues(t, expectedCleanName, cleanName) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("Clean a .git path", func(t *testing.T) { |
||||||
|
name := "this/is/test/.git" |
||||||
|
cleanName := CleanUploadFileName(name) |
||||||
|
expectedCleanName := "" |
||||||
|
assert.EqualValues(t, expectedCleanName, cleanName) |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,92 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repofiles |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
api "code.gitea.io/sdk/gitea" |
||||||
|
) |
||||||
|
|
||||||
|
// GetTreeBySHA get the GitTreeResponse of a repository using a sha hash.
|
||||||
|
func GetTreeBySHA(repo *models.Repository, sha string, page, perPage int, recursive bool) (*api.GitTreeResponse, error) { |
||||||
|
gitRepo, err := git.OpenRepository(repo.RepoPath()) |
||||||
|
gitTree, err := gitRepo.GetTree(sha) |
||||||
|
if err != nil || gitTree == nil { |
||||||
|
return nil, models.ErrSHANotFound{ |
||||||
|
SHA: sha, |
||||||
|
} |
||||||
|
} |
||||||
|
tree := new(api.GitTreeResponse) |
||||||
|
tree.SHA = gitTree.ID.String() |
||||||
|
tree.URL = repo.APIURL() + "/git/trees/" + tree.SHA |
||||||
|
var entries git.Entries |
||||||
|
if recursive { |
||||||
|
entries, err = gitTree.ListEntriesRecursive() |
||||||
|
} else { |
||||||
|
entries, err = gitTree.ListEntries() |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
apiURL := repo.APIURL() |
||||||
|
apiURLLen := len(apiURL) |
||||||
|
|
||||||
|
// 51 is len(sha1) + len("/git/blobs/"). 40 + 11.
|
||||||
|
blobURL := make([]byte, apiURLLen+51) |
||||||
|
copy(blobURL[:], apiURL) |
||||||
|
copy(blobURL[apiURLLen:], "/git/blobs/") |
||||||
|
|
||||||
|
// 51 is len(sha1) + len("/git/trees/"). 40 + 11.
|
||||||
|
treeURL := make([]byte, apiURLLen+51) |
||||||
|
copy(treeURL[:], apiURL) |
||||||
|
copy(treeURL[apiURLLen:], "/git/trees/") |
||||||
|
|
||||||
|
// 40 is the size of the sha1 hash in hexadecimal format.
|
||||||
|
copyPos := len(treeURL) - 40 |
||||||
|
|
||||||
|
if perPage <= 0 || perPage > setting.API.DefaultGitTreesPerPage { |
||||||
|
perPage = setting.API.DefaultGitTreesPerPage |
||||||
|
} |
||||||
|
if page <= 0 { |
||||||
|
page = 1 |
||||||
|
} |
||||||
|
tree.Page = page |
||||||
|
tree.TotalCount = len(entries) |
||||||
|
rangeStart := perPage * (page - 1) |
||||||
|
if rangeStart >= len(entries) { |
||||||
|
return tree, nil |
||||||
|
} |
||||||
|
var rangeEnd int |
||||||
|
if len(entries) > perPage { |
||||||
|
tree.Truncated = true |
||||||
|
} |
||||||
|
if rangeStart+perPage < len(entries) { |
||||||
|
rangeEnd = rangeStart + perPage |
||||||
|
} else { |
||||||
|
rangeEnd = len(entries) |
||||||
|
} |
||||||
|
tree.Entries = make([]api.GitEntry, rangeEnd-rangeStart) |
||||||
|
for e := rangeStart; e < rangeEnd; e++ { |
||||||
|
i := e - rangeStart |
||||||
|
tree.Entries[i].Path = entries[e].Name() |
||||||
|
tree.Entries[i].Mode = fmt.Sprintf("%06x", entries[e].Mode()) |
||||||
|
tree.Entries[i].Type = string(entries[e].Type) |
||||||
|
tree.Entries[i].Size = entries[e].Size() |
||||||
|
tree.Entries[i].SHA = entries[e].ID.String() |
||||||
|
|
||||||
|
if entries[e].IsDir() { |
||||||
|
copy(treeURL[copyPos:], entries[e].ID.String()) |
||||||
|
tree.Entries[i].URL = string(treeURL[:]) |
||||||
|
} else { |
||||||
|
copy(blobURL[copyPos:], entries[e].ID.String()) |
||||||
|
tree.Entries[i].URL = string(blobURL[:]) |
||||||
|
} |
||||||
|
} |
||||||
|
return tree, nil |
||||||
|
} |
@ -0,0 +1,331 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repofiles |
||||||
|
|
||||||
|
import ( |
||||||
|
"fmt" |
||||||
|
"path" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
"code.gitea.io/gitea/modules/lfs" |
||||||
|
"code.gitea.io/gitea/modules/setting" |
||||||
|
"code.gitea.io/sdk/gitea" |
||||||
|
) |
||||||
|
|
||||||
|
// IdentityOptions for a person's identity like an author or committer
|
||||||
|
type IdentityOptions struct { |
||||||
|
Name string |
||||||
|
Email string |
||||||
|
} |
||||||
|
|
||||||
|
// UpdateRepoFileOptions holds the repository file update options
|
||||||
|
type UpdateRepoFileOptions struct { |
||||||
|
LastCommitID string |
||||||
|
OldBranch string |
||||||
|
NewBranch string |
||||||
|
TreePath string |
||||||
|
FromTreePath string |
||||||
|
Message string |
||||||
|
Content string |
||||||
|
SHA string |
||||||
|
IsNewFile bool |
||||||
|
Author *IdentityOptions |
||||||
|
Committer *IdentityOptions |
||||||
|
} |
||||||
|
|
||||||
|
// CreateOrUpdateRepoFile adds or updates a file in the given repository
|
||||||
|
func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *UpdateRepoFileOptions) (*gitea.FileResponse, error) { |
||||||
|
// If no branch name is set, assume master
|
||||||
|
if opts.OldBranch == "" { |
||||||
|
opts.OldBranch = repo.DefaultBranch |
||||||
|
} |
||||||
|
if opts.NewBranch == "" { |
||||||
|
opts.NewBranch = opts.OldBranch |
||||||
|
} |
||||||
|
|
||||||
|
// oldBranch must exist for this operation
|
||||||
|
if _, err := repo.GetBranch(opts.OldBranch); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// A NewBranch can be specified for the file to be created/updated in a new branch.
|
||||||
|
// Check to make sure the branch does not already exist, otherwise we can't proceed.
|
||||||
|
// If we aren't branching to a new branch, make sure user can commit to the given branch
|
||||||
|
if opts.NewBranch != opts.OldBranch { |
||||||
|
existingBranch, err := repo.GetBranch(opts.NewBranch) |
||||||
|
if existingBranch != nil { |
||||||
|
return nil, models.ErrBranchAlreadyExists{ |
||||||
|
BranchName: opts.NewBranch, |
||||||
|
} |
||||||
|
} |
||||||
|
if err != nil && !models.IsErrBranchNotExist(err) { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} else { |
||||||
|
if protected, _ := repo.IsProtectedBranchForPush(opts.OldBranch, doer); protected { |
||||||
|
return nil, models.ErrUserCannotCommit{UserName: doer.LowerName} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// If FromTreePath is not set, set it to the opts.TreePath
|
||||||
|
if opts.TreePath != "" && opts.FromTreePath == "" { |
||||||
|
opts.FromTreePath = opts.TreePath |
||||||
|
} |
||||||
|
|
||||||
|
// Check that the path given in opts.treePath is valid (not a git path)
|
||||||
|
treePath := CleanUploadFileName(opts.TreePath) |
||||||
|
if treePath == "" { |
||||||
|
return nil, models.ErrFilenameInvalid{ |
||||||
|
Path: opts.TreePath, |
||||||
|
} |
||||||
|
} |
||||||
|
// If there is a fromTreePath (we are copying it), also clean it up
|
||||||
|
fromTreePath := CleanUploadFileName(opts.FromTreePath) |
||||||
|
if fromTreePath == "" && opts.FromTreePath != "" { |
||||||
|
return nil, models.ErrFilenameInvalid{ |
||||||
|
Path: opts.FromTreePath, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
message := strings.TrimSpace(opts.Message) |
||||||
|
|
||||||
|
author, committer := GetAuthorAndCommitterUsers(opts.Committer, opts.Author, doer) |
||||||
|
|
||||||
|
t, err := NewTemporaryUploadRepository(repo) |
||||||
|
defer t.Close() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if err := t.Clone(opts.OldBranch); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if err := t.SetDefaultIndex(); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Get the commit of the original branch
|
||||||
|
commit, err := t.GetBranchCommit(opts.OldBranch) |
||||||
|
if err != nil { |
||||||
|
return nil, err // Couldn't get a commit for the branch
|
||||||
|
} |
||||||
|
|
||||||
|
// Assigned LastCommitID in opts if it hasn't been set
|
||||||
|
if opts.LastCommitID == "" { |
||||||
|
opts.LastCommitID = commit.ID.String() |
||||||
|
} |
||||||
|
|
||||||
|
if !opts.IsNewFile { |
||||||
|
fromEntry, err := commit.GetTreeEntryByPath(fromTreePath) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if opts.SHA != "" { |
||||||
|
// If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error
|
||||||
|
if opts.SHA != fromEntry.ID.String() { |
||||||
|
return nil, models.ErrSHADoesNotMatch{ |
||||||
|
Path: treePath, |
||||||
|
GivenSHA: opts.SHA, |
||||||
|
CurrentSHA: fromEntry.ID.String(), |
||||||
|
} |
||||||
|
} |
||||||
|
} else if opts.LastCommitID != "" { |
||||||
|
// If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw
|
||||||
|
// an error, but only if we aren't creating a new branch.
|
||||||
|
if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch { |
||||||
|
if changed, err := commit.FileChangedSinceCommit(treePath, opts.LastCommitID); err != nil { |
||||||
|
return nil, err |
||||||
|
} else if changed { |
||||||
|
return nil, models.ErrCommitIDDoesNotMatch{ |
||||||
|
GivenCommitID: opts.LastCommitID, |
||||||
|
CurrentCommitID: opts.LastCommitID, |
||||||
|
} |
||||||
|
} |
||||||
|
// The file wasn't modified, so we are good to delete it
|
||||||
|
} |
||||||
|
} else { |
||||||
|
// When updating a file, a lastCommitID or SHA needs to be given to make sure other commits
|
||||||
|
// haven't been made. We throw an error if one wasn't provided.
|
||||||
|
return nil, models.ErrSHAOrCommitIDNotProvided{} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// For the path where this file will be created/updated, we need to make
|
||||||
|
// sure no parts of the path are existing files or links except for the last
|
||||||
|
// item in the path which is the file name, and that shouldn't exist IF it is
|
||||||
|
// a new file OR is being moved to a new path.
|
||||||
|
treePathParts := strings.Split(treePath, "/") |
||||||
|
subTreePath := "" |
||||||
|
for index, part := range treePathParts { |
||||||
|
subTreePath = path.Join(subTreePath, part) |
||||||
|
entry, err := commit.GetTreeEntryByPath(subTreePath) |
||||||
|
if err != nil { |
||||||
|
if git.IsErrNotExist(err) { |
||||||
|
// Means there is no item with that name, so we're good
|
||||||
|
break |
||||||
|
} |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
if index < len(treePathParts)-1 { |
||||||
|
if !entry.IsDir() { |
||||||
|
return nil, models.ErrFilePathInvalid{ |
||||||
|
Message: fmt.Sprintf("a file exists where you’re trying to create a subdirectory [path: %s]", subTreePath), |
||||||
|
Path: subTreePath, |
||||||
|
Name: part, |
||||||
|
Type: git.EntryModeBlob, |
||||||
|
} |
||||||
|
} |
||||||
|
} else if entry.IsLink() { |
||||||
|
return nil, models.ErrFilePathInvalid{ |
||||||
|
Message: fmt.Sprintf("a symbolic link exists where you’re trying to create a subdirectory [path: %s]", subTreePath), |
||||||
|
Path: subTreePath, |
||||||
|
Name: part, |
||||||
|
Type: git.EntryModeSymlink, |
||||||
|
} |
||||||
|
} else if entry.IsDir() { |
||||||
|
return nil, models.ErrFilePathInvalid{ |
||||||
|
Message: fmt.Sprintf("a directory exists where you’re trying to create a file [path: %s]", subTreePath), |
||||||
|
Path: subTreePath, |
||||||
|
Name: part, |
||||||
|
Type: git.EntryModeTree, |
||||||
|
} |
||||||
|
} else if fromTreePath != treePath || opts.IsNewFile { |
||||||
|
// The entry shouldn't exist if we are creating new file or moving to a new path
|
||||||
|
return nil, models.ErrRepoFileAlreadyExists{ |
||||||
|
Path: treePath, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
// Get the two paths (might be the same if not moving) from the index if they exist
|
||||||
|
filesInIndex, err := t.LsFiles(opts.TreePath, opts.FromTreePath) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("UpdateRepoFile: %v", err) |
||||||
|
} |
||||||
|
// If is a new file (not updating) then the given path shouldn't exist
|
||||||
|
if opts.IsNewFile { |
||||||
|
for _, file := range filesInIndex { |
||||||
|
if file == opts.TreePath { |
||||||
|
return nil, models.ErrRepoFileAlreadyExists{ |
||||||
|
Path: opts.TreePath, |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Remove the old path from the tree
|
||||||
|
if fromTreePath != treePath && len(filesInIndex) > 0 { |
||||||
|
for _, file := range filesInIndex { |
||||||
|
if file == fromTreePath { |
||||||
|
if err := t.RemoveFilesFromIndex(opts.FromTreePath); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Check there is no way this can return multiple infos
|
||||||
|
filename2attribute2info, err := t.CheckAttribute("filter", treePath) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
content := opts.Content |
||||||
|
var lfsMetaObject *models.LFSMetaObject |
||||||
|
|
||||||
|
if filename2attribute2info[treePath] != nil && filename2attribute2info[treePath]["filter"] == "lfs" { |
||||||
|
// OK so we are supposed to LFS this data!
|
||||||
|
oid, err := models.GenerateLFSOid(strings.NewReader(opts.Content)) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
lfsMetaObject = &models.LFSMetaObject{Oid: oid, Size: int64(len(opts.Content)), RepositoryID: repo.ID} |
||||||
|
content = lfsMetaObject.Pointer() |
||||||
|
} |
||||||
|
|
||||||
|
// Add the object to the database
|
||||||
|
objectHash, err := t.HashObject(strings.NewReader(content)) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Add the object to the index
|
||||||
|
if err := t.AddObjectToIndex("100644", objectHash, treePath); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Now write the tree
|
||||||
|
treeHash, err := t.WriteTree() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Now commit the tree
|
||||||
|
commitHash, err := t.CommitTree(author, committer, treeHash, message) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
if lfsMetaObject != nil { |
||||||
|
// We have an LFS object - create it
|
||||||
|
lfsMetaObject, err = models.NewLFSMetaObject(lfsMetaObject) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
contentStore := &lfs.ContentStore{BasePath: setting.LFS.ContentPath} |
||||||
|
if !contentStore.Exists(lfsMetaObject) { |
||||||
|
if err := contentStore.Put(lfsMetaObject, strings.NewReader(opts.Content)); err != nil { |
||||||
|
if err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil { |
||||||
|
return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err) |
||||||
|
} |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Then push this tree to NewBranch
|
||||||
|
if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
// Simulate push event.
|
||||||
|
oldCommitID := opts.LastCommitID |
||||||
|
if opts.NewBranch != opts.OldBranch || oldCommitID == "" { |
||||||
|
oldCommitID = git.EmptySHA |
||||||
|
} |
||||||
|
|
||||||
|
if err = repo.GetOwner(); err != nil { |
||||||
|
return nil, fmt.Errorf("GetOwner: %v", err) |
||||||
|
} |
||||||
|
err = models.PushUpdate( |
||||||
|
opts.NewBranch, |
||||||
|
models.PushUpdateOptions{ |
||||||
|
PusherID: doer.ID, |
||||||
|
PusherName: doer.Name, |
||||||
|
RepoUserName: repo.Owner.Name, |
||||||
|
RepoName: repo.Name, |
||||||
|
RefFullName: git.BranchPrefix + opts.NewBranch, |
||||||
|
OldCommitID: oldCommitID, |
||||||
|
NewCommitID: commitHash, |
||||||
|
}, |
||||||
|
) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("PushUpdate: %v", err) |
||||||
|
} |
||||||
|
models.UpdateRepoIndexer(repo) |
||||||
|
|
||||||
|
commit, err = t.GetCommit(commitHash) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
|
||||||
|
file, err := GetFileResponseFromCommit(repo, commit, opts.NewBranch, treePath) |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return file, nil |
||||||
|
} |
@ -0,0 +1,357 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repofiles |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
"time" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
"code.gitea.io/gitea/modules/test" |
||||||
|
api "code.gitea.io/sdk/gitea" |
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
func getCreateRepoFileOptions(repo *models.Repository) *UpdateRepoFileOptions { |
||||||
|
return &UpdateRepoFileOptions{ |
||||||
|
OldBranch: repo.DefaultBranch, |
||||||
|
NewBranch: repo.DefaultBranch, |
||||||
|
TreePath: "new/file.txt", |
||||||
|
Message: "Creates new/file.txt", |
||||||
|
Content: "This is a NEW file", |
||||||
|
IsNewFile: true, |
||||||
|
Author: nil, |
||||||
|
Committer: nil, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func getUpdateRepoFileOptions(repo *models.Repository) *UpdateRepoFileOptions { |
||||||
|
return &UpdateRepoFileOptions{ |
||||||
|
OldBranch: repo.DefaultBranch, |
||||||
|
NewBranch: repo.DefaultBranch, |
||||||
|
TreePath: "README.md", |
||||||
|
Message: "Updates README.md", |
||||||
|
SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f", |
||||||
|
Content: "This is UPDATED content for the README file", |
||||||
|
IsNewFile: false, |
||||||
|
Author: nil, |
||||||
|
Committer: nil, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func getExpectedFileResponseForCreate(commitID string) *api.FileResponse { |
||||||
|
return &api.FileResponse{ |
||||||
|
Content: &api.FileContentResponse{ |
||||||
|
Name: "file.txt", |
||||||
|
Path: "new/file.txt", |
||||||
|
SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885", |
||||||
|
Size: 18, |
||||||
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/new/file.txt", |
||||||
|
HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/new/file.txt", |
||||||
|
GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/103ff9234cefeee5ec5361d22b49fbb04d385885", |
||||||
|
DownloadURL: "https://try.gitea.io/user2/repo1/raw/branch/master/new/file.txt", |
||||||
|
Type: "blob", |
||||||
|
Links: &api.FileLinksResponse{ |
||||||
|
Self: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/new/file.txt", |
||||||
|
GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/103ff9234cefeee5ec5361d22b49fbb04d385885", |
||||||
|
HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/new/file.txt", |
||||||
|
}, |
||||||
|
}, |
||||||
|
Commit: &api.FileCommitResponse{ |
||||||
|
CommitMeta: api.CommitMeta{ |
||||||
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/commits/" + commitID, |
||||||
|
SHA: commitID, |
||||||
|
}, |
||||||
|
HTMLURL: "https://try.gitea.io/user2/repo1/commit/" + commitID, |
||||||
|
Author: &api.CommitUser{ |
||||||
|
Identity: api.Identity{ |
||||||
|
Name: "User Two", |
||||||
|
Email: "user2@", |
||||||
|
}, |
||||||
|
Date: time.Now().UTC().Format(time.RFC3339), |
||||||
|
}, |
||||||
|
Committer: &api.CommitUser{ |
||||||
|
Identity: api.Identity{ |
||||||
|
Name: "User Two", |
||||||
|
Email: "user2@", |
||||||
|
}, |
||||||
|
Date: time.Now().UTC().Format(time.RFC3339), |
||||||
|
}, |
||||||
|
Parents: []*api.CommitMeta{ |
||||||
|
{ |
||||||
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d", |
||||||
|
SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", |
||||||
|
}, |
||||||
|
}, |
||||||
|
Message: "Updates README.md\n", |
||||||
|
Tree: &api.CommitMeta{ |
||||||
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/f93e3a1a1525fb5b91020da86e44810c87a2d7bc", |
||||||
|
SHA: "f93e3a1a1525fb5b91020git dda86e44810c87a2d7bc", |
||||||
|
}, |
||||||
|
}, |
||||||
|
Verification: &api.PayloadCommitVerification{ |
||||||
|
Verified: false, |
||||||
|
Reason: "unsigned", |
||||||
|
Signature: "", |
||||||
|
Payload: "", |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func getExpectedFileResponseForUpdate(commitID string) *api.FileResponse { |
||||||
|
return &api.FileResponse{ |
||||||
|
Content: &api.FileContentResponse{ |
||||||
|
Name: "README.md", |
||||||
|
Path: "README.md", |
||||||
|
SHA: "dbf8d00e022e05b7e5cf7e535de857de57925647", |
||||||
|
Size: 43, |
||||||
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md", |
||||||
|
HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/README.md", |
||||||
|
GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/dbf8d00e022e05b7e5cf7e535de857de57925647", |
||||||
|
DownloadURL: "https://try.gitea.io/user2/repo1/raw/branch/master/README.md", |
||||||
|
Type: "blob", |
||||||
|
Links: &api.FileLinksResponse{ |
||||||
|
Self: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md", |
||||||
|
GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/dbf8d00e022e05b7e5cf7e535de857de57925647", |
||||||
|
HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/README.md", |
||||||
|
}, |
||||||
|
}, |
||||||
|
Commit: &api.FileCommitResponse{ |
||||||
|
CommitMeta: api.CommitMeta{ |
||||||
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/commits/" + commitID, |
||||||
|
SHA: commitID, |
||||||
|
}, |
||||||
|
HTMLURL: "https://try.gitea.io/user2/repo1/commit/" + commitID, |
||||||
|
Author: &api.CommitUser{ |
||||||
|
Identity: api.Identity{ |
||||||
|
Name: "User Two", |
||||||
|
Email: "user2@", |
||||||
|
}, |
||||||
|
Date: time.Now().UTC().Format(time.RFC3339), |
||||||
|
}, |
||||||
|
Committer: &api.CommitUser{ |
||||||
|
Identity: api.Identity{ |
||||||
|
Name: "User Two", |
||||||
|
Email: "user2@", |
||||||
|
}, |
||||||
|
Date: time.Now().UTC().Format(time.RFC3339), |
||||||
|
}, |
||||||
|
Parents: []*api.CommitMeta{ |
||||||
|
{ |
||||||
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d", |
||||||
|
SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d", |
||||||
|
}, |
||||||
|
}, |
||||||
|
Message: "Updates README.md\n", |
||||||
|
Tree: &api.CommitMeta{ |
||||||
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/f93e3a1a1525fb5b91020da86e44810c87a2d7bc", |
||||||
|
SHA: "f93e3a1a1525fb5b91020da86e44810c87a2d7bc", |
||||||
|
}, |
||||||
|
}, |
||||||
|
Verification: &api.PayloadCommitVerification{ |
||||||
|
Verified: false, |
||||||
|
Reason: "unsigned", |
||||||
|
Signature: "", |
||||||
|
Payload: "", |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestCreateOrUpdateRepoFileForCreate(t *testing.T) { |
||||||
|
// setup
|
||||||
|
models.PrepareTestEnv(t) |
||||||
|
ctx := test.MockContext(t, "user2/repo1") |
||||||
|
ctx.SetParams(":id", "1") |
||||||
|
test.LoadRepo(t, ctx, 1) |
||||||
|
test.LoadRepoCommit(t, ctx) |
||||||
|
test.LoadUser(t, ctx, 2) |
||||||
|
test.LoadGitRepo(t, ctx) |
||||||
|
repo := ctx.Repo.Repository |
||||||
|
doer := ctx.User |
||||||
|
opts := getCreateRepoFileOptions(repo) |
||||||
|
|
||||||
|
// test
|
||||||
|
fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts) |
||||||
|
|
||||||
|
// asserts
|
||||||
|
assert.Nil(t, err) |
||||||
|
gitRepo, _ := git.OpenRepository(repo.RepoPath()) |
||||||
|
commitID, _ := gitRepo.GetBranchCommitID(opts.NewBranch) |
||||||
|
expectedFileResponse := getExpectedFileResponseForCreate(commitID) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name) |
||||||
|
} |
||||||
|
|
||||||
|
func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) { |
||||||
|
// setup
|
||||||
|
models.PrepareTestEnv(t) |
||||||
|
ctx := test.MockContext(t, "user2/repo1") |
||||||
|
ctx.SetParams(":id", "1") |
||||||
|
test.LoadRepo(t, ctx, 1) |
||||||
|
test.LoadRepoCommit(t, ctx) |
||||||
|
test.LoadUser(t, ctx, 2) |
||||||
|
test.LoadGitRepo(t, ctx) |
||||||
|
repo := ctx.Repo.Repository |
||||||
|
doer := ctx.User |
||||||
|
opts := getUpdateRepoFileOptions(repo) |
||||||
|
|
||||||
|
// test
|
||||||
|
fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts) |
||||||
|
|
||||||
|
// asserts
|
||||||
|
assert.Nil(t, err) |
||||||
|
gitRepo, _ := git.OpenRepository(repo.RepoPath()) |
||||||
|
commitID, _ := gitRepo.GetBranchCommitID(opts.NewBranch) |
||||||
|
expectedFileResponse := getExpectedFileResponseForUpdate(commitID) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name) |
||||||
|
} |
||||||
|
|
||||||
|
func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) { |
||||||
|
// setup
|
||||||
|
models.PrepareTestEnv(t) |
||||||
|
ctx := test.MockContext(t, "user2/repo1") |
||||||
|
ctx.SetParams(":id", "1") |
||||||
|
test.LoadRepo(t, ctx, 1) |
||||||
|
test.LoadRepoCommit(t, ctx) |
||||||
|
test.LoadUser(t, ctx, 2) |
||||||
|
test.LoadGitRepo(t, ctx) |
||||||
|
repo := ctx.Repo.Repository |
||||||
|
doer := ctx.User |
||||||
|
opts := getUpdateRepoFileOptions(repo) |
||||||
|
suffix := "_new" |
||||||
|
opts.FromTreePath = "README.md" |
||||||
|
opts.TreePath = "README.md" + suffix // new file name, README.md_new
|
||||||
|
|
||||||
|
// test
|
||||||
|
fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts) |
||||||
|
|
||||||
|
// asserts
|
||||||
|
assert.Nil(t, err) |
||||||
|
gitRepo, _ := git.OpenRepository(repo.RepoPath()) |
||||||
|
commit, _ := gitRepo.GetBranchCommit(opts.NewBranch) |
||||||
|
expectedFileResponse := getExpectedFileResponseForUpdate(commit.ID.String()) |
||||||
|
// assert that the old file no longer exists in the last commit of the branch
|
||||||
|
fromEntry, err := commit.GetTreeEntryByPath(opts.FromTreePath) |
||||||
|
toEntry, err := commit.GetTreeEntryByPath(opts.TreePath) |
||||||
|
assert.Nil(t, fromEntry) // Should no longer exist here
|
||||||
|
assert.NotNil(t, toEntry) // Should exist here
|
||||||
|
// assert SHA has remained the same but paths use the new file name
|
||||||
|
assert.EqualValues(t, expectedFileResponse.Content.SHA, fileResponse.Content.SHA) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Content.Name+suffix, fileResponse.Content.Name) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Content.Path+suffix, fileResponse.Content.Path) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Content.URL+suffix, fileResponse.Content.URL) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL) |
||||||
|
} |
||||||
|
|
||||||
|
// Test opts with branch names removed, should get same results as above test
|
||||||
|
func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) { |
||||||
|
// setup
|
||||||
|
models.PrepareTestEnv(t) |
||||||
|
ctx := test.MockContext(t, "user2/repo1") |
||||||
|
ctx.SetParams(":id", "1") |
||||||
|
test.LoadRepo(t, ctx, 1) |
||||||
|
test.LoadRepoCommit(t, ctx) |
||||||
|
test.LoadUser(t, ctx, 2) |
||||||
|
test.LoadGitRepo(t, ctx) |
||||||
|
repo := ctx.Repo.Repository |
||||||
|
doer := ctx.User |
||||||
|
opts := getUpdateRepoFileOptions(repo) |
||||||
|
opts.OldBranch = "" |
||||||
|
opts.NewBranch = "" |
||||||
|
|
||||||
|
// test
|
||||||
|
fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts) |
||||||
|
|
||||||
|
// asserts
|
||||||
|
assert.Nil(t, err) |
||||||
|
gitRepo, _ := git.OpenRepository(repo.RepoPath()) |
||||||
|
commitID, _ := gitRepo.GetBranchCommitID(repo.DefaultBranch) |
||||||
|
expectedFileResponse := getExpectedFileResponseForUpdate(commitID) |
||||||
|
assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content) |
||||||
|
} |
||||||
|
|
||||||
|
func TestCreateOrUpdateRepoFileErrors(t *testing.T) { |
||||||
|
// setup
|
||||||
|
models.PrepareTestEnv(t) |
||||||
|
ctx := test.MockContext(t, "user2/repo1") |
||||||
|
ctx.SetParams(":id", "1") |
||||||
|
test.LoadRepo(t, ctx, 1) |
||||||
|
test.LoadRepoCommit(t, ctx) |
||||||
|
test.LoadUser(t, ctx, 2) |
||||||
|
test.LoadGitRepo(t, ctx) |
||||||
|
repo := ctx.Repo.Repository |
||||||
|
doer := ctx.User |
||||||
|
|
||||||
|
t.Run("bad branch", func(t *testing.T) { |
||||||
|
opts := getUpdateRepoFileOptions(repo) |
||||||
|
opts.OldBranch = "bad_branch" |
||||||
|
fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts) |
||||||
|
assert.Error(t, err) |
||||||
|
assert.Nil(t, fileResponse) |
||||||
|
expectedError := "branch does not exist [name: " + opts.OldBranch + "]" |
||||||
|
assert.EqualError(t, err, expectedError) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("bad SHA", func(t *testing.T) { |
||||||
|
opts := getUpdateRepoFileOptions(repo) |
||||||
|
origSHA := opts.SHA |
||||||
|
opts.SHA = "bad_sha" |
||||||
|
fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts) |
||||||
|
assert.Nil(t, fileResponse) |
||||||
|
assert.Error(t, err) |
||||||
|
expectedError := "sha does not match [given: " + opts.SHA + ", expected: " + origSHA + "]" |
||||||
|
assert.EqualError(t, err, expectedError) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("new branch already exists", func(t *testing.T) { |
||||||
|
opts := getUpdateRepoFileOptions(repo) |
||||||
|
opts.NewBranch = "develop" |
||||||
|
fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts) |
||||||
|
assert.Nil(t, fileResponse) |
||||||
|
assert.Error(t, err) |
||||||
|
expectedError := "branch already exists [name: " + opts.NewBranch + "]" |
||||||
|
assert.EqualError(t, err, expectedError) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("treePath is empty:", func(t *testing.T) { |
||||||
|
opts := getUpdateRepoFileOptions(repo) |
||||||
|
opts.TreePath = "" |
||||||
|
fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts) |
||||||
|
assert.Nil(t, fileResponse) |
||||||
|
assert.Error(t, err) |
||||||
|
expectedError := "path contains a malformed path component [path: ]" |
||||||
|
assert.EqualError(t, err, expectedError) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("treePath is a git directory:", func(t *testing.T) { |
||||||
|
opts := getUpdateRepoFileOptions(repo) |
||||||
|
opts.TreePath = ".git" |
||||||
|
fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts) |
||||||
|
assert.Nil(t, fileResponse) |
||||||
|
assert.Error(t, err) |
||||||
|
expectedError := "path contains a malformed path component [path: " + opts.TreePath + "]" |
||||||
|
assert.EqualError(t, err, expectedError) |
||||||
|
}) |
||||||
|
|
||||||
|
t.Run("create file that already exists", func(t *testing.T) { |
||||||
|
opts := getCreateRepoFileOptions(repo) |
||||||
|
opts.TreePath = "README.md" //already exists
|
||||||
|
fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts) |
||||||
|
assert.Nil(t, fileResponse) |
||||||
|
assert.Error(t, err) |
||||||
|
expectedError := "repository file already exists [path: " + opts.TreePath + "]" |
||||||
|
assert.EqualError(t, err, expectedError) |
||||||
|
}) |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repofiles |
||||||
|
|
||||||
|
import ( |
||||||
|
"code.gitea.io/gitea/models" |
||||||
|
"code.gitea.io/gitea/modules/git" |
||||||
|
"code.gitea.io/sdk/gitea" |
||||||
|
) |
||||||
|
|
||||||
|
// GetPayloadCommitVerification returns the verification information of a commit
|
||||||
|
func GetPayloadCommitVerification(commit *git.Commit) *gitea.PayloadCommitVerification { |
||||||
|
verification := &gitea.PayloadCommitVerification{} |
||||||
|
commitVerification := models.ParseCommitWithSignature(commit) |
||||||
|
if commit.Signature != nil { |
||||||
|
verification.Signature = commit.Signature.Signature |
||||||
|
verification.Payload = commit.Signature.Payload |
||||||
|
} |
||||||
|
if verification.Reason != "" { |
||||||
|
verification.Reason = commitVerification.Reason |
||||||
|
} else { |
||||||
|
if verification.Verified { |
||||||
|
verification.Reason = "unsigned" |
||||||
|
} |
||||||
|
} |
||||||
|
return verification |
||||||
|
} |
@ -1,100 +0,0 @@ |
|||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package uploader |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
|
|
||||||
"code.gitea.io/gitea/models" |
|
||||||
"code.gitea.io/gitea/modules/git" |
|
||||||
) |
|
||||||
|
|
||||||
// DeleteRepoFileOptions holds the repository delete file options
|
|
||||||
type DeleteRepoFileOptions struct { |
|
||||||
LastCommitID string |
|
||||||
OldBranch string |
|
||||||
NewBranch string |
|
||||||
TreePath string |
|
||||||
Message string |
|
||||||
} |
|
||||||
|
|
||||||
// DeleteRepoFile deletes a file in the given repository
|
|
||||||
func DeleteRepoFile(repo *models.Repository, doer *models.User, opts *DeleteRepoFileOptions) error { |
|
||||||
t, err := NewTemporaryUploadRepository(repo) |
|
||||||
defer t.Close() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if err := t.Clone(opts.OldBranch); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if err := t.SetDefaultIndex(); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
filesInIndex, err := t.LsFiles(opts.TreePath) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("UpdateRepoFile: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
inFilelist := false |
|
||||||
for _, file := range filesInIndex { |
|
||||||
if file == opts.TreePath { |
|
||||||
inFilelist = true |
|
||||||
} |
|
||||||
} |
|
||||||
if !inFilelist { |
|
||||||
return git.ErrNotExist{RelPath: opts.TreePath} |
|
||||||
} |
|
||||||
|
|
||||||
if err := t.RemoveFilesFromIndex(opts.TreePath); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// Now write the tree
|
|
||||||
treeHash, err := t.WriteTree() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// Now commit the tree
|
|
||||||
commitHash, err := t.CommitTree(doer, treeHash, opts.Message) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// Then push this tree to NewBranch
|
|
||||||
if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// Simulate push event.
|
|
||||||
oldCommitID := opts.LastCommitID |
|
||||||
if opts.NewBranch != opts.OldBranch { |
|
||||||
oldCommitID = git.EmptySHA |
|
||||||
} |
|
||||||
|
|
||||||
if err = repo.GetOwner(); err != nil { |
|
||||||
return fmt.Errorf("GetOwner: %v", err) |
|
||||||
} |
|
||||||
err = models.PushUpdate( |
|
||||||
opts.NewBranch, |
|
||||||
models.PushUpdateOptions{ |
|
||||||
PusherID: doer.ID, |
|
||||||
PusherName: doer.Name, |
|
||||||
RepoUserName: repo.Owner.Name, |
|
||||||
RepoName: repo.Name, |
|
||||||
RefFullName: git.BranchPrefix + opts.NewBranch, |
|
||||||
OldCommitID: oldCommitID, |
|
||||||
NewCommitID: commitHash, |
|
||||||
}, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("PushUpdate: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
// FIXME: Should we UpdateRepoIndexer(repo) here?
|
|
||||||
return nil |
|
||||||
} |
|
@ -1,159 +0,0 @@ |
|||||||
// Copyright 2019 The Gitea Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package uploader |
|
||||||
|
|
||||||
import ( |
|
||||||
"fmt" |
|
||||||
"strings" |
|
||||||
|
|
||||||
"code.gitea.io/gitea/models" |
|
||||||
"code.gitea.io/gitea/modules/git" |
|
||||||
"code.gitea.io/gitea/modules/lfs" |
|
||||||
"code.gitea.io/gitea/modules/setting" |
|
||||||
) |
|
||||||
|
|
||||||
// UpdateRepoFileOptions holds the repository file update options
|
|
||||||
type UpdateRepoFileOptions struct { |
|
||||||
LastCommitID string |
|
||||||
OldBranch string |
|
||||||
NewBranch string |
|
||||||
OldTreeName string |
|
||||||
NewTreeName string |
|
||||||
Message string |
|
||||||
Content string |
|
||||||
IsNewFile bool |
|
||||||
} |
|
||||||
|
|
||||||
// UpdateRepoFile adds or updates a file in the given repository
|
|
||||||
func UpdateRepoFile(repo *models.Repository, doer *models.User, opts *UpdateRepoFileOptions) error { |
|
||||||
t, err := NewTemporaryUploadRepository(repo) |
|
||||||
defer t.Close() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if err := t.Clone(opts.OldBranch); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
if err := t.SetDefaultIndex(); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
filesInIndex, err := t.LsFiles(opts.NewTreeName, opts.OldTreeName) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("UpdateRepoFile: %v", err) |
|
||||||
} |
|
||||||
|
|
||||||
if opts.IsNewFile { |
|
||||||
for _, file := range filesInIndex { |
|
||||||
if file == opts.NewTreeName { |
|
||||||
return models.ErrRepoFileAlreadyExist{FileName: opts.NewTreeName} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
//var stdout string
|
|
||||||
if opts.OldTreeName != opts.NewTreeName && len(filesInIndex) > 0 { |
|
||||||
for _, file := range filesInIndex { |
|
||||||
if file == opts.OldTreeName { |
|
||||||
if err := t.RemoveFilesFromIndex(opts.OldTreeName); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
// Check there is no way this can return multiple infos
|
|
||||||
filename2attribute2info, err := t.CheckAttribute("filter", opts.NewTreeName) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
content := opts.Content |
|
||||||
var lfsMetaObject *models.LFSMetaObject |
|
||||||
|
|
||||||
if filename2attribute2info[opts.NewTreeName] != nil && filename2attribute2info[opts.NewTreeName]["filter"] == "lfs" { |
|
||||||
// OK so we are supposed to LFS this data!
|
|
||||||
oid, err := models.GenerateLFSOid(strings.NewReader(opts.Content)) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
lfsMetaObject = &models.LFSMetaObject{Oid: oid, Size: int64(len(opts.Content)), RepositoryID: repo.ID} |
|
||||||
content = lfsMetaObject.Pointer() |
|
||||||
} |
|
||||||
|
|
||||||
// Add the object to the database
|
|
||||||
objectHash, err := t.HashObject(strings.NewReader(content)) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// Add the object to the index
|
|
||||||
if err := t.AddObjectToIndex("100644", objectHash, opts.NewTreeName); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// Now write the tree
|
|
||||||
treeHash, err := t.WriteTree() |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// Now commit the tree
|
|
||||||
commitHash, err := t.CommitTree(doer, treeHash, opts.Message) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
if lfsMetaObject != nil { |
|
||||||
// We have an LFS object - create it
|
|
||||||
lfsMetaObject, err = models.NewLFSMetaObject(lfsMetaObject) |
|
||||||
if err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
contentStore := &lfs.ContentStore{BasePath: setting.LFS.ContentPath} |
|
||||||
if !contentStore.Exists(lfsMetaObject) { |
|
||||||
if err := contentStore.Put(lfsMetaObject, strings.NewReader(opts.Content)); err != nil { |
|
||||||
if err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil { |
|
||||||
return fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err) |
|
||||||
} |
|
||||||
return err |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Then push this tree to NewBranch
|
|
||||||
if err := t.Push(doer, commitHash, opts.NewBranch); err != nil { |
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
// Simulate push event.
|
|
||||||
oldCommitID := opts.LastCommitID |
|
||||||
if opts.NewBranch != opts.OldBranch { |
|
||||||
oldCommitID = git.EmptySHA |
|
||||||
} |
|
||||||
|
|
||||||
if err = repo.GetOwner(); err != nil { |
|
||||||
return fmt.Errorf("GetOwner: %v", err) |
|
||||||
} |
|
||||||
err = models.PushUpdate( |
|
||||||
opts.NewBranch, |
|
||||||
models.PushUpdateOptions{ |
|
||||||
PusherID: doer.ID, |
|
||||||
PusherName: doer.Name, |
|
||||||
RepoUserName: repo.Owner.Name, |
|
||||||
RepoName: repo.Name, |
|
||||||
RefFullName: git.BranchPrefix + opts.NewBranch, |
|
||||||
OldCommitID: oldCommitID, |
|
||||||
NewCommitID: commitHash, |
|
||||||
}, |
|
||||||
) |
|
||||||
if err != nil { |
|
||||||
return fmt.Errorf("PushUpdate: %v", err) |
|
||||||
} |
|
||||||
models.UpdateRepoIndexer(repo) |
|
||||||
|
|
||||||
return nil |
|
||||||
} |
|
@ -0,0 +1,51 @@ |
|||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package repo |
||||||
|
|
||||||
|
import ( |
||||||
|
"net/http" |
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/context" |
||||||
|
"code.gitea.io/gitea/modules/repofiles" |
||||||
|
) |
||||||
|
|
||||||
|
// GetBlob get the blob of a repository file.
|
||||||
|
func GetBlob(ctx *context.APIContext) { |
||||||
|
// swagger:operation GET /repos/{owner}/{repo}/git/blobs/{sha} repository GetBlob
|
||||||
|
// ---
|
||||||
|
// summary: Gets the blob of a repository.
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
// parameters:
|
||||||
|
// - name: owner
|
||||||
|
// in: path
|
||||||
|
// description: owner of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: repo
|
||||||
|
// in: path
|
||||||
|
// description: name of the repo
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: sha
|
||||||
|
// in: path
|
||||||
|
// description: sha of the commit
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/GitBlobResponse"
|
||||||
|
|
||||||
|
sha := ctx.Params("sha") |
||||||
|
if len(sha) == 0 { |
||||||
|
ctx.Error(http.StatusBadRequest, "", "sha not provided") |
||||||
|
return |
||||||
|
} |
||||||
|
if blob, err := repofiles.GetBlobBySHA(ctx.Repo.Repository, sha); err != nil { |
||||||
|
ctx.Error(http.StatusBadRequest, "", err) |
||||||
|
} else { |
||||||
|
ctx.JSON(http.StatusOK, blob) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue