Fixes 4762 - Content API for Creating, Updating, Deleting Files (#6314)
This commit is contained in:
parent
059195b127
commit
2262811e40
54 changed files with 4154 additions and 563 deletions
|
@ -672,6 +672,8 @@ MAX_RESPONSE_ITEMS = 50
|
||||||
DEFAULT_PAGING_NUM = 30
|
DEFAULT_PAGING_NUM = 30
|
||||||
; Default and maximum number of items per page for git trees api
|
; Default and maximum number of items per page for git trees api
|
||||||
DEFAULT_GIT_TREES_PER_PAGE = 1000
|
DEFAULT_GIT_TREES_PER_PAGE = 1000
|
||||||
|
; Default size of a blob returned by the blobs API (default is 10MiB)
|
||||||
|
DEFAULT_MAX_BLOB_SIZE = 10485760
|
||||||
|
|
||||||
[oauth2]
|
[oauth2]
|
||||||
; Enables OAuth2 provider
|
; Enables OAuth2 provider
|
||||||
|
|
|
@ -402,8 +402,9 @@ NB: You must `REDIRECT_MACARON_LOG` and have `DISABLE_ROUTER_LOG` set to `false`
|
||||||
|
|
||||||
- `ENABLE_SWAGGER`: **true**: Enables /api/swagger, /api/v1/swagger etc. endpoints. True or false; default is true.
|
- `ENABLE_SWAGGER`: **true**: Enables /api/swagger, /api/v1/swagger etc. endpoints. True or false; default is true.
|
||||||
- `MAX_RESPONSE_ITEMS`: **50**: Max number of items in a page.
|
- `MAX_RESPONSE_ITEMS`: **50**: Max number of items in a page.
|
||||||
- `DEFAULT_PAGING_NUM`: **30**: Default paging number of api.
|
- `DEFAULT_PAGING_NUM`: **30**: Default paging number of API.
|
||||||
- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: Default and maximum number of items per page for git trees api.
|
- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: Default and maximum number of items per page for git trees API.
|
||||||
|
- `DEFAULT_MAX_BLOB_SIZE`: **10485760**: Default max size of a blob that can be return by the blobs API.
|
||||||
|
|
||||||
## OAuth2 (`oauth2`)
|
## OAuth2 (`oauth2`)
|
||||||
|
|
||||||
|
|
|
@ -215,7 +215,8 @@ menu:
|
||||||
- `ENABLE_SWAGGER`: **true**: 是否启用swagger路由 /api/swagger, /api/v1/swagger etc. endpoints. True 或 false; 默认是 true.
|
- `ENABLE_SWAGGER`: **true**: 是否启用swagger路由 /api/swagger, /api/v1/swagger etc. endpoints. True 或 false; 默认是 true.
|
||||||
- `MAX_RESPONSE_ITEMS`: **50**: 一个页面最大的项目数。
|
- `MAX_RESPONSE_ITEMS`: **50**: 一个页面最大的项目数。
|
||||||
- `DEFAULT_PAGING_NUM`: **30**: API中默认分页条数。
|
- `DEFAULT_PAGING_NUM`: **30**: API中默认分页条数。
|
||||||
- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: GIT TREES API每页的默认和最大项数.
|
- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: GIT TREES API每页的默认最大项数.
|
||||||
|
- `DEFAULT_MAX_BLOB_SIZE`: **10485760**: BLOBS API默认最大大小.
|
||||||
|
|
||||||
## Markup (`markup`)
|
## Markup (`markup`)
|
||||||
|
|
||||||
|
|
114
integrations/api_repo_file_content_test.go
Normal file
114
integrations/api_repo_file_content_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
215
integrations/api_repo_file_create_test.go
Normal file
215
integrations/api_repo_file_create_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
163
integrations/api_repo_file_delete_test.go
Normal file
163
integrations/api_repo_file_delete_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
27
integrations/api_repo_file_helpers.go
Normal file
27
integrations/api_repo_file_helpers.go
Normal file
|
@ -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)
|
||||||
|
}
|
234
integrations/api_repo_file_update_test.go
Normal file
234
integrations/api_repo_file_update_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
76
integrations/api_repo_git_blobs_test.go
Normal file
76
integrations/api_repo_git_blobs_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
74
integrations/api_repo_git_trees_test.go
Normal file
74
integrations/api_repo_git_trees_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/routers"
|
"code.gitea.io/gitea/routers"
|
||||||
"code.gitea.io/gitea/routers/routes"
|
"code.gitea.io/gitea/routers/routes"
|
||||||
|
@ -96,7 +97,7 @@ func TestMain(m *testing.M) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func initIntegrationTest() {
|
func initIntegrationTest() {
|
||||||
giteaRoot := os.Getenv("GITEA_ROOT")
|
giteaRoot := base.SetupGiteaRoot()
|
||||||
if giteaRoot == "" {
|
if giteaRoot == "" {
|
||||||
fmt.Println("Environment variable $GITEA_ROOT not set")
|
fmt.Println("Environment variable $GITEA_ROOT not set")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"code.gitea.io/gitea/integrations"
|
"code.gitea.io/gitea/integrations"
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/models/migrations"
|
"code.gitea.io/gitea/models/migrations"
|
||||||
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/go-xorm/xorm"
|
"github.com/go-xorm/xorm"
|
||||||
|
@ -28,7 +29,7 @@ var currentEngine *xorm.Engine
|
||||||
|
|
||||||
func initMigrationTest(t *testing.T) {
|
func initMigrationTest(t *testing.T) {
|
||||||
integrations.PrintCurrentTest(t, 2)
|
integrations.PrintCurrentTest(t, 2)
|
||||||
giteaRoot := os.Getenv("GITEA_ROOT")
|
giteaRoot := base.SetupGiteaRoot()
|
||||||
if giteaRoot == "" {
|
if giteaRoot == "" {
|
||||||
integrations.Printf("Environment variable $GITEA_ROOT not set\n")
|
integrations.Printf("Environment variable $GITEA_ROOT not set\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|
168
models/error.go
168
models/error.go
|
@ -1,10 +1,15 @@
|
||||||
// Copyright 2015 The Gogs Authors. All rights reserved.
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
)
|
||||||
|
|
||||||
// ErrNameReserved represents a "reserved name" error.
|
// ErrNameReserved represents a "reserved name" error.
|
||||||
type ErrNameReserved struct {
|
type ErrNameReserved struct {
|
||||||
|
@ -26,8 +31,7 @@ type ErrNamePatternNotAllowed struct {
|
||||||
Pattern string
|
Pattern string
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrNamePatternNotAllowed checks if an error is an
|
// IsErrNamePatternNotAllowed checks if an error is an ErrNamePatternNotAllowed.
|
||||||
// ErrNamePatternNotAllowed.
|
|
||||||
func IsErrNamePatternNotAllowed(err error) bool {
|
func IsErrNamePatternNotAllowed(err error) bool {
|
||||||
_, ok := err.(ErrNamePatternNotAllowed)
|
_, ok := err.(ErrNamePatternNotAllowed)
|
||||||
return ok
|
return ok
|
||||||
|
@ -676,7 +680,7 @@ type ErrRepoRedirectNotExist struct {
|
||||||
RepoName string
|
RepoName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrRepoRedirectNotExist check if an error is an ErrRepoRedirectNotExist
|
// IsErrRepoRedirectNotExist check if an error is an ErrRepoRedirectNotExist.
|
||||||
func IsErrRepoRedirectNotExist(err error) bool {
|
func IsErrRepoRedirectNotExist(err error) bool {
|
||||||
_, ok := err.(ErrRepoRedirectNotExist)
|
_, ok := err.(ErrRepoRedirectNotExist)
|
||||||
return ok
|
return ok
|
||||||
|
@ -765,28 +769,95 @@ func (err ErrInvalidTagName) Error() string {
|
||||||
return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName)
|
return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrRepoFileAlreadyExist represents a "RepoFileAlreadyExist" kind of error.
|
// ErrRepoFileAlreadyExists represents a "RepoFileAlreadyExist" kind of error.
|
||||||
type ErrRepoFileAlreadyExist struct {
|
type ErrRepoFileAlreadyExists struct {
|
||||||
FileName string
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrRepoFileAlreadyExist checks if an error is a ErrRepoFileAlreadyExist.
|
// IsErrRepoFileAlreadyExists checks if an error is a ErrRepoFileAlreadyExists.
|
||||||
func IsErrRepoFileAlreadyExist(err error) bool {
|
func IsErrRepoFileAlreadyExists(err error) bool {
|
||||||
_, ok := err.(ErrRepoFileAlreadyExist)
|
_, ok := err.(ErrRepoFileAlreadyExists)
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (err ErrRepoFileAlreadyExist) Error() string {
|
func (err ErrRepoFileAlreadyExists) Error() string {
|
||||||
return fmt.Sprintf("repository file already exists [file_name: %s]", err.FileName)
|
return fmt.Sprintf("repository file already exists [path: %s]", err.Path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo
|
// ErrRepoFileDoesNotExist represents a "RepoFileDoesNotExist" kind of error.
|
||||||
|
type ErrRepoFileDoesNotExist struct {
|
||||||
|
Path string
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrRepoFileDoesNotExist checks if an error is a ErrRepoDoesNotExist.
|
||||||
|
func IsErrRepoFileDoesNotExist(err error) bool {
|
||||||
|
_, ok := err.(ErrRepoFileDoesNotExist)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrRepoFileDoesNotExist) Error() string {
|
||||||
|
return fmt.Sprintf("repository file does not exist [path: %s]", err.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrFilenameInvalid represents a "FilenameInvalid" kind of error.
|
||||||
|
type ErrFilenameInvalid struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrFilenameInvalid checks if an error is an ErrFilenameInvalid.
|
||||||
|
func IsErrFilenameInvalid(err error) bool {
|
||||||
|
_, ok := err.(ErrFilenameInvalid)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrFilenameInvalid) Error() string {
|
||||||
|
return fmt.Sprintf("path contains a malformed path component [path: %s]", err.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUserCannotCommit represents "UserCannotCommit" kind of error.
|
||||||
|
type ErrUserCannotCommit struct {
|
||||||
|
UserName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrUserCannotCommit checks if an error is an ErrUserCannotCommit.
|
||||||
|
func IsErrUserCannotCommit(err error) bool {
|
||||||
|
_, ok := err.(ErrUserCannotCommit)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrUserCannotCommit) Error() string {
|
||||||
|
return fmt.Sprintf("user cannot commit to repo [user: %s]", err.UserName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrFilePathInvalid represents a "FilePathInvalid" kind of error.
|
||||||
|
type ErrFilePathInvalid struct {
|
||||||
|
Message string
|
||||||
|
Path string
|
||||||
|
Name string
|
||||||
|
Type git.EntryMode
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrFilePathInvalid checks if an error is an ErrFilePathInvalid.
|
||||||
|
func IsErrFilePathInvalid(err error) bool {
|
||||||
|
_, ok := err.(ErrFilePathInvalid)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrFilePathInvalid) Error() string {
|
||||||
|
if err.Message != "" {
|
||||||
|
return err.Message
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("path is invalid [path: %s]", err.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo.
|
||||||
type ErrUserDoesNotHaveAccessToRepo struct {
|
type ErrUserDoesNotHaveAccessToRepo struct {
|
||||||
UserID int64
|
UserID int64
|
||||||
RepoName string
|
RepoName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExist.
|
// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExists.
|
||||||
func IsErrUserDoesNotHaveAccessToRepo(err error) bool {
|
func IsErrUserDoesNotHaveAccessToRepo(err error) bool {
|
||||||
_, ok := err.(ErrUserDoesNotHaveAccessToRepo)
|
_, ok := err.(ErrUserDoesNotHaveAccessToRepo)
|
||||||
return ok
|
return ok
|
||||||
|
@ -818,7 +889,7 @@ func (err ErrBranchNotExist) Error() string {
|
||||||
return fmt.Sprintf("branch does not exist [name: %s]", err.Name)
|
return fmt.Sprintf("branch does not exist [name: %s]", err.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrBranchAlreadyExists represents an error that branch with such name already exists
|
// ErrBranchAlreadyExists represents an error that branch with such name already exists.
|
||||||
type ErrBranchAlreadyExists struct {
|
type ErrBranchAlreadyExists struct {
|
||||||
BranchName string
|
BranchName string
|
||||||
}
|
}
|
||||||
|
@ -833,7 +904,7 @@ func (err ErrBranchAlreadyExists) Error() string {
|
||||||
return fmt.Sprintf("branch already exists [name: %s]", err.BranchName)
|
return fmt.Sprintf("branch already exists [name: %s]", err.BranchName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrBranchNameConflict represents an error that branch name conflicts with other branch
|
// ErrBranchNameConflict represents an error that branch name conflicts with other branch.
|
||||||
type ErrBranchNameConflict struct {
|
type ErrBranchNameConflict struct {
|
||||||
BranchName string
|
BranchName string
|
||||||
}
|
}
|
||||||
|
@ -848,7 +919,7 @@ func (err ErrBranchNameConflict) Error() string {
|
||||||
return fmt.Sprintf("branch conflicts with existing branch [name: %s]", err.BranchName)
|
return fmt.Sprintf("branch conflicts with existing branch [name: %s]", err.BranchName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrNotAllowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it
|
// ErrNotAllowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it.
|
||||||
type ErrNotAllowedToMerge struct {
|
type ErrNotAllowedToMerge struct {
|
||||||
Reason string
|
Reason string
|
||||||
}
|
}
|
||||||
|
@ -863,7 +934,7 @@ func (err ErrNotAllowedToMerge) Error() string {
|
||||||
return fmt.Sprintf("not allowed to merge [reason: %s]", err.Reason)
|
return fmt.Sprintf("not allowed to merge [reason: %s]", err.Reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrTagAlreadyExists represents an error that tag with such name already exists
|
// ErrTagAlreadyExists represents an error that tag with such name already exists.
|
||||||
type ErrTagAlreadyExists struct {
|
type ErrTagAlreadyExists struct {
|
||||||
TagName string
|
TagName string
|
||||||
}
|
}
|
||||||
|
@ -878,6 +949,67 @@ func (err ErrTagAlreadyExists) Error() string {
|
||||||
return fmt.Sprintf("tag already exists [name: %s]", err.TagName)
|
return fmt.Sprintf("tag already exists [name: %s]", err.TagName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrSHADoesNotMatch represents a "SHADoesNotMatch" kind of error.
|
||||||
|
type ErrSHADoesNotMatch struct {
|
||||||
|
Path string
|
||||||
|
GivenSHA string
|
||||||
|
CurrentSHA string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrSHADoesNotMatch checks if an error is a ErrSHADoesNotMatch.
|
||||||
|
func IsErrSHADoesNotMatch(err error) bool {
|
||||||
|
_, ok := err.(ErrSHADoesNotMatch)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrSHADoesNotMatch) Error() string {
|
||||||
|
return fmt.Sprintf("sha does not match [given: %s, expected: %s]", err.GivenSHA, err.CurrentSHA)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrSHANotFound represents a "SHADoesNotMatch" kind of error.
|
||||||
|
type ErrSHANotFound struct {
|
||||||
|
SHA string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrSHANotFound checks if an error is a ErrSHANotFound.
|
||||||
|
func IsErrSHANotFound(err error) bool {
|
||||||
|
_, ok := err.(ErrSHANotFound)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrSHANotFound) Error() string {
|
||||||
|
return fmt.Sprintf("sha not found [%s]", err.SHA)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrCommitIDDoesNotMatch represents a "CommitIDDoesNotMatch" kind of error.
|
||||||
|
type ErrCommitIDDoesNotMatch struct {
|
||||||
|
GivenCommitID string
|
||||||
|
CurrentCommitID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsErrCommitIDDoesNotMatch checks if an error is a ErrCommitIDDoesNotMatch.
|
||||||
|
func IsErrCommitIDDoesNotMatch(err error) bool {
|
||||||
|
_, ok := err.(ErrCommitIDDoesNotMatch)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrCommitIDDoesNotMatch) Error() string {
|
||||||
|
return fmt.Sprintf("file CommitID does not match [given: %s, expected: %s]", err.GivenCommitID, err.CurrentCommitID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrSHAOrCommitIDNotProvided represents a "SHAOrCommitIDNotProvided" kind of error.
|
||||||
|
type ErrSHAOrCommitIDNotProvided struct{}
|
||||||
|
|
||||||
|
// IsErrSHAOrCommitIDNotProvided checks if an error is a ErrSHAOrCommitIDNotProvided.
|
||||||
|
func IsErrSHAOrCommitIDNotProvided(err error) bool {
|
||||||
|
_, ok := err.(ErrSHAOrCommitIDNotProvided)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err ErrSHAOrCommitIDNotProvided) Error() string {
|
||||||
|
return fmt.Sprintf("a SHA or commmit ID must be proved when updating a file")
|
||||||
|
}
|
||||||
|
|
||||||
// __ __ ___. .__ __
|
// __ __ ___. .__ __
|
||||||
// / \ / \ ____\_ |__ | |__ ____ ____ | | __
|
// / \ / \ ____\_ |__ | |__ ____ ____ | | __
|
||||||
// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /
|
// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/base"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/Unknwon/com"
|
"github.com/Unknwon/com"
|
||||||
|
@ -116,6 +117,7 @@ func PrepareTestEnv(t testing.TB) {
|
||||||
assert.NoError(t, removeAllWithRetry(setting.RepoRootPath))
|
assert.NoError(t, removeAllWithRetry(setting.RepoRootPath))
|
||||||
metaPath := filepath.Join(giteaRoot, "integrations", "gitea-repositories-meta")
|
metaPath := filepath.Join(giteaRoot, "integrations", "gitea-repositories-meta")
|
||||||
assert.NoError(t, com.CopyDir(metaPath, setting.RepoRootPath))
|
assert.NoError(t, com.CopyDir(metaPath, setting.RepoRootPath))
|
||||||
|
base.SetupGiteaRoot() // Makes sure GITEA_ROOT is set
|
||||||
}
|
}
|
||||||
|
|
||||||
type testCond struct {
|
type testCond struct {
|
||||||
|
|
|
@ -590,6 +590,7 @@ type DeleteRepoFileForm struct {
|
||||||
CommitMessage string
|
CommitMessage string
|
||||||
CommitChoice string `binding:"Required;MaxSize(50)"`
|
CommitChoice string `binding:"Required;MaxSize(50)"`
|
||||||
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
|
NewBranchName string `binding:"GitRefName;MaxSize(100)"`
|
||||||
|
LastCommit string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the fields
|
// Validate validates the fields
|
||||||
|
|
|
@ -16,7 +16,10 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -603,3 +606,25 @@ func EntryIcon(entry *git.TreeEntry) string {
|
||||||
|
|
||||||
return "file-text"
|
return "file-text"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value
|
||||||
|
func SetupGiteaRoot() string {
|
||||||
|
giteaRoot := os.Getenv("GITEA_ROOT")
|
||||||
|
if giteaRoot == "" {
|
||||||
|
_, filename, _, _ := runtime.Caller(0)
|
||||||
|
giteaRoot = strings.TrimSuffix(filename, "modules/base/tool.go")
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
rel, err := filepath.Rel(giteaRoot, wd)
|
||||||
|
if err != nil && strings.HasPrefix(filepath.ToSlash(rel), "../") {
|
||||||
|
giteaRoot = wd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(giteaRoot, "gitea")); os.IsNotExist(err) {
|
||||||
|
giteaRoot = ""
|
||||||
|
} else if err := os.Setenv("GITEA_ROOT", giteaRoot); err != nil {
|
||||||
|
giteaRoot = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return giteaRoot
|
||||||
|
}
|
||||||
|
|
|
@ -128,6 +128,21 @@ func (r *Repository) BranchNameSubURL() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileExists returns true if a file exists in the given repo branch
|
||||||
|
func (r *Repository) FileExists(path string, branch string) (bool, error) {
|
||||||
|
if branch == "" {
|
||||||
|
branch = r.Repository.DefaultBranch
|
||||||
|
}
|
||||||
|
commit, err := r.GitRepo.GetBranchCommit(branch)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if _, err := commit.GetTreeEntryByPath(path); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetEditorconfig returns the .editorconfig definition if found in the
|
// GetEditorconfig returns the .editorconfig definition if found in the
|
||||||
// HEAD of the default repo branch.
|
// HEAD of the default repo branch.
|
||||||
func (r *Repository) GetEditorconfig() (*editorconfig.Editorconfig, error) {
|
func (r *Repository) GetEditorconfig() (*editorconfig.Editorconfig, error) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -71,3 +72,32 @@ func (b *Blob) DataAsync() (io.ReadCloser, error) {
|
||||||
|
|
||||||
return cmdReadCloser{stdout: stdout, cmd: cmd}, nil
|
return cmdReadCloser{stdout: stdout, cmd: cmd}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBlobContentBase64 Reads the content of the blob with a base64 encode and returns the encoded string
|
||||||
|
func (b *Blob) GetBlobContentBase64() (string, error) {
|
||||||
|
dataRc, err := b.DataAsync()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer dataRc.Close()
|
||||||
|
|
||||||
|
pr, pw := io.Pipe()
|
||||||
|
encoder := base64.NewEncoder(base64.StdEncoding, pw)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_, err := io.Copy(encoder, dataRc)
|
||||||
|
encoder.Close()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
pw.CloseWithError(err)
|
||||||
|
} else {
|
||||||
|
pw.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
out, err := ioutil.ReadAll(pr)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(out), nil
|
||||||
|
}
|
||||||
|
|
|
@ -263,6 +263,11 @@ func (c *Commit) GetFilesChangedSinceCommit(pastCommit string) ([]string, error)
|
||||||
return c.repo.getFilesChanged(pastCommit, c.ID.String())
|
return c.repo.getFilesChanged(pastCommit, c.ID.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileChangedSinceCommit Returns true if the file given has changed since the the past commit
|
||||||
|
func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, error) {
|
||||||
|
return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String())
|
||||||
|
}
|
||||||
|
|
||||||
// GetSubModules get all the sub modules of current revision git tree
|
// GetSubModules get all the sub modules of current revision git tree
|
||||||
func (c *Commit) GetSubModules() (*ObjectCache, error) {
|
func (c *Commit) GetSubModules() (*ObjectCache, error) {
|
||||||
if c.submoduleCache != nil {
|
if c.submoduleCache != nil {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
version "github.com/mcuadros/go-version"
|
"github.com/mcuadros/go-version"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
|
// GetRefCommitID returns the last commit ID string of given reference (branch or tag).
|
||||||
|
@ -270,7 +270,7 @@ func (repo *Repository) searchCommits(id SHA1, opts SearchCommitsOptions) (*list
|
||||||
return repo.parsePrettyFormatLogToList(stdout)
|
return repo.parsePrettyFormatLogToList(stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *Repository) getFilesChanged(id1 string, id2 string) ([]string, error) {
|
func (repo *Repository) getFilesChanged(id1, id2 string) ([]string, error) {
|
||||||
stdout, err := NewCommand("diff", "--name-only", id1, id2).RunInDirBytes(repo.Path)
|
stdout, err := NewCommand("diff", "--name-only", id1, id2).RunInDirBytes(repo.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -278,6 +278,15 @@ func (repo *Repository) getFilesChanged(id1 string, id2 string) ([]string, error
|
||||||
return strings.Split(string(stdout), "\n"), nil
|
return strings.Split(string(stdout), "\n"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileChangedBetweenCommits Returns true if the file changed between commit IDs id1 and id2
|
||||||
|
func (repo *Repository) FileChangedBetweenCommits(filename, id1, id2 string) (bool, error) {
|
||||||
|
stdout, err := NewCommand("diff", "--name-only", "-z", id1, id2, "--", filename).RunInDirBytes(repo.Path)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return len(strings.TrimSpace(string(stdout))) > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
// FileCommitsCount return the number of files at a revison
|
// FileCommitsCount return the number of files at a revison
|
||||||
func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) {
|
func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) {
|
||||||
return commitsCount(repo.Path, revision, file)
|
return commitsCount(repo.Path, revision, file)
|
||||||
|
|
38
modules/repofiles/blob.go
Normal file
38
modules/repofiles/blob.go
Normal file
|
@ -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
|
||||||
|
}
|
38
modules/repofiles/blob_test.go
Normal file
38
modules/repofiles/blob_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
73
modules/repofiles/content.go
Normal file
73
modules/repofiles/content.go
Normal file
|
@ -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
|
||||||
|
}
|
90
modules/repofiles/content_test.go
Normal file
90
modules/repofiles/content_test.go
Normal file
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
209
modules/repofiles/delete.go
Normal file
209
modules/repofiles/delete.go
Normal file
|
@ -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
|
||||||
|
}
|
183
modules/repofiles/delete_test.go
Normal file
183
modules/repofiles/delete_test.go
Normal file
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package uploader
|
package repofiles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -12,11 +12,14 @@ import (
|
||||||
|
|
||||||
// GetDiffPreview produces and returns diff result of a file which is not yet committed.
|
// GetDiffPreview produces and returns diff result of a file which is not yet committed.
|
||||||
func GetDiffPreview(repo *models.Repository, branch, treePath, content string) (*models.Diff, error) {
|
func GetDiffPreview(repo *models.Repository, branch, treePath, content string) (*models.Diff, error) {
|
||||||
|
if branch == "" {
|
||||||
|
branch = repo.DefaultBranch
|
||||||
|
}
|
||||||
t, err := NewTemporaryUploadRepository(repo)
|
t, err := NewTemporaryUploadRepository(repo)
|
||||||
defer t.Close()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
defer t.Close()
|
||||||
if err := t.Clone(branch); err != nil {
|
if err := t.Clone(branch); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
143
modules/repofiles/diff_test.go
Normal file
143
modules/repofiles/diff_test.go
Normal file
|
@ -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: ]")
|
||||||
|
})
|
||||||
|
}
|
125
modules/repofiles/file.go
Normal file
125
modules/repofiles/file.go
Normal file
|
@ -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
|
||||||
|
}
|
90
modules/repofiles/file_test.go
Normal file
90
modules/repofiles/file_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
23
modules/repofiles/repofiles.go
Normal file
23
modules/repofiles/repofiles.go
Normal file
|
@ -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
|
||||||
|
}
|
27
modules/repofiles/repofiles_test.go
Normal file
27
modules/repofiles/repofiles_test.go
Normal file
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package uploader
|
package repofiles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -12,19 +12,22 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/process"
|
"code.gitea.io/gitea/modules/process"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/Unknwon/com"
|
"github.com/Unknwon/com"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TemporaryUploadRepository is a type to wrap our upload repositories
|
// TemporaryUploadRepository is a type to wrap our upload repositories as a shallow clone
|
||||||
type TemporaryUploadRepository struct {
|
type TemporaryUploadRepository struct {
|
||||||
repo *models.Repository
|
repo *models.Repository
|
||||||
|
gitRepo *git.Repository
|
||||||
basePath string
|
basePath string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +36,10 @@ func NewTemporaryUploadRepository(repo *models.Repository) (*TemporaryUploadRepo
|
||||||
timeStr := com.ToStr(time.Now().Nanosecond()) // SHOULD USE SOMETHING UNIQUE
|
timeStr := com.ToStr(time.Now().Nanosecond()) // SHOULD USE SOMETHING UNIQUE
|
||||||
basePath := path.Join(models.LocalCopyPath(), "upload-"+timeStr+".git")
|
basePath := path.Join(models.LocalCopyPath(), "upload-"+timeStr+".git")
|
||||||
if err := os.MkdirAll(path.Dir(basePath), os.ModePerm); err != nil {
|
if err := os.MkdirAll(path.Dir(basePath), os.ModePerm); err != nil {
|
||||||
return nil, fmt.Errorf("Failed to create dir %s: %v", basePath, err)
|
return nil, fmt.Errorf("failed to create dir %s: %v", basePath, err)
|
||||||
|
}
|
||||||
|
if repo.RepoPath() == "" {
|
||||||
|
return nil, fmt.Errorf("no path to repository on system")
|
||||||
}
|
}
|
||||||
t := &TemporaryUploadRepository{repo: repo, basePath: basePath}
|
t := &TemporaryUploadRepository{repo: repo, basePath: basePath}
|
||||||
return t, nil
|
return t, nil
|
||||||
|
@ -51,8 +57,26 @@ func (t *TemporaryUploadRepository) Clone(branch string) error {
|
||||||
if _, stderr, err := process.GetManager().ExecTimeout(5*time.Minute,
|
if _, stderr, err := process.GetManager().ExecTimeout(5*time.Minute,
|
||||||
fmt.Sprintf("Clone (git clone -s --bare): %s", t.basePath),
|
fmt.Sprintf("Clone (git clone -s --bare): %s", t.basePath),
|
||||||
"git", "clone", "-s", "--bare", "-b", branch, t.repo.RepoPath(), t.basePath); err != nil {
|
"git", "clone", "-s", "--bare", "-b", branch, t.repo.RepoPath(), t.basePath); err != nil {
|
||||||
|
if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched {
|
||||||
|
return models.ErrBranchNotExist{
|
||||||
|
Name: branch,
|
||||||
|
}
|
||||||
|
} else if matched, _ := regexp.MatchString(".* repository .* does not exist.*", stderr); matched {
|
||||||
|
return models.ErrRepoNotExist{
|
||||||
|
ID: t.repo.ID,
|
||||||
|
UID: t.repo.OwnerID,
|
||||||
|
OwnerName: t.repo.OwnerName,
|
||||||
|
Name: t.repo.Name,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
return fmt.Errorf("Clone: %v %s", err, stderr)
|
return fmt.Errorf("Clone: %v %s", err, stderr)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
gitRepo, err := git.OpenRepository(t.basePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
t.gitRepo = gitRepo
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,6 +210,12 @@ func (t *TemporaryUploadRepository) AddObjectToIndex(mode, objectHash, objectPat
|
||||||
t.basePath,
|
t.basePath,
|
||||||
fmt.Sprintf("addObjectToIndex (git update-index): %s", t.basePath),
|
fmt.Sprintf("addObjectToIndex (git update-index): %s", t.basePath),
|
||||||
"git", "update-index", "--add", "--replace", "--cacheinfo", mode, objectHash, objectPath); err != nil {
|
"git", "update-index", "--add", "--replace", "--cacheinfo", mode, objectHash, objectPath); err != nil {
|
||||||
|
if matched, _ := regexp.MatchString(".*Invalid path '.*", stderr); matched {
|
||||||
|
return models.ErrFilePathInvalid{
|
||||||
|
Message: objectPath,
|
||||||
|
Path: objectPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
return fmt.Errorf("git update-index: %s", stderr)
|
return fmt.Errorf("git update-index: %s", stderr)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -201,22 +231,42 @@ func (t *TemporaryUploadRepository) WriteTree() (string, error) {
|
||||||
return "", fmt.Errorf("git write-tree: %s", stderr)
|
return "", fmt.Errorf("git write-tree: %s", stderr)
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(treeHash), nil
|
return strings.TrimSpace(treeHash), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastCommit gets the last commit ID SHA of the repo
|
||||||
|
func (t *TemporaryUploadRepository) GetLastCommit() (string, error) {
|
||||||
|
return t.GetLastCommitByRef("HEAD")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLastCommitByRef gets the last commit ID SHA of the repo by ref
|
||||||
|
func (t *TemporaryUploadRepository) GetLastCommitByRef(ref string) (string, error) {
|
||||||
|
if ref == "" {
|
||||||
|
ref = "HEAD"
|
||||||
|
}
|
||||||
|
treeHash, stderr, err := process.GetManager().ExecDir(5*time.Minute,
|
||||||
|
t.basePath,
|
||||||
|
fmt.Sprintf("GetLastCommit (git rev-parse %s): %s", ref, t.basePath),
|
||||||
|
"git", "rev-parse", ref)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("git rev-parse %s: %s", ref, stderr)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(treeHash), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommitTree creates a commit from a given tree for the user with provided message
|
// CommitTree creates a commit from a given tree for the user with provided message
|
||||||
func (t *TemporaryUploadRepository) CommitTree(doer *models.User, treeHash string, message string) (string, error) {
|
func (t *TemporaryUploadRepository) CommitTree(author, committer *models.User, treeHash string, message string) (string, error) {
|
||||||
commitTimeStr := time.Now().Format(time.UnixDate)
|
commitTimeStr := time.Now().Format(time.UnixDate)
|
||||||
sig := doer.NewGitSig()
|
authorSig := author.NewGitSig()
|
||||||
|
committerSig := committer.NewGitSig()
|
||||||
|
|
||||||
// FIXME: Should we add SSH_ORIGINAL_COMMAND to this
|
// FIXME: Should we add SSH_ORIGINAL_COMMAND to this
|
||||||
// Because this may call hooks we should pass in the environment
|
// Because this may call hooks we should pass in the environment
|
||||||
env := append(os.Environ(),
|
env := append(os.Environ(),
|
||||||
"GIT_AUTHOR_NAME="+sig.Name,
|
"GIT_AUTHOR_NAME="+authorSig.Name,
|
||||||
"GIT_AUTHOR_EMAIL="+sig.Email,
|
"GIT_AUTHOR_EMAIL="+authorSig.Email,
|
||||||
"GIT_AUTHOR_DATE="+commitTimeStr,
|
"GIT_AUTHOR_DATE="+commitTimeStr,
|
||||||
"GIT_COMMITTER_NAME="+sig.Name,
|
"GIT_COMMITTER_NAME="+committerSig.Name,
|
||||||
"GIT_COMMITTER_EMAIL="+sig.Email,
|
"GIT_COMMITTER_EMAIL="+committerSig.Email,
|
||||||
"GIT_COMMITTER_DATE="+commitTimeStr,
|
"GIT_COMMITTER_DATE="+commitTimeStr,
|
||||||
)
|
)
|
||||||
commitHash, stderr, err := process.GetManager().ExecDirEnv(5*time.Minute,
|
commitHash, stderr, err := process.GetManager().ExecDirEnv(5*time.Minute,
|
||||||
|
@ -357,3 +407,19 @@ func (t *TemporaryUploadRepository) CheckAttribute(attribute string, args ...str
|
||||||
|
|
||||||
return name2attribute2info, err
|
return name2attribute2info, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetBranchCommit Gets the commit object of the given branch
|
||||||
|
func (t *TemporaryUploadRepository) GetBranchCommit(branch string) (*git.Commit, error) {
|
||||||
|
if t.gitRepo == nil {
|
||||||
|
return nil, fmt.Errorf("repository has not been cloned")
|
||||||
|
}
|
||||||
|
return t.gitRepo.GetBranchCommit(branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCommit Gets the commit object of the given commit ID
|
||||||
|
func (t *TemporaryUploadRepository) GetCommit(commitID string) (*git.Commit, error) {
|
||||||
|
if t.gitRepo == nil {
|
||||||
|
return nil, fmt.Errorf("repository has not been cloned")
|
||||||
|
}
|
||||||
|
return t.gitRepo.GetCommit(commitID)
|
||||||
|
}
|
92
modules/repofiles/tree.go
Normal file
92
modules/repofiles/tree.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -2,34 +2,37 @@
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package repo
|
package repofiles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/context"
|
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
"code.gitea.io/sdk/gitea"
|
api "code.gitea.io/sdk/gitea"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetTreeBySHA(t *testing.T) {
|
func TestGetTreeBySHA(t *testing.T) {
|
||||||
models.PrepareTestEnv(t)
|
models.PrepareTestEnv(t)
|
||||||
sha := "master"
|
|
||||||
ctx := test.MockContext(t, "user2/repo1")
|
ctx := test.MockContext(t, "user2/repo1")
|
||||||
ctx.SetParams(":id", "1")
|
|
||||||
ctx.SetParams(":sha", sha)
|
|
||||||
test.LoadRepo(t, ctx, 1)
|
test.LoadRepo(t, ctx, 1)
|
||||||
test.LoadRepoCommit(t, ctx)
|
test.LoadRepoCommit(t, ctx)
|
||||||
test.LoadUser(t, ctx, 2)
|
test.LoadUser(t, ctx, 2)
|
||||||
test.LoadGitRepo(t, ctx)
|
test.LoadGitRepo(t, ctx)
|
||||||
|
sha := ctx.Repo.Repository.DefaultBranch
|
||||||
|
page := 1
|
||||||
|
perPage := 10
|
||||||
|
ctx.SetParams(":id", "1")
|
||||||
|
ctx.SetParams(":sha", sha)
|
||||||
|
|
||||||
tree := GetTreeBySHA(&context.APIContext{Context: ctx, Org: nil}, ctx.Params("sha"))
|
tree, err := GetTreeBySHA(ctx.Repo.Repository, ctx.Params(":sha"), page, perPage, true)
|
||||||
expectedTree := &gitea.GitTreeResponse{
|
assert.Nil(t, err)
|
||||||
|
expectedTree := &api.GitTreeResponse{
|
||||||
SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
|
SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
|
||||||
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/65f1bf27bc3bf70f64657658635e66094edbcb4d",
|
URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/65f1bf27bc3bf70f64657658635e66094edbcb4d",
|
||||||
Entries: []gitea.GitEntry{
|
Entries: []api.GitEntry{
|
||||||
{
|
{
|
||||||
Path: "README.md",
|
Path: "README.md",
|
||||||
Mode: "100644",
|
Mode: "100644",
|
||||||
|
@ -43,6 +46,5 @@ func TestGetTreeBySHA(t *testing.T) {
|
||||||
Page: 1,
|
Page: 1,
|
||||||
TotalCount: 1,
|
TotalCount: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.EqualValues(t, tree, expectedTree)
|
assert.EqualValues(t, tree, expectedTree)
|
||||||
}
|
}
|
331
modules/repofiles/update.go
Normal file
331
modules/repofiles/update.go
Normal file
|
@ -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
|
||||||
|
}
|
357
modules/repofiles/update_test.go
Normal file
357
modules/repofiles/update_test.go
Normal file
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
|
@ -2,7 +2,7 @@
|
||||||
// Use of this source code is governed by a MIT-style
|
// Use of this source code is governed by a MIT-style
|
||||||
// license that can be found in the LICENSE file.
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
package uploader
|
package repofiles
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -127,8 +127,12 @@ func UploadRepoFiles(repo *models.Repository, doer *models.User, opts *UploadRep
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// make author and committer the doer
|
||||||
|
author := doer
|
||||||
|
committer := doer
|
||||||
|
|
||||||
// Now commit the tree
|
// Now commit the tree
|
||||||
commitHash, err := t.CommitTree(doer, treeHash, opts.Message)
|
commitHash, err := t.CommitTree(author, committer, treeHash, opts.Message)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
29
modules/repofiles/verification.go
Normal file
29
modules/repofiles/verification.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -293,11 +293,13 @@ var (
|
||||||
MaxResponseItems int
|
MaxResponseItems int
|
||||||
DefaultPagingNum int
|
DefaultPagingNum int
|
||||||
DefaultGitTreesPerPage int
|
DefaultGitTreesPerPage int
|
||||||
|
DefaultMaxBlobSize int64
|
||||||
}{
|
}{
|
||||||
EnableSwagger: true,
|
EnableSwagger: true,
|
||||||
MaxResponseItems: 50,
|
MaxResponseItems: 50,
|
||||||
DefaultPagingNum: 30,
|
DefaultPagingNum: 30,
|
||||||
DefaultGitTreesPerPage: 1000,
|
DefaultGitTreesPerPage: 1000,
|
||||||
|
DefaultMaxBlobSize: 10485760,
|
||||||
}
|
}
|
||||||
|
|
||||||
OAuth2 = struct {
|
OAuth2 = struct {
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -667,7 +667,7 @@ editor.filename_help = Add a directory by typing its name followed by a slash ('
|
||||||
editor.or = or
|
editor.or = or
|
||||||
editor.cancel_lower = Cancel
|
editor.cancel_lower = Cancel
|
||||||
editor.commit_changes = Commit Changes
|
editor.commit_changes = Commit Changes
|
||||||
editor.add_tmpl = Add '%s/<filename>'
|
editor.add_tmpl = Add '<filename>'
|
||||||
editor.add = Add '%s'
|
editor.add = Add '%s'
|
||||||
editor.update = Update '%s'
|
editor.update = Update '%s'
|
||||||
editor.delete = Delete '%s'
|
editor.delete = Delete '%s'
|
||||||
|
@ -677,11 +677,14 @@ editor.create_new_branch = Create a <strong>new branch</strong> for this commit
|
||||||
editor.new_branch_name_desc = New branch name…
|
editor.new_branch_name_desc = New branch name…
|
||||||
editor.cancel = Cancel
|
editor.cancel = Cancel
|
||||||
editor.filename_cannot_be_empty = The filename cannot be empty.
|
editor.filename_cannot_be_empty = The filename cannot be empty.
|
||||||
|
editor.filename_is_invalid = The filename is invalid: '%s'.
|
||||||
|
editor.branch_does_not_exist = Branch '%s' does not exist in this repository.
|
||||||
editor.branch_already_exists = Branch '%s' already exists in this repository.
|
editor.branch_already_exists = Branch '%s' already exists in this repository.
|
||||||
editor.directory_is_a_file = Directory name '%s' is already used as a filename in this repository.
|
editor.directory_is_a_file = Directory name '%s' is already used as a filename in this repository.
|
||||||
editor.file_is_a_symlink = '%s' is a symbolic link. Symbolic links cannot be edited in the web editor
|
editor.file_is_a_symlink = '%s' is a symbolic link. Symbolic links cannot be edited in the web editor
|
||||||
editor.filename_is_a_directory = Filename '%s' is already used as a directory name in this repository.
|
editor.filename_is_a_directory = Filename '%s' is already used as a directory name in this repository.
|
||||||
editor.file_editing_no_longer_exists = The file being edited, '%s', no longer exists in this repository.
|
editor.file_editing_no_longer_exists = The file being edited, '%s', no longer exists in this repository.
|
||||||
|
editor.file_deleting_no_longer_exists = The file being deleted, '%s', no longer exists in this repository.
|
||||||
editor.file_changed_while_editing = The file contents have changed since you started editing. <a target="_blank" rel="noopener noreferrer" href="%s">Click here</a> to see them or <strong>Commit Changes again</strong> to overwrite them.
|
editor.file_changed_while_editing = The file contents have changed since you started editing. <a target="_blank" rel="noopener noreferrer" href="%s">Click here</a> to see them or <strong>Commit Changes again</strong> to overwrite them.
|
||||||
editor.file_already_exists = A file named '%s' already exists in this repository.
|
editor.file_already_exists = A file named '%s' already exists in this repository.
|
||||||
editor.no_changes_to_show = There are no changes to show.
|
editor.no_changes_to_show = There are no changes to show.
|
||||||
|
|
|
@ -659,7 +659,16 @@ func RegisterRoutes(m *macaron.Macaron) {
|
||||||
})
|
})
|
||||||
m.Get("/refs", repo.GetGitAllRefs)
|
m.Get("/refs", repo.GetGitAllRefs)
|
||||||
m.Get("/refs/*", repo.GetGitRefs)
|
m.Get("/refs/*", repo.GetGitRefs)
|
||||||
m.Combo("/trees/:sha", context.RepoRef()).Get(repo.GetTree)
|
m.Get("/trees/:sha", context.RepoRef(), repo.GetTree)
|
||||||
|
m.Get("/blobs/:sha", context.RepoRef(), repo.GetBlob)
|
||||||
|
}, reqRepoReader(models.UnitTypeCode))
|
||||||
|
m.Group("/contents", func() {
|
||||||
|
m.Get("/*", repo.GetFileContents)
|
||||||
|
m.Group("/*", func() {
|
||||||
|
m.Post("", bind(api.CreateFileOptions{}), repo.CreateFile)
|
||||||
|
m.Put("", bind(api.UpdateFileOptions{}), repo.UpdateFile)
|
||||||
|
m.Delete("", bind(api.DeleteFileOptions{}), repo.DeleteFile)
|
||||||
|
}, reqRepoWriter(models.UnitTypeCode), reqToken())
|
||||||
}, reqRepoReader(models.UnitTypeCode))
|
}, reqRepoReader(models.UnitTypeCode))
|
||||||
}, repoAssignment())
|
}, repoAssignment())
|
||||||
})
|
})
|
||||||
|
|
51
routers/api/v1/repo/blob.go
Normal file
51
routers/api/v1/repo/blob.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,10 +6,15 @@
|
||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/repofiles"
|
||||||
"code.gitea.io/gitea/routers/repo"
|
"code.gitea.io/gitea/routers/repo"
|
||||||
|
api "code.gitea.io/sdk/gitea"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetRawFile get a file by path on a repository
|
// GetRawFile get a file by path on a repository
|
||||||
|
@ -48,12 +53,12 @@ func GetRawFile(ctx *context.APIContext) {
|
||||||
if git.IsErrNotExist(err) {
|
if git.IsErrNotExist(err) {
|
||||||
ctx.NotFound()
|
ctx.NotFound()
|
||||||
} else {
|
} else {
|
||||||
ctx.Error(500, "GetBlobByPath", err)
|
ctx.Error(http.StatusInternalServerError, "GetBlobByPath", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = repo.ServeBlob(ctx.Context, blob); err != nil {
|
if err = repo.ServeBlob(ctx.Context, blob); err != nil {
|
||||||
ctx.Error(500, "ServeBlob", err)
|
ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +91,7 @@ func GetArchive(ctx *context.APIContext) {
|
||||||
repoPath := models.RepoPath(ctx.Params(":username"), ctx.Params(":reponame"))
|
repoPath := models.RepoPath(ctx.Params(":username"), ctx.Params(":reponame"))
|
||||||
gitRepo, err := git.OpenRepository(repoPath)
|
gitRepo, err := git.OpenRepository(repoPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(500, "OpenRepository", err)
|
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Repo.GitRepo = gitRepo
|
ctx.Repo.GitRepo = gitRepo
|
||||||
|
@ -125,7 +130,7 @@ func GetEditorconfig(ctx *context.APIContext) {
|
||||||
if git.IsErrNotExist(err) {
|
if git.IsErrNotExist(err) {
|
||||||
ctx.NotFound(err)
|
ctx.NotFound(err)
|
||||||
} else {
|
} else {
|
||||||
ctx.Error(500, "GetEditorconfig", err)
|
ctx.Error(http.StatusInternalServerError, "GetEditorconfig", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -136,5 +141,264 @@ func GetEditorconfig(ctx *context.APIContext) {
|
||||||
ctx.NotFound(err)
|
ctx.NotFound(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.JSON(200, def)
|
ctx.JSON(http.StatusOK, def)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanWriteFiles returns true if repository is editable and user has proper access level.
|
||||||
|
func CanWriteFiles(r *context.Repository) bool {
|
||||||
|
return r.Permission.CanWrite(models.UnitTypeCode) && !r.Repository.IsMirror && !r.Repository.IsArchived
|
||||||
|
}
|
||||||
|
|
||||||
|
// CanReadFiles returns true if repository is readable and user has proper access level.
|
||||||
|
func CanReadFiles(r *context.Repository) bool {
|
||||||
|
return r.Permission.CanRead(models.UnitTypeCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateFile handles API call for creating a file
|
||||||
|
func CreateFile(ctx *context.APIContext, apiOpts api.CreateFileOptions) {
|
||||||
|
// swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
|
||||||
|
// ---
|
||||||
|
// summary: Create a file in a repository
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// 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: filepath
|
||||||
|
// in: path
|
||||||
|
// description: path of the file to create
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// description: "'content' must be base64 encoded\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'sha' is the SHA for the file that already exists\n\n 'new_branch' (optional) will make a new branch from 'branch' before creating the file"
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/CreateFileOptions"
|
||||||
|
// responses:
|
||||||
|
// "201":
|
||||||
|
// "$ref": "#/responses/FileResponse"
|
||||||
|
|
||||||
|
opts := &repofiles.UpdateRepoFileOptions{
|
||||||
|
Content: apiOpts.Content,
|
||||||
|
IsNewFile: true,
|
||||||
|
Message: apiOpts.Message,
|
||||||
|
TreePath: ctx.Params("*"),
|
||||||
|
OldBranch: apiOpts.BranchName,
|
||||||
|
NewBranch: apiOpts.NewBranchName,
|
||||||
|
Committer: &repofiles.IdentityOptions{
|
||||||
|
Name: apiOpts.Committer.Name,
|
||||||
|
Email: apiOpts.Committer.Email,
|
||||||
|
},
|
||||||
|
Author: &repofiles.IdentityOptions{
|
||||||
|
Name: apiOpts.Author.Name,
|
||||||
|
Email: apiOpts.Author.Email,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "CreateFile", err)
|
||||||
|
} else {
|
||||||
|
ctx.JSON(http.StatusCreated, fileResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateFile handles API call for updating a file
|
||||||
|
func UpdateFile(ctx *context.APIContext, apiOpts api.UpdateFileOptions) {
|
||||||
|
// swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
|
||||||
|
// ---
|
||||||
|
// summary: Update a file in a repository
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// 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: filepath
|
||||||
|
// in: path
|
||||||
|
// description: path of the file to update
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// description: "'content' must be base64 encoded\n\n 'sha' is the SHA for the file that already exists\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'new_branch' (optional) will make a new branch from 'branch' before updating the file"
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/UpdateFileOptions"
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/FileResponse"
|
||||||
|
|
||||||
|
opts := &repofiles.UpdateRepoFileOptions{
|
||||||
|
Content: apiOpts.Content,
|
||||||
|
SHA: apiOpts.SHA,
|
||||||
|
IsNewFile: false,
|
||||||
|
Message: apiOpts.Message,
|
||||||
|
FromTreePath: apiOpts.FromPath,
|
||||||
|
TreePath: ctx.Params("*"),
|
||||||
|
OldBranch: apiOpts.BranchName,
|
||||||
|
NewBranch: apiOpts.NewBranchName,
|
||||||
|
Committer: &repofiles.IdentityOptions{
|
||||||
|
Name: apiOpts.Committer.Name,
|
||||||
|
Email: apiOpts.Committer.Email,
|
||||||
|
},
|
||||||
|
Author: &repofiles.IdentityOptions{
|
||||||
|
Name: apiOpts.Author.Name,
|
||||||
|
Email: apiOpts.Author.Email,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "UpdateFile", err)
|
||||||
|
} else {
|
||||||
|
ctx.JSON(http.StatusOK, fileResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called from both CreateFile or UpdateFile to handle both
|
||||||
|
func createOrUpdateFile(ctx *context.APIContext, opts *repofiles.UpdateRepoFileOptions) (*api.FileResponse, error) {
|
||||||
|
if !CanWriteFiles(ctx.Repo) {
|
||||||
|
return nil, models.ErrUserDoesNotHaveAccessToRepo{
|
||||||
|
UserID: ctx.User.ID,
|
||||||
|
RepoName: ctx.Repo.Repository.LowerName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := base64.StdEncoding.DecodeString(opts.Content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
opts.Content = string(content)
|
||||||
|
|
||||||
|
return repofiles.CreateOrUpdateRepoFile(ctx.Repo.Repository, ctx.User, opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteFile Delete a fle in a repository
|
||||||
|
func DeleteFile(ctx *context.APIContext, apiOpts api.DeleteFileOptions) {
|
||||||
|
// swagger:operation DELETE /repos/{owner}/{repo}/contents/{filepath} repository repoDeleteFile
|
||||||
|
// ---
|
||||||
|
// summary: Delete a file in a repository
|
||||||
|
// consumes:
|
||||||
|
// - application/json
|
||||||
|
// 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: filepath
|
||||||
|
// in: path
|
||||||
|
// description: path of the file to delete
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: body
|
||||||
|
// in: body
|
||||||
|
// description: "'sha' is the SHA for the file to be deleted\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'new_branch' (optional) will make a new branch from 'branch' before deleting the file"
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/DeleteFileOptions"
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/FileDeleteResponse"
|
||||||
|
if !CanWriteFiles(ctx.Repo) {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "DeleteFile", models.ErrUserDoesNotHaveAccessToRepo{
|
||||||
|
UserID: ctx.User.ID,
|
||||||
|
RepoName: ctx.Repo.Repository.LowerName,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := &repofiles.DeleteRepoFileOptions{
|
||||||
|
Message: apiOpts.Message,
|
||||||
|
OldBranch: apiOpts.BranchName,
|
||||||
|
NewBranch: apiOpts.NewBranchName,
|
||||||
|
SHA: apiOpts.SHA,
|
||||||
|
TreePath: ctx.Params("*"),
|
||||||
|
Committer: &repofiles.IdentityOptions{
|
||||||
|
Name: apiOpts.Committer.Name,
|
||||||
|
Email: apiOpts.Committer.Email,
|
||||||
|
},
|
||||||
|
Author: &repofiles.IdentityOptions{
|
||||||
|
Name: apiOpts.Author.Name,
|
||||||
|
Email: apiOpts.Author.Email,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileResponse, err := repofiles.DeleteRepoFile(ctx.Repo.Repository, ctx.User, opts); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
|
||||||
|
} else {
|
||||||
|
ctx.JSON(http.StatusOK, fileResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFileContents Get the contents of a fle in a repository
|
||||||
|
func GetFileContents(ctx *context.APIContext) {
|
||||||
|
// swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetFileContents
|
||||||
|
// ---
|
||||||
|
// summary: Gets the contents of a file or directory in 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: filepath
|
||||||
|
// in: path
|
||||||
|
// description: path of the file to delete
|
||||||
|
// type: string
|
||||||
|
// required: true
|
||||||
|
// - name: ref
|
||||||
|
// in: query
|
||||||
|
// description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
|
||||||
|
// required: false
|
||||||
|
// type: string
|
||||||
|
// responses:
|
||||||
|
// "200":
|
||||||
|
// "$ref": "#/responses/FileContentResponse"
|
||||||
|
|
||||||
|
if !CanReadFiles(ctx.Repo) {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "GetFileContents", models.ErrUserDoesNotHaveAccessToRepo{
|
||||||
|
UserID: ctx.User.ID,
|
||||||
|
RepoName: ctx.Repo.Repository.LowerName,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
treePath := ctx.Params("*")
|
||||||
|
ref := ctx.QueryTrim("ref")
|
||||||
|
|
||||||
|
if fileContents, err := repofiles.GetFileContents(ctx.Repo.Repository, treePath, ref); err != nil {
|
||||||
|
ctx.Error(http.StatusInternalServerError, "GetFileContents", err)
|
||||||
|
} else {
|
||||||
|
ctx.JSON(http.StatusOK, fileContents)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,8 @@
|
||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/repofiles"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
|
||||||
"code.gitea.io/sdk/gitea"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetTree get the tree of a repository.
|
// GetTree get the tree of a repository.
|
||||||
|
@ -55,92 +50,15 @@ func GetTree(ctx *context.APIContext) {
|
||||||
// responses:
|
// responses:
|
||||||
// "200":
|
// "200":
|
||||||
// "$ref": "#/responses/GitTreeResponse"
|
// "$ref": "#/responses/GitTreeResponse"
|
||||||
sha := ctx.Params("sha")
|
|
||||||
|
sha := ctx.Params(":sha")
|
||||||
if len(sha) == 0 {
|
if len(sha) == 0 {
|
||||||
ctx.Error(400, "", "sha not provided")
|
ctx.Error(400, "", "sha not provided")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tree := GetTreeBySHA(ctx, sha)
|
if tree, err := repofiles.GetTreeBySHA(ctx.Repo.Repository, sha, ctx.QueryInt("page"), ctx.QueryInt("per_page"), ctx.QueryBool("recursive")); err != nil {
|
||||||
if tree != nil {
|
ctx.Error(400, "", err.Error())
|
||||||
|
} else {
|
||||||
ctx.JSON(200, tree)
|
ctx.JSON(200, tree)
|
||||||
} else {
|
|
||||||
ctx.Error(400, "", "sha invalid")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTreeBySHA get the GitTreeResponse of a repository using a sha hash.
|
|
||||||
func GetTreeBySHA(ctx *context.APIContext, sha string) *gitea.GitTreeResponse {
|
|
||||||
gitTree, err := ctx.Repo.GitRepo.GetTree(sha)
|
|
||||||
if err != nil || gitTree == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
tree := new(gitea.GitTreeResponse)
|
|
||||||
repoID := strings.TrimRight(setting.AppURL, "/") + "/api/v1/repos/" + ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
|
|
||||||
tree.SHA = gitTree.ID.String()
|
|
||||||
tree.URL = repoID + "/git/trees/" + tree.SHA
|
|
||||||
var entries git.Entries
|
|
||||||
if ctx.QueryBool("recursive") {
|
|
||||||
entries, err = gitTree.ListEntriesRecursive()
|
|
||||||
} else {
|
|
||||||
entries, err = gitTree.ListEntries()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return tree
|
|
||||||
}
|
|
||||||
repoIDLen := len(repoID)
|
|
||||||
|
|
||||||
// 51 is len(sha1) + len("/git/blobs/"). 40 + 11.
|
|
||||||
blobURL := make([]byte, repoIDLen+51)
|
|
||||||
copy(blobURL[:], repoID)
|
|
||||||
copy(blobURL[repoIDLen:], "/git/blobs/")
|
|
||||||
|
|
||||||
// 51 is len(sha1) + len("/git/trees/"). 40 + 11.
|
|
||||||
treeURL := make([]byte, repoIDLen+51)
|
|
||||||
copy(treeURL[:], repoID)
|
|
||||||
copy(treeURL[repoIDLen:], "/git/trees/")
|
|
||||||
|
|
||||||
// 40 is the size of the sha1 hash in hexadecimal format.
|
|
||||||
copyPos := len(treeURL) - 40
|
|
||||||
|
|
||||||
page := ctx.QueryInt("page")
|
|
||||||
perPage := ctx.QueryInt("per_page")
|
|
||||||
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
|
|
||||||
}
|
|
||||||
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([]gitea.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
|
|
||||||
}
|
|
||||||
|
|
|
@ -97,6 +97,7 @@ type swaggerParameterBodies struct {
|
||||||
|
|
||||||
// in:body
|
// in:body
|
||||||
CreateUserOption api.CreateUserOption
|
CreateUserOption api.CreateUserOption
|
||||||
|
|
||||||
// in:body
|
// in:body
|
||||||
EditUserOption api.EditUserOption
|
EditUserOption api.EditUserOption
|
||||||
|
|
||||||
|
@ -105,4 +106,13 @@ type swaggerParameterBodies struct {
|
||||||
|
|
||||||
// in:body
|
// in:body
|
||||||
EditAttachmentOptions api.EditAttachmentOptions
|
EditAttachmentOptions api.EditAttachmentOptions
|
||||||
|
|
||||||
|
// in:body
|
||||||
|
CreateFileOptions api.CreateFileOptions
|
||||||
|
|
||||||
|
// in:body
|
||||||
|
UpdateFileOptions api.UpdateFileOptions
|
||||||
|
|
||||||
|
// in:body
|
||||||
|
DeleteFileOptions api.DeleteFileOptions
|
||||||
}
|
}
|
||||||
|
|
|
@ -162,9 +162,37 @@ type swaggerGitTreeResponse struct {
|
||||||
Body api.GitTreeResponse `json:"body"`
|
Body api.GitTreeResponse `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GitBlobResponse
|
||||||
|
// swagger:response GitBlobResponse
|
||||||
|
type swaggerGitBlobResponse struct {
|
||||||
|
//in: body
|
||||||
|
Body api.GitBlobResponse `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
// Commit
|
// Commit
|
||||||
// swagger:response Commit
|
// swagger:response Commit
|
||||||
type swaggerCommit struct {
|
type swaggerCommit struct {
|
||||||
//in: body
|
//in: body
|
||||||
Body api.Commit `json:"body"`
|
Body api.Commit `json:"body"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FileResponse
|
||||||
|
// swagger:response FileResponse
|
||||||
|
type swaggerFileResponse struct {
|
||||||
|
//in: body
|
||||||
|
Body api.FileResponse `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileContentResponse
|
||||||
|
// swagger:response FileContentResponse
|
||||||
|
type swaggerFileContentResponse struct {
|
||||||
|
//in: body
|
||||||
|
Body api.FileContentResponse `json:"body"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileDeleteResponse
|
||||||
|
// swagger:response FileDeleteResponse
|
||||||
|
type swaggerFileDeleteResponse struct {
|
||||||
|
//in: body
|
||||||
|
Body api.FileDeleteResponse `json:"body"`
|
||||||
|
}
|
||||||
|
|
|
@ -17,9 +17,9 @@ import (
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/repofiles"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/templates"
|
"code.gitea.io/gitea/modules/templates"
|
||||||
"code.gitea.io/gitea/modules/uploader"
|
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ func editFile(ctx *context.Context, isNewFile bool) {
|
||||||
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
|
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
|
||||||
}
|
}
|
||||||
ctx.Data["new_branch_name"] = ""
|
ctx.Data["new_branch_name"] = ""
|
||||||
ctx.Data["last_commit"] = ctx.Repo.Commit.ID
|
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
||||||
ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
|
ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
|
||||||
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
|
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
|
||||||
ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
|
ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
|
||||||
|
@ -159,39 +159,27 @@ func NewFile(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bool) {
|
func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bool) {
|
||||||
ctx.Data["PageIsEdit"] = true
|
|
||||||
ctx.Data["IsNewFile"] = isNewFile
|
|
||||||
ctx.Data["RequireHighlightJS"] = true
|
|
||||||
ctx.Data["RequireSimpleMDE"] = true
|
|
||||||
canCommit := renderCommitRights(ctx)
|
canCommit := renderCommitRights(ctx)
|
||||||
|
treeNames, treePaths := getParentTreeFields(form.TreePath)
|
||||||
oldBranchName := ctx.Repo.BranchName
|
branchName := ctx.Repo.BranchName
|
||||||
branchName := oldBranchName
|
|
||||||
oldTreePath := cleanUploadFileName(ctx.Repo.TreePath)
|
|
||||||
lastCommit := form.LastCommit
|
|
||||||
form.LastCommit = ctx.Repo.Commit.ID.String()
|
|
||||||
|
|
||||||
if form.CommitChoice == frmCommitChoiceNewBranch {
|
if form.CommitChoice == frmCommitChoiceNewBranch {
|
||||||
branchName = form.NewBranchName
|
branchName = form.NewBranchName
|
||||||
}
|
}
|
||||||
|
|
||||||
form.TreePath = cleanUploadFileName(form.TreePath)
|
ctx.Data["PageIsEdit"] = true
|
||||||
if len(form.TreePath) == 0 {
|
ctx.Data["IsNewFile"] = isNewFile
|
||||||
ctx.Error(500, "Upload file name is invalid")
|
ctx.Data["RequireHighlightJS"] = true
|
||||||
return
|
ctx.Data["RequireSimpleMDE"] = true
|
||||||
}
|
|
||||||
treeNames, treePaths := getParentTreeFields(form.TreePath)
|
|
||||||
|
|
||||||
ctx.Data["TreePath"] = form.TreePath
|
ctx.Data["TreePath"] = form.TreePath
|
||||||
ctx.Data["TreeNames"] = treeNames
|
ctx.Data["TreeNames"] = treeNames
|
||||||
ctx.Data["TreePaths"] = treePaths
|
ctx.Data["TreePaths"] = treePaths
|
||||||
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + branchName
|
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + ctx.Repo.BranchName
|
||||||
ctx.Data["FileContent"] = form.Content
|
ctx.Data["FileContent"] = form.Content
|
||||||
ctx.Data["commit_summary"] = form.CommitSummary
|
ctx.Data["commit_summary"] = form.CommitSummary
|
||||||
ctx.Data["commit_message"] = form.CommitMessage
|
ctx.Data["commit_message"] = form.CommitMessage
|
||||||
ctx.Data["commit_choice"] = form.CommitChoice
|
ctx.Data["commit_choice"] = form.CommitChoice
|
||||||
ctx.Data["new_branch_name"] = branchName
|
ctx.Data["new_branch_name"] = form.NewBranchName
|
||||||
ctx.Data["last_commit"] = form.LastCommit
|
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
||||||
ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
|
ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
|
||||||
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
|
ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
|
||||||
ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
|
ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
|
||||||
|
@ -201,101 +189,16 @@ func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bo
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(form.TreePath) == 0 {
|
// Cannot commit to a an existing branch if user doesn't have rights
|
||||||
ctx.Data["Err_TreePath"] = true
|
if branchName == ctx.Repo.BranchName && !canCommit {
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_cannot_be_empty"), tplEditFile, &form)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if oldBranchName != branchName {
|
|
||||||
if _, err := ctx.Repo.Repository.GetBranch(branchName); err == nil {
|
|
||||||
ctx.Data["Err_NewBranchName"] = true
|
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplEditFile, &form)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if !canCommit {
|
|
||||||
ctx.Data["Err_NewBranchName"] = true
|
ctx.Data["Err_NewBranchName"] = true
|
||||||
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
|
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
|
ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var newTreePath string
|
// CommitSummary is optional in the web form, if empty, give it a default message based on add or update
|
||||||
for index, part := range treeNames {
|
// `message` will be both the summary and message combined
|
||||||
newTreePath = path.Join(newTreePath, part)
|
|
||||||
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath)
|
|
||||||
if err != nil {
|
|
||||||
if git.IsErrNotExist(err) {
|
|
||||||
// Means there is no item with that name, so we're good
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if index != len(treeNames)-1 {
|
|
||||||
if !entry.IsDir() {
|
|
||||||
ctx.Data["Err_TreePath"] = true
|
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplEditFile, &form)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if entry.IsLink() {
|
|
||||||
ctx.Data["Err_TreePath"] = true
|
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", part), tplEditFile, &form)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if entry.IsDir() {
|
|
||||||
ctx.Data["Err_TreePath"] = true
|
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", part), tplEditFile, &form)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isNewFile {
|
|
||||||
_, err := ctx.Repo.Commit.GetTreeEntryByPath(oldTreePath)
|
|
||||||
if err != nil {
|
|
||||||
if git.IsErrNotExist(err) {
|
|
||||||
ctx.Data["Err_TreePath"] = true
|
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", oldTreePath), tplEditFile, &form)
|
|
||||||
} else {
|
|
||||||
ctx.ServerError("GetTreeEntryByPath", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if lastCommit != ctx.Repo.CommitID {
|
|
||||||
files, err := ctx.Repo.Commit.GetFilesChangedSinceCommit(lastCommit)
|
|
||||||
if err != nil {
|
|
||||||
ctx.ServerError("GetFilesChangedSinceCommit", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
if file == form.TreePath {
|
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+lastCommit+"..."+ctx.Repo.CommitID), tplEditFile, &form)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if oldTreePath != form.TreePath {
|
|
||||||
// We have a new filename (rename or completely new file) so we need to make sure it doesn't already exist, can't clobber.
|
|
||||||
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(form.TreePath)
|
|
||||||
if err != nil {
|
|
||||||
if !git.IsErrNotExist(err) {
|
|
||||||
ctx.ServerError("GetTreeEntryByPath", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if entry != nil {
|
|
||||||
ctx.Data["Err_TreePath"] = true
|
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
message := strings.TrimSpace(form.CommitSummary)
|
message := strings.TrimSpace(form.CommitSummary)
|
||||||
if len(message) == 0 {
|
if len(message) == 0 {
|
||||||
if isNewFile {
|
if isNewFile {
|
||||||
|
@ -304,28 +207,74 @@ func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bo
|
||||||
message = ctx.Tr("repo.editor.update", form.TreePath)
|
message = ctx.Tr("repo.editor.update", form.TreePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
|
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
|
||||||
if len(form.CommitMessage) > 0 {
|
if len(form.CommitMessage) > 0 {
|
||||||
message += "\n\n" + form.CommitMessage
|
message += "\n\n" + form.CommitMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := uploader.UpdateRepoFile(ctx.Repo.Repository, ctx.User, &uploader.UpdateRepoFileOptions{
|
if _, err := repofiles.CreateOrUpdateRepoFile(ctx.Repo.Repository, ctx.User, &repofiles.UpdateRepoFileOptions{
|
||||||
LastCommitID: lastCommit,
|
LastCommitID: form.LastCommit,
|
||||||
OldBranch: oldBranchName,
|
OldBranch: ctx.Repo.BranchName,
|
||||||
NewBranch: branchName,
|
NewBranch: branchName,
|
||||||
OldTreeName: oldTreePath,
|
FromTreePath: ctx.Repo.TreePath,
|
||||||
NewTreeName: form.TreePath,
|
TreePath: form.TreePath,
|
||||||
Message: message,
|
Message: message,
|
||||||
Content: strings.Replace(form.Content, "\r", "", -1),
|
Content: strings.Replace(form.Content, "\r", "", -1),
|
||||||
IsNewFile: isNewFile,
|
IsNewFile: isNewFile,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
// This is where we handle all the errors thrown by repofiles.CreateOrUpdateRepoFile
|
||||||
|
if git.IsErrNotExist(err) {
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
|
||||||
|
} else if models.IsErrFilenameInvalid(err) {
|
||||||
ctx.Data["Err_TreePath"] = true
|
ctx.Data["Err_TreePath"] = true
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_update_file", form.TreePath, err), tplEditFile, &form)
|
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form)
|
||||||
return
|
} else if models.IsErrFilePathInvalid(err) {
|
||||||
|
ctx.Data["Err_TreePath"] = true
|
||||||
|
if fileErr, ok := err.(models.ErrFilePathInvalid); ok {
|
||||||
|
switch fileErr.Type {
|
||||||
|
case git.EntryModeSymlink:
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form)
|
||||||
|
break
|
||||||
|
case git.EntryModeTree:
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form)
|
||||||
|
break
|
||||||
|
case git.EntryModeBlob:
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
ctx.Error(500, err.Error())
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ctx.Error(500, err.Error())
|
||||||
|
}
|
||||||
|
} else if models.IsErrRepoFileAlreadyExists(err) {
|
||||||
|
ctx.Data["Err_TreePath"] = true
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form)
|
||||||
|
} else if models.IsErrBranchNotExist(err) {
|
||||||
|
// For when a user adds/updates a file to a branch that no longer exists
|
||||||
|
if branchErr, ok := err.(models.ErrBranchNotExist); ok {
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form)
|
||||||
|
} else {
|
||||||
|
ctx.Error(500, err.Error())
|
||||||
|
}
|
||||||
|
} else if models.IsErrBranchAlreadyExists(err) {
|
||||||
|
// For when a user specifies a new branch that already exists
|
||||||
|
ctx.Data["Err_NewBranchName"] = true
|
||||||
|
if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok {
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
|
||||||
|
} else {
|
||||||
|
ctx.Error(500, err.Error())
|
||||||
|
}
|
||||||
|
} else if models.IsErrCommitIDDoesNotMatch(err) {
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplEditFile, &form)
|
||||||
|
} else {
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_update_file", form.TreePath, err), tplEditFile, &form)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + branchName + "/" + strings.NewReplacer("%", "%25", "#", "%23", " ", "%20", "?", "%3F").Replace(cleanUploadFileName(form.TreePath)))
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(form.TreePath))
|
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(form.TreePath))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditFilePost response for editing file
|
// EditFilePost response for editing file
|
||||||
|
@ -355,7 +304,7 @@ func DiffPreviewPost(ctx *context.Context, form auth.EditPreviewDiffForm) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
diff, err := uploader.GetDiffPreview(ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content)
|
diff, err := repofiles.GetDiffPreview(ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(500, "GetDiffPreview: "+err.Error())
|
ctx.Error(500, "GetDiffPreview: "+err.Error())
|
||||||
return
|
return
|
||||||
|
@ -386,6 +335,7 @@ func DeleteFile(ctx *context.Context) {
|
||||||
|
|
||||||
ctx.Data["commit_summary"] = ""
|
ctx.Data["commit_summary"] = ""
|
||||||
ctx.Data["commit_message"] = ""
|
ctx.Data["commit_message"] = ""
|
||||||
|
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
||||||
if canCommit {
|
if canCommit {
|
||||||
ctx.Data["commit_choice"] = frmCommitChoiceDirect
|
ctx.Data["commit_choice"] = frmCommitChoiceDirect
|
||||||
} else {
|
} else {
|
||||||
|
@ -398,41 +348,27 @@ func DeleteFile(ctx *context.Context) {
|
||||||
|
|
||||||
// DeleteFilePost response for deleting file
|
// DeleteFilePost response for deleting file
|
||||||
func DeleteFilePost(ctx *context.Context, form auth.DeleteRepoFileForm) {
|
func DeleteFilePost(ctx *context.Context, form auth.DeleteRepoFileForm) {
|
||||||
ctx.Data["PageIsDelete"] = true
|
|
||||||
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
|
|
||||||
|
|
||||||
ctx.Repo.TreePath = cleanUploadFileName(ctx.Repo.TreePath)
|
|
||||||
if len(ctx.Repo.TreePath) == 0 {
|
|
||||||
ctx.Error(500, "Delete file name is invalid")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Data["TreePath"] = ctx.Repo.TreePath
|
|
||||||
canCommit := renderCommitRights(ctx)
|
canCommit := renderCommitRights(ctx)
|
||||||
|
branchName := ctx.Repo.BranchName
|
||||||
oldBranchName := ctx.Repo.BranchName
|
|
||||||
branchName := oldBranchName
|
|
||||||
|
|
||||||
if form.CommitChoice == frmCommitChoiceNewBranch {
|
if form.CommitChoice == frmCommitChoiceNewBranch {
|
||||||
branchName = form.NewBranchName
|
branchName = form.NewBranchName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.Data["PageIsDelete"] = true
|
||||||
|
ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
|
||||||
|
ctx.Data["TreePath"] = ctx.Repo.TreePath
|
||||||
ctx.Data["commit_summary"] = form.CommitSummary
|
ctx.Data["commit_summary"] = form.CommitSummary
|
||||||
ctx.Data["commit_message"] = form.CommitMessage
|
ctx.Data["commit_message"] = form.CommitMessage
|
||||||
ctx.Data["commit_choice"] = form.CommitChoice
|
ctx.Data["commit_choice"] = form.CommitChoice
|
||||||
ctx.Data["new_branch_name"] = branchName
|
ctx.Data["new_branch_name"] = form.NewBranchName
|
||||||
|
ctx.Data["last_commit"] = ctx.Repo.CommitID
|
||||||
|
|
||||||
if ctx.HasError() {
|
if ctx.HasError() {
|
||||||
ctx.HTML(200, tplDeleteFile)
|
ctx.HTML(200, tplDeleteFile)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldBranchName != branchName {
|
if branchName != ctx.Repo.BranchName && !canCommit {
|
||||||
if _, err := ctx.Repo.Repository.GetBranch(branchName); err == nil {
|
|
||||||
ctx.Data["Err_NewBranchName"] = true
|
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplDeleteFile, &form)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if !canCommit {
|
|
||||||
ctx.Data["Err_NewBranchName"] = true
|
ctx.Data["Err_NewBranchName"] = true
|
||||||
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
|
ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form)
|
ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form)
|
||||||
|
@ -443,25 +379,67 @@ func DeleteFilePost(ctx *context.Context, form auth.DeleteRepoFileForm) {
|
||||||
if len(message) == 0 {
|
if len(message) == 0 {
|
||||||
message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath)
|
message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
|
form.CommitMessage = strings.TrimSpace(form.CommitMessage)
|
||||||
if len(form.CommitMessage) > 0 {
|
if len(form.CommitMessage) > 0 {
|
||||||
message += "\n\n" + form.CommitMessage
|
message += "\n\n" + form.CommitMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := uploader.DeleteRepoFile(ctx.Repo.Repository, ctx.User, &uploader.DeleteRepoFileOptions{
|
if _, err := repofiles.DeleteRepoFile(ctx.Repo.Repository, ctx.User, &repofiles.DeleteRepoFileOptions{
|
||||||
LastCommitID: ctx.Repo.CommitID,
|
LastCommitID: form.LastCommit,
|
||||||
OldBranch: oldBranchName,
|
OldBranch: ctx.Repo.BranchName,
|
||||||
NewBranch: branchName,
|
NewBranch: branchName,
|
||||||
TreePath: ctx.Repo.TreePath,
|
TreePath: ctx.Repo.TreePath,
|
||||||
Message: message,
|
Message: message,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
|
// This is where we handle all the errors thrown by repofiles.DeleteRepoFile
|
||||||
|
if git.IsErrNotExist(err) || models.IsErrRepoFileDoesNotExist(err) {
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
|
||||||
|
} else if models.IsErrFilenameInvalid(err) {
|
||||||
|
ctx.Data["Err_TreePath"] = true
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplEditFile, &form)
|
||||||
|
} else if models.IsErrFilePathInvalid(err) {
|
||||||
|
ctx.Data["Err_TreePath"] = true
|
||||||
|
if fileErr, ok := err.(models.ErrFilePathInvalid); ok {
|
||||||
|
switch fileErr.Type {
|
||||||
|
case git.EntryModeSymlink:
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form)
|
||||||
|
break
|
||||||
|
case git.EntryModeTree:
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form)
|
||||||
|
break
|
||||||
|
case git.EntryModeBlob:
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form)
|
||||||
|
break
|
||||||
|
default:
|
||||||
ctx.ServerError("DeleteRepoFile", err)
|
ctx.ServerError("DeleteRepoFile", err)
|
||||||
return
|
break
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("DeleteRepoFile", err)
|
||||||
|
}
|
||||||
|
} else if models.IsErrBranchNotExist(err) {
|
||||||
|
// For when a user deletes a file to a branch that no longer exists
|
||||||
|
if branchErr, ok := err.(models.ErrBranchNotExist); ok {
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form)
|
||||||
|
} else {
|
||||||
|
ctx.Error(500, err.Error())
|
||||||
|
}
|
||||||
|
} else if models.IsErrBranchAlreadyExists(err) {
|
||||||
|
// For when a user specifies a new branch that already exists
|
||||||
|
if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok {
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
|
||||||
|
} else {
|
||||||
|
ctx.Error(500, err.Error())
|
||||||
|
}
|
||||||
|
} else if models.IsErrCommitIDDoesNotMatch(err) {
|
||||||
|
ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplEditFile, &form)
|
||||||
|
} else {
|
||||||
|
ctx.ServerError("DeleteRepoFile", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
|
ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName))
|
ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderUploadSettings(ctx *context.Context) {
|
func renderUploadSettings(ctx *context.Context) {
|
||||||
|
@ -584,7 +562,7 @@ func UploadFilePost(ctx *context.Context, form auth.UploadRepoFileForm) {
|
||||||
message += "\n\n" + form.CommitMessage
|
message += "\n\n" + form.CommitMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := uploader.UploadRepoFiles(ctx.Repo.Repository, ctx.User, &uploader.UploadRepoFileOptions{
|
if err := repofiles.UploadRepoFiles(ctx.Repo.Repository, ctx.User, &repofiles.UploadRepoFileOptions{
|
||||||
LastCommitID: ctx.Repo.CommitID,
|
LastCommitID: ctx.Repo.CommitID,
|
||||||
OldBranch: oldBranchName,
|
OldBranch: oldBranchName,
|
||||||
NewBranch: branchName,
|
NewBranch: branchName,
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="commit-form">
|
<div class="commit-form">
|
||||||
<h3>{{.i18n.Tr "repo.editor.commit_changes"}}</h3>
|
<h3>{{.i18n.Tr "repo.editor.commit_changes"}}</h3>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<input name="commit_summary" placeholder="{{if .PageIsDelete}}{{.i18n.Tr "repo.editor.delete" .TreePath}}{{else if .PageIsUpload}}{{.i18n.Tr "repo.editor.upload_files_to_dir" .TreePath}}{{else if .IsNewFile}}{{.i18n.Tr "repo.editor.add_tmpl" .TreePath}}{{else}}{{.i18n.Tr "repo.editor.update" .TreePath}}{{end}}" value="{{.commit_summary}}" autofocus>
|
<input name="commit_summary" placeholder="{{if .PageIsDelete}}{{.i18n.Tr "repo.editor.delete" .TreePath}}{{else if .PageIsUpload}}{{.i18n.Tr "repo.editor.upload_files_to_dir" .TreePath}}{{else if .IsNewFile}}{{.i18n.Tr "repo.editor.add_tmpl"}}{{else}}{{.i18n.Tr "repo.editor.update" .TreePath}}{{end}}" value="{{.commit_summary}}" autofocus>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<textarea name="commit_message" placeholder="{{.i18n.Tr "repo.editor.commit_message_desc"}}" rows="5">{{.commit_message}}</textarea>
|
<textarea name="commit_message" placeholder="{{.i18n.Tr "repo.editor.commit_message_desc"}}" rows="5">{{.commit_message}}</textarea>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
{{template "base/alert" .}}
|
{{template "base/alert" .}}
|
||||||
<form class="ui form" method="post">
|
<form class="ui form" method="post">
|
||||||
{{.CsrfTokenHtml}}
|
{{.CsrfTokenHtml}}
|
||||||
|
<input type="hidden" name="last_commit" value="{{.last_commit}}">
|
||||||
{{template "repo/editor/commit_form" .}}
|
{{template "repo/editor/commit_form" .}}
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1511,6 +1511,199 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{owner}/{repo}/contents/{filepath}": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Gets the contents of a file or directory in a repository",
|
||||||
|
"operationId": "repoGetFileContents",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "path of the file to delete",
|
||||||
|
"name": "filepath",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "The name of the commit/branch/tag. Default the repository’s default branch (usually master)",
|
||||||
|
"name": "ref",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/FileContentResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Update a file in a repository",
|
||||||
|
"operationId": "repoUpdateFile",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "path of the file to update",
|
||||||
|
"name": "filepath",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "'content' must be base64 encoded\n\n 'sha' is the SHA for the file that already exists\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'new_branch' (optional) will make a new branch from 'branch' before updating the file",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/UpdateFileOptions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/FileResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Create a file in a repository",
|
||||||
|
"operationId": "repoCreateFile",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "path of the file to create",
|
||||||
|
"name": "filepath",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "'content' must be base64 encoded\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'sha' is the SHA for the file that already exists\n\n 'new_branch' (optional) will make a new branch from 'branch' before creating the file",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/CreateFileOptions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"201": {
|
||||||
|
"$ref": "#/responses/FileResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Delete a file in a repository",
|
||||||
|
"operationId": "repoDeleteFile",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "path of the file to delete",
|
||||||
|
"name": "filepath",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "'sha' is the SHA for the file to be deleted\n\n 'author' and 'committer' are optional (if only one is given, it will be used for the other, otherwise the authenticated user will be used)\n\n If 'branch' is not given, default branch will be used\n\n 'new_branch' (optional) will make a new branch from 'branch' before deleting the file",
|
||||||
|
"name": "body",
|
||||||
|
"in": "body",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/DeleteFileOptions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/FileDeleteResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{owner}/{repo}/editorconfig/{filepath}": {
|
"/repos/{owner}/{repo}/editorconfig/{filepath}": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
@ -1622,6 +1815,46 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/repos/{owner}/{repo}/git/blobs/{sha}": {
|
||||||
|
"get": {
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"repository"
|
||||||
|
],
|
||||||
|
"summary": "Gets the blob of a repository.",
|
||||||
|
"operationId": "GetBlob",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "owner of the repo",
|
||||||
|
"name": "owner",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "name of the repo",
|
||||||
|
"name": "repo",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "sha of the commit",
|
||||||
|
"name": "sha",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"$ref": "#/responses/GitBlobResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{owner}/{repo}/git/commits/{sha}": {
|
"/repos/{owner}/{repo}/git/commits/{sha}": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
@ -6637,6 +6870,35 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
},
|
},
|
||||||
|
"CreateFileOptions": {
|
||||||
|
"description": "CreateFileOptions options for creating files",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"author": {
|
||||||
|
"$ref": "#/definitions/Identity"
|
||||||
|
},
|
||||||
|
"branch": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "BranchName"
|
||||||
|
},
|
||||||
|
"committer": {
|
||||||
|
"$ref": "#/definitions/Identity"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Content"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Message"
|
||||||
|
},
|
||||||
|
"new_branch": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "NewBranchName"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
|
},
|
||||||
"CreateForkOption": {
|
"CreateForkOption": {
|
||||||
"description": "CreateForkOption options for creating a fork",
|
"description": "CreateForkOption options for creating a fork",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -7129,6 +7391,35 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
},
|
},
|
||||||
|
"DeleteFileOptions": {
|
||||||
|
"description": "DeleteFileOptions options for deleting files (used for other File structs below)",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"author": {
|
||||||
|
"$ref": "#/definitions/Identity"
|
||||||
|
},
|
||||||
|
"branch": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "BranchName"
|
||||||
|
},
|
||||||
|
"committer": {
|
||||||
|
"$ref": "#/definitions/Identity"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Message"
|
||||||
|
},
|
||||||
|
"new_branch": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "NewBranchName"
|
||||||
|
},
|
||||||
|
"sha": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "SHA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
|
},
|
||||||
"DeployKey": {
|
"DeployKey": {
|
||||||
"description": "DeployKey a deploy key",
|
"description": "DeployKey a deploy key",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -7567,6 +7858,144 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
},
|
},
|
||||||
|
"FileCommitResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"title": "FileCommitResponse contains information generated from a Git commit for a repo's file.",
|
||||||
|
"properties": {
|
||||||
|
"author": {
|
||||||
|
"$ref": "#/definitions/CommitUser"
|
||||||
|
},
|
||||||
|
"committer": {
|
||||||
|
"$ref": "#/definitions/CommitUser"
|
||||||
|
},
|
||||||
|
"html_url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "HTMLURL"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Message"
|
||||||
|
},
|
||||||
|
"parents": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/CommitMeta"
|
||||||
|
},
|
||||||
|
"x-go-name": "Parents"
|
||||||
|
},
|
||||||
|
"sha": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "SHA"
|
||||||
|
},
|
||||||
|
"tree": {
|
||||||
|
"$ref": "#/definitions/CommitMeta"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
|
},
|
||||||
|
"FileContentResponse": {
|
||||||
|
"description": "FileContentResponse contains information about a repo's file stats and content",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"_links": {
|
||||||
|
"$ref": "#/definitions/FileLinksResponse"
|
||||||
|
},
|
||||||
|
"download_url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "DownloadURL"
|
||||||
|
},
|
||||||
|
"git_url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "GitURL"
|
||||||
|
},
|
||||||
|
"html_url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "HTMLURL"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Name"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Path"
|
||||||
|
},
|
||||||
|
"sha": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "SHA"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "Size"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Type"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
|
},
|
||||||
|
"FileDeleteResponse": {
|
||||||
|
"description": "FileDeleteResponse contains information about a repo's file that was deleted",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"commit": {
|
||||||
|
"$ref": "#/definitions/FileCommitResponse"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "object",
|
||||||
|
"x-go-name": "Content"
|
||||||
|
},
|
||||||
|
"verification": {
|
||||||
|
"$ref": "#/definitions/PayloadCommitVerification"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
|
},
|
||||||
|
"FileLinksResponse": {
|
||||||
|
"description": "FileLinksResponse contains the links for a repo's file",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"git_url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "GitURL"
|
||||||
|
},
|
||||||
|
"html_url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "HTMLURL"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Self"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
|
},
|
||||||
|
"FileResponse": {
|
||||||
|
"description": "FileResponse contains information about a repo's file",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"commit": {
|
||||||
|
"$ref": "#/definitions/FileCommitResponse"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"$ref": "#/definitions/FileContentResponse"
|
||||||
|
},
|
||||||
|
"verification": {
|
||||||
|
"$ref": "#/definitions/PayloadCommitVerification"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
|
},
|
||||||
"GPGKey": {
|
"GPGKey": {
|
||||||
"description": "GPGKey a user GPG key to sign commit and tag in repository",
|
"description": "GPGKey a user GPG key to sign commit and tag in repository",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -7646,6 +8075,34 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
},
|
},
|
||||||
|
"GitBlobResponse": {
|
||||||
|
"description": "GitBlobResponse represents a git blob",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Content"
|
||||||
|
},
|
||||||
|
"encoding": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Encoding"
|
||||||
|
},
|
||||||
|
"sha": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "SHA"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"x-go-name": "Size"
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "URL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
|
},
|
||||||
"GitEntry": {
|
"GitEntry": {
|
||||||
"description": "GitEntry represents a git tree",
|
"description": "GitEntry represents a git tree",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -7796,6 +8253,22 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
},
|
},
|
||||||
|
"Identity": {
|
||||||
|
"description": "Identity for a person's identity like an author or committer",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"email": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "email",
|
||||||
|
"x-go-name": "Email"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
|
},
|
||||||
"Issue": {
|
"Issue": {
|
||||||
"description": "Issue represents an issue in a repository",
|
"description": "Issue represents an issue in a repository",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -8855,6 +9328,43 @@
|
||||||
},
|
},
|
||||||
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
},
|
},
|
||||||
|
"UpdateFileOptions": {
|
||||||
|
"description": "UpdateFileOptions options for updating files",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"author": {
|
||||||
|
"$ref": "#/definitions/Identity"
|
||||||
|
},
|
||||||
|
"branch": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "BranchName"
|
||||||
|
},
|
||||||
|
"committer": {
|
||||||
|
"$ref": "#/definitions/Identity"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Content"
|
||||||
|
},
|
||||||
|
"from_path": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "FromPath"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "Message"
|
||||||
|
},
|
||||||
|
"new_branch": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "NewBranchName"
|
||||||
|
},
|
||||||
|
"sha": {
|
||||||
|
"type": "string",
|
||||||
|
"x-go-name": "SHA"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
|
||||||
|
},
|
||||||
"User": {
|
"User": {
|
||||||
"description": "User represents a user",
|
"description": "User represents a user",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
|
@ -9040,6 +9550,24 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"FileContentResponse": {
|
||||||
|
"description": "FileContentResponse",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/FileContentResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FileDeleteResponse": {
|
||||||
|
"description": "FileDeleteResponse",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/FileDeleteResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"FileResponse": {
|
||||||
|
"description": "FileResponse",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/FileResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
"GPGKey": {
|
"GPGKey": {
|
||||||
"description": "GPGKey",
|
"description": "GPGKey",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
@ -9055,6 +9583,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"GitBlobResponse": {
|
||||||
|
"description": "GitBlobResponse",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/GitBlobResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
"GitHook": {
|
"GitHook": {
|
||||||
"description": "GitHook",
|
"description": "GitHook",
|
||||||
"schema": {
|
"schema": {
|
||||||
|
@ -9362,7 +9896,7 @@
|
||||||
"parameterBodies": {
|
"parameterBodies": {
|
||||||
"description": "parameterBodies",
|
"description": "parameterBodies",
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/definitions/EditAttachmentOptions"
|
"$ref": "#/definitions/DeleteFileOptions"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"redirect": {
|
"redirect": {
|
||||||
|
|
Loading…
Reference in a new issue