History

Fixes 4762 - Content API for Creating, Updating, Deleting Files (#6314)

Richard Mahn committed 7 years ago

Commit 22628111 parent 059195b54 files · +4154 −563Browse files

Unified · Split

54 files
  1. custom/conf/app.ini.sample +2 −0
  2. docs/content/doc/advanced/config-cheat-sheet.en-us.md +3 −2
  3. docs/content/doc/advanced/config-cheat-sheet.zh-cn.md +2 −1
  4. integrations/api_repo_file_content_test.go +114 −0
  5. integrations/api_repo_file_create_test.go +215 −0
  6. integrations/api_repo_file_delete_test.go +163 −0
  7. integrations/api_repo_file_helpers.go +27 −0
  8. integrations/api_repo_file_update_test.go +234 −0
  9. integrations/api_repo_git_blobs_test.go +76 −0
  10. integrations/api_repo_git_trees_test.go +74 −0
  11. integrations/integration_test.go +2 −1
  12. integrations/migration-test/migration_test.go +2 −1
  13. models/error.go +150 −18
  14. models/unit_tests.go +2 −0
  15. modules/auth/repo_form.go +1 −0
  16. modules/base/tool.go +25 −0
  17. modules/context/repo.go +15 −0
  18. modules/git/blob.go +30 −0
  19. modules/git/commit.go +5 −0
  20. modules/git/repo_commit.go +11 −2
  21. modules/repofiles/blob.go +38 −0
  22. modules/repofiles/blob_test.go +38 −0
  23. modules/repofiles/content.go +73 −0
  24. modules/repofiles/content_test.go +90 −0
  25. modules/repofiles/delete.go +209 −0
  26. modules/repofiles/delete_test.go +183 −0
  27. modules/repofiles/diff.go +5 −2
  28. modules/repofiles/diff_test.go +143 −0
  29. modules/repofiles/file.go +125 −0
  30. modules/repofiles/file_test.go +90 −0
  31. modules/repofiles/repofiles.go +23 −0
  32. modules/repofiles/repofiles_test.go +27 −0
  33. modules/repofiles/temp_repo.go +76 −10
  34. modules/repofiles/tree.go +92 −0
  35. modules/repofiles/tree_test.go +13 −11
  36. modules/repofiles/update.go +331 −0
  37. modules/repofiles/update_test.go +357 −0
  38. modules/repofiles/upload.go +6 −2
  39. modules/repofiles/verification.go +29 −0
  40. modules/setting/setting.go +2 −0
  41. modules/uploader/delete.go +0 −100
  42. modules/uploader/update.go +0 −159
  43. options/locale/locale_en-US.ini +4 −1
  44. routers/api/v1/api.go +10 −1
  45. routers/api/v1/repo/blob.go +51 −0
  46. routers/api/v1/repo/file.go +269 −5
  47. routers/api/v1/repo/tree.go +6 −88
  48. routers/api/v1/swagger/options.go +10 −0
  49. routers/api/v1/swagger/repo.go +28 −0
  50. routers/repo/editor.go +134 −156
  51. routers/repo/editor_test.go +1 −0
  52. templates/repo/editor/commit_form.tmpl +1 −1
  53. templates/repo/editor/delete.tmpl +2 −1
  54. templates/swagger/v1_json.tmpl +535 −1
custom/conf/app.ini.sample+2−0
@@ -672,6 +672,8 @@ MAX_RESPONSE_ITEMS = 50 · 671 unmodified lines
672DEFAULT_PAGING_NUM = 30672DEFAULT_PAGING_NUM = 30
673; Default and maximum number of items per page for git trees api673; Default and maximum number of items per page for git trees api
674DEFAULT_GIT_TREES_PER_PAGE = 1000674DEFAULT_GIT_TREES_PER_PAGE = 1000
675; Default size of a blob returned by the blobs API (default is 10MiB)
676DEFAULT_MAX_BLOB_SIZE = 10485760
675 677
676[oauth2]678[oauth2]
677; Enables OAuth2 provider679; Enables OAuth2 provider
docs/content/doc/advanced/config-cheat-sheet.en-us.md+3−2
@@ -402,8 +402,9 @@ NB: You must `REDIRECT_MACARON_LOG` and have `DISABLE_ROUTER_LOG` set to `false` · 401 unmodified lines
402 402
403- `ENABLE_SWAGGER`: **true**: Enables /api/swagger, /api/v1/swagger etc. endpoints. True or false; default is true.403- `ENABLE_SWAGGER`: **true**: Enables /api/swagger, /api/v1/swagger etc. endpoints. True or false; default is true.
404- `MAX_RESPONSE_ITEMS`: **50**: Max number of items in a page.404- `MAX_RESPONSE_ITEMS`: **50**: Max number of items in a page.
405- `DEFAULT_PAGING_NUM`: **30**: Default paging number of api.405- `DEFAULT_PAGING_NUM`: **30**: Default paging number of API.
406- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: Default and maximum number of items per page for git trees api.406- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: Default and maximum number of items per page for git trees API.
407- `DEFAULT_MAX_BLOB_SIZE`: **10485760**: Default max size of a blob that can be return by the blobs API.
407 408
408## OAuth2 (`oauth2`)409## OAuth2 (`oauth2`)
409 410
docs/content/doc/advanced/config-cheat-sheet.zh-cn.md+2−1
@@ -215,7 +215,8 @@ menu: · 214 unmodified lines
215- `ENABLE_SWAGGER`: **true**: 是否启用swagger路由 /api/swagger, /api/v1/swagger etc. endpoints. True 或 false; 默认是 true.215- `ENABLE_SWAGGER`: **true**: 是否启用swagger路由 /api/swagger, /api/v1/swagger etc. endpoints. True 或 false; 默认是 true.
216- `MAX_RESPONSE_ITEMS`: **50**: 一个页面最大的项目数。216- `MAX_RESPONSE_ITEMS`: **50**: 一个页面最大的项目数。
217- `DEFAULT_PAGING_NUM`: **30**: API中默认分页条数。217- `DEFAULT_PAGING_NUM`: **30**: API中默认分页条数。
218- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: GIT TREES API每页的默认和最大项数.218- `DEFAULT_GIT_TREES_PER_PAGE`: **1000**: GIT TREES API每页的默认最大项数.
219- `DEFAULT_MAX_BLOB_SIZE`: **10485760**: BLOBS API默认最大大小.
219 220
220## Markup (`markup`)221## Markup (`markup`)
221 222
integrations/api_repo_file_content_test.go+114−0
@@ -0,0 +1,114 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package integrations
6
7import (
8 "net/http"
9 "path/filepath"
10 "testing"
11
12 "code.gitea.io/gitea/models"
13 "code.gitea.io/gitea/modules/base"
14 "code.gitea.io/gitea/modules/context"
15 "code.gitea.io/gitea/modules/setting"
16 api "code.gitea.io/sdk/gitea"
17
18 "github.com/stretchr/testify/assert"
19)
20
21func getExpectedFileContentResponseForFileContents(branch string) *api.FileContentResponse {
22 treePath := "README.md"
23 sha := "4b4851ad51df6a7d9f25c979345979eaeb5b349f"
24 return &api.FileContentResponse{
25 Name: filepath.Base(treePath),
26 Path: treePath,
27 SHA: sha,
28 Size: 30,
29 URL: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
30 HTMLURL: setting.AppURL + "user2/repo1/blob/" + branch + "/" + treePath,
31 GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
32 DownloadURL: setting.AppURL + "user2/repo1/raw/branch/" + branch + "/" + treePath,
33 Type: "blob",
34 Links: &api.FileLinksResponse{
35 Self: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
36 GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
37 HTMLURL: setting.AppURL + "user2/repo1/blob/" + branch + "/" + treePath,
38 },
39 }
40}
41
42func TestAPIGetFileContents(t *testing.T) {
43 prepareTestEnv(t)
44 user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16
45 user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3, is an org
46 user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos
47 repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo
48 repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo
49 repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
50 treePath := "README.md"
51
52 // Get user2's token
53 session := loginUser(t, user2.Name)
54 token2 := getTokenForLoggedInUser(t, session)
55 session = emptyTestSession(t)
56 // Get user4's token
57 session = loginUser(t, user4.Name)
58 token4 := getTokenForLoggedInUser(t, session)
59 session = emptyTestSession(t)
60
61 // Make a second master branch in repo1
62 repo1.CreateNewBranch(user2, repo1.DefaultBranch, "master2")
63
64 // ref is default branch
65 branch := repo1.DefaultBranch
66 req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, branch)
67 resp := session.MakeRequest(t, req, http.StatusOK)
68 var fileContentResponse api.FileContentResponse
69 DecodeJSON(t, resp, &fileContentResponse)
70 assert.NotNil(t, fileContentResponse)
71 expectedFileContentResponse := getExpectedFileContentResponseForFileContents(branch)
72 assert.EqualValues(t, *expectedFileContentResponse, fileContentResponse)
73
74 // No ref
75 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s", user2.Name, repo1.Name, treePath)
76 resp = session.MakeRequest(t, req, http.StatusOK)
77 DecodeJSON(t, resp, &fileContentResponse)
78 assert.NotNil(t, fileContentResponse)
79 expectedFileContentResponse = getExpectedFileContentResponseForFileContents(repo1.DefaultBranch)
80 assert.EqualValues(t, *expectedFileContentResponse, fileContentResponse)
81
82 // ref is master2
83 branch = "master2"
84 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, branch)
85 resp = session.MakeRequest(t, req, http.StatusOK)
86 DecodeJSON(t, resp, &fileContentResponse)
87 assert.NotNil(t, fileContentResponse)
88 expectedFileContentResponse = getExpectedFileContentResponseForFileContents("master2")
89 assert.EqualValues(t, *expectedFileContentResponse, fileContentResponse)
90
91 // Test file contents a file with the wrong branch
92 branch = "badbranch"
93 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?ref=%s", user2.Name, repo1.Name, treePath, branch)
94 resp = session.MakeRequest(t, req, http.StatusInternalServerError)
95 expectedAPIError := context.APIError{
96 Message: "object does not exist [id: " + branch + ", rel_path: ]",
97 URL: base.DocURL,
98 }
99 var apiError context.APIError
100 DecodeJSON(t, resp, &apiError)
101 assert.Equal(t, expectedAPIError, apiError)
102
103 // Test accessing private branch with user token that does not have access - should fail
104 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4)
105 session.MakeRequest(t, req, http.StatusNotFound)
106
107 // Test access private branch of owner of token
108 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/readme.md?token=%s", user2.Name, repo16.Name, token2)
109 session.MakeRequest(t, req, http.StatusOK)
110
111 // Test access of org user3 private repo file by owner user2
112 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2)
113 session.MakeRequest(t, req, http.StatusOK)
114}
integrations/api_repo_file_create_test.go+215−0
@@ -0,0 +1,215 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package integrations
6
7import (
8 "encoding/base64"
9 "fmt"
10 "net/http"
11 "path/filepath"
12 "testing"
13
14 "code.gitea.io/gitea/models"
15 "code.gitea.io/gitea/modules/base"
16 "code.gitea.io/gitea/modules/context"
17 "code.gitea.io/gitea/modules/git"
18 "code.gitea.io/gitea/modules/setting"
19 api "code.gitea.io/sdk/gitea"
20
21 "github.com/stretchr/testify/assert"
22)
23
24func getCreateFileOptions() api.CreateFileOptions {
25 content := "This is new text"
26 contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
27 return api.CreateFileOptions{
28 FileOptions: api.FileOptions{
29 BranchName: "master",
30 NewBranchName: "master",
31 Message: "Creates new/file.txt",
32 Author: api.Identity{
33 Name: "John Doe",
34 Email: "johndoe@example.com",
35 },
36 Committer: api.Identity{
37 Name: "Jane Doe",
38 Email: "janedoe@example.com",
39 },
40 },
41 Content: contentEncoded,
42 }
43}
44
45func getExpectedFileResponseForCreate(commitID, treePath string) *api.FileResponse {
46 sha := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
47 return &api.FileResponse{
48 Content: &api.FileContentResponse{
49 Name: filepath.Base(treePath),
50 Path: treePath,
51 SHA: sha,
52 Size: 16,
53 URL: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
54 HTMLURL: setting.AppURL + "user2/repo1/blob/master/" + treePath,
55 GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
56 DownloadURL: setting.AppURL + "user2/repo1/raw/branch/master/" + treePath,
57 Type: "blob",
58 Links: &api.FileLinksResponse{
59 Self: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
60 GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
61 HTMLURL: setting.AppURL + "user2/repo1/blob/master/" + treePath,
62 },
63 },
64 Commit: &api.FileCommitResponse{
65 CommitMeta: api.CommitMeta{
66 URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID,
67 SHA: commitID,
68 },
69 HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID,
70 Author: &api.CommitUser{
71 Identity: api.Identity{
72 Name: "Jane Doe",
73 Email: "janedoe@example.com",
74 },
75 },
76 Committer: &api.CommitUser{
77 Identity: api.Identity{
78 Name: "John Doe",
79 Email: "johndoe@example.com",
80 },
81 },
82 Message: "Updates README.md\n",
83 },
84 Verification: &api.PayloadCommitVerification{
85 Verified: false,
86 Reason: "unsigned",
87 Signature: "",
88 Payload: "",
89 },
90 }
91}
92
93func TestAPICreateFile(t *testing.T) {
94 prepareTestEnv(t)
95 user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16
96 user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3, is an org
97 user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos
98 repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo
99 repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo
100 repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
101 fileID := 0
102
103 // Get user2's token
104 session := loginUser(t, user2.Name)
105 token2 := getTokenForLoggedInUser(t, session)
106 session = emptyTestSession(t)
107 // Get user4's token
108 session = loginUser(t, user4.Name)
109 token4 := getTokenForLoggedInUser(t, session)
110 session = emptyTestSession(t)
111
112 // Test creating a file in repo1 which user2 owns, try both with branch and empty branch
113 for _, branch := range [...]string{
114 "master", // Branch
115 "", // Empty branch
116 } {
117 createFileOptions := getCreateFileOptions()
118 createFileOptions.BranchName = branch
119 fileID++
120 treePath := fmt.Sprintf("new/file%d.txt", fileID)
121 url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
122 req := NewRequestWithJSON(t, "POST", url, &createFileOptions)
123 resp := session.MakeRequest(t, req, http.StatusCreated)
124 gitRepo, _ := git.OpenRepository(repo1.RepoPath())
125 commitID, _ := gitRepo.GetBranchCommitID(createFileOptions.NewBranchName)
126 expectedFileResponse := getExpectedFileResponseForCreate(commitID, treePath)
127 var fileResponse api.FileResponse
128 DecodeJSON(t, resp, &fileResponse)
129 assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
130 assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
131 assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
132 assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
133 assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
134 }
135
136 // Test creating a file in a new branch
137 createFileOptions := getCreateFileOptions()
138 createFileOptions.BranchName = repo1.DefaultBranch
139 createFileOptions.NewBranchName = "new_branch"
140 fileID++
141 treePath := fmt.Sprintf("new/file%d.txt", fileID)
142 url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
143 req := NewRequestWithJSON(t, "POST", url, &createFileOptions)
144 resp := session.MakeRequest(t, req, http.StatusCreated)
145 var fileResponse api.FileResponse
146 DecodeJSON(t, resp, &fileResponse)
147 expectedSHA := "a635aa942442ddfdba07468cf9661c08fbdf0ebf"
148 expectedHTMLURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/blob/new_branch/new/file%d.txt", fileID)
149 expectedDownloadURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/raw/branch/new_branch/new/file%d.txt", fileID)
150 assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
151 assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL)
152 assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL)
153
154 // Test trying to create a file that already exists, should fail
155 createFileOptions = getCreateFileOptions()
156 treePath = "README.md"
157 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
158 req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
159 resp = session.MakeRequest(t, req, http.StatusInternalServerError)
160 expectedAPIError := context.APIError{
161 Message: "repository file already exists [path: " + treePath + "]",
162 URL: base.DocURL,
163 }
164 var apiError context.APIError
165 DecodeJSON(t, resp, &apiError)
166 assert.Equal(t, expectedAPIError, apiError)
167
168 // Test creating a file in repo1 by user4 who does not have write access
169 createFileOptions = getCreateFileOptions()
170 fileID++
171 treePath = fmt.Sprintf("new/file%d.txt", fileID)
172 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4)
173 req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
174 session.MakeRequest(t, req, http.StatusNotFound)
175
176 // Tests a repo with no token given so will fail
177 createFileOptions = getCreateFileOptions()
178 fileID++
179 treePath = fmt.Sprintf("new/file%d.txt", fileID)
180 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath)
181 req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
182 session.MakeRequest(t, req, http.StatusNotFound)
183
184 // Test using access token for a private repo that the user of the token owns
185 createFileOptions = getCreateFileOptions()
186 fileID++
187 treePath = fmt.Sprintf("new/file%d.txt", fileID)
188 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token2)
189 req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
190 session.MakeRequest(t, req, http.StatusCreated)
191
192 // Test using org repo "user3/repo3" where user2 is a collaborator
193 createFileOptions = getCreateFileOptions()
194 fileID++
195 treePath = fmt.Sprintf("new/file%d.txt", fileID)
196 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2)
197 req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
198 session.MakeRequest(t, req, http.StatusCreated)
199
200 // Test using org repo "user3/repo3" with no user token
201 createFileOptions = getCreateFileOptions()
202 fileID++
203 treePath = fmt.Sprintf("new/file%d.txt", fileID)
204 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user3.Name, repo3.Name, treePath)
205 req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
206 session.MakeRequest(t, req, http.StatusNotFound)
207
208 // Test using repo "user2/repo1" where user4 is a NOT collaborator
209 createFileOptions = getCreateFileOptions()
210 fileID++
211 treePath = fmt.Sprintf("new/file%d.txt", fileID)
212 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token4)
213 req = NewRequestWithJSON(t, "POST", url, &createFileOptions)
214 session.MakeRequest(t, req, http.StatusForbidden)
215}
integrations/api_repo_file_delete_test.go+163−0
@@ -0,0 +1,163 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package integrations
6
7import (
8 "fmt"
9 "net/http"
10 "testing"
11
12 "code.gitea.io/gitea/models"
13 "code.gitea.io/gitea/modules/base"
14 "code.gitea.io/gitea/modules/context"
15 api "code.gitea.io/sdk/gitea"
16
17 "github.com/stretchr/testify/assert"
18)
19
20func getDeleteFileOptions() *api.DeleteFileOptions {
21 return &api.DeleteFileOptions{
22 FileOptions: api.FileOptions{
23 BranchName: "master",
24 NewBranchName: "master",
25 Message: "Updates new/file.txt",
26 Author: api.Identity{
27 Name: "John Doe",
28 Email: "johndoe@example.com",
29 },
30 Committer: api.Identity{
31 Name: "Jane Doe",
32 Email: "janedoe@example.com",
33 },
34 },
35 SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
36 }
37}
38
39func TestAPIDeleteFile(t *testing.T) {
40 prepareTestEnv(t)
41 user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16
42 user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3, is an org
43 user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos
44 repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo
45 repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo
46 repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
47 fileID := 0
48
49 // Get user2's token
50 session := loginUser(t, user2.Name)
51 token2 := getTokenForLoggedInUser(t, session)
52 session = emptyTestSession(t)
53 // Get user4's token
54 session = loginUser(t, user4.Name)
55 token4 := getTokenForLoggedInUser(t, session)
56 session = emptyTestSession(t)
57
58 // Test deleting a file in repo1 which user2 owns, try both with branch and empty branch
59 for _, branch := range [...]string{
60 "master", // Branch
61 "", // Empty branch
62 } {
63 fileID++
64 treePath := fmt.Sprintf("delete/file%d.txt", fileID)
65 createFile(user2, repo1, treePath)
66 deleteFileOptions := getDeleteFileOptions()
67 deleteFileOptions.BranchName = branch
68 url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
69 req := NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
70 resp := session.MakeRequest(t, req, http.StatusOK)
71 var fileResponse api.FileResponse
72 DecodeJSON(t, resp, &fileResponse)
73 assert.NotNil(t, fileResponse)
74 assert.Nil(t, fileResponse.Content)
75 }
76
77 // Test deleting file and making the delete in a new branch
78 fileID++
79 treePath := fmt.Sprintf("delete/file%d.txt", fileID)
80 createFile(user2, repo1, treePath)
81 deleteFileOptions := getDeleteFileOptions()
82 deleteFileOptions.BranchName = repo1.DefaultBranch
83 deleteFileOptions.NewBranchName = "new_branch"
84 url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
85 req := NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
86 resp := session.MakeRequest(t, req, http.StatusOK)
87 var fileResponse api.FileResponse
88 DecodeJSON(t, resp, &fileResponse)
89 assert.NotNil(t, fileResponse)
90 assert.Nil(t, fileResponse.Content)
91
92 // Test deleting a file with the wrong SHA
93 fileID++
94 treePath = fmt.Sprintf("delete/file%d.txt", fileID)
95 createFile(user2, repo1, treePath)
96 deleteFileOptions = getDeleteFileOptions()
97 correctSHA := deleteFileOptions.SHA
98 deleteFileOptions.SHA = "badsha"
99 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
100 req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
101 resp = session.MakeRequest(t, req, http.StatusInternalServerError)
102 expectedAPIError := context.APIError{
103 Message: "sha does not match [given: " + deleteFileOptions.SHA + ", expected: " + correctSHA + "]",
104 URL: base.DocURL,
105 }
106 var apiError context.APIError
107 DecodeJSON(t, resp, &apiError)
108 assert.Equal(t, expectedAPIError, apiError)
109
110 // Test creating a file in repo1 by user4 who does not have write access
111 fileID++
112 treePath = fmt.Sprintf("delete/file%d.txt", fileID)
113 createFile(user2, repo16, treePath)
114 deleteFileOptions = getDeleteFileOptions()
115 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4)
116 req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
117 session.MakeRequest(t, req, http.StatusNotFound)
118
119 // Tests a repo with no token given so will fail
120 fileID++
121 treePath = fmt.Sprintf("delete/file%d.txt", fileID)
122 createFile(user2, repo16, treePath)
123 deleteFileOptions = getDeleteFileOptions()
124 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath)
125 req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
126 session.MakeRequest(t, req, http.StatusNotFound)
127
128 // Test using access token for a private repo that the user of the token owns
129 fileID++
130 treePath = fmt.Sprintf("delete/file%d.txt", fileID)
131 createFile(user2, repo16, treePath)
132 deleteFileOptions = getDeleteFileOptions()
133 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token2)
134 req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
135 session.MakeRequest(t, req, http.StatusOK)
136
137 // Test using org repo "user3/repo3" where user2 is a collaborator
138 fileID++
139 treePath = fmt.Sprintf("delete/file%d.txt", fileID)
140 createFile(user3, repo3, treePath)
141 deleteFileOptions = getDeleteFileOptions()
142 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2)
143 req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
144 session.MakeRequest(t, req, http.StatusOK)
145
146 // Test using org repo "user3/repo3" with no user token
147 fileID++
148 treePath = fmt.Sprintf("delete/file%d.txt", fileID)
149 createFile(user3, repo3, treePath)
150 deleteFileOptions = getDeleteFileOptions()
151 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user3.Name, repo3.Name, treePath)
152 req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
153 session.MakeRequest(t, req, http.StatusNotFound)
154
155 // Test using repo "user2/repo1" where user4 is a NOT collaborator
156 fileID++
157 treePath = fmt.Sprintf("delete/file%d.txt", fileID)
158 createFile(user2, repo1, treePath)
159 deleteFileOptions = getDeleteFileOptions()
160 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token4)
161 req = NewRequestWithJSON(t, "DELETE", url, &deleteFileOptions)
162 session.MakeRequest(t, req, http.StatusForbidden)
163}
integrations/api_repo_file_helpers.go+27−0
@@ -0,0 +1,27 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package integrations
6
7import (
8 "code.gitea.io/gitea/models"
9 "code.gitea.io/gitea/modules/repofiles"
10 api "code.gitea.io/sdk/gitea"
11)
12
13func createFileInBranch(user *models.User, repo *models.Repository, treePath, branchName string) (*api.FileResponse, error) {
14 opts := &repofiles.UpdateRepoFileOptions{
15 OldBranch: branchName,
16 TreePath: treePath,
17 Content: "This is a NEW file",
18 IsNewFile: true,
19 Author: nil,
20 Committer: nil,
21 }
22 return repofiles.CreateOrUpdateRepoFile(repo, user, opts)
23}
24
25func createFile(user *models.User, repo *models.Repository, treePath string) (*api.FileResponse, error) {
26 return createFileInBranch(user, repo, treePath, repo.DefaultBranch)
27}
integrations/api_repo_file_update_test.go+234−0
@@ -0,0 +1,234 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package integrations
6
7import (
8 "encoding/base64"
9 "fmt"
10 "net/http"
11 "path/filepath"
12 "testing"
13
14 "code.gitea.io/gitea/models"
15 "code.gitea.io/gitea/modules/base"
16 "code.gitea.io/gitea/modules/context"
17 "code.gitea.io/gitea/modules/git"
18 "code.gitea.io/gitea/modules/setting"
19 api "code.gitea.io/sdk/gitea"
20
21 "github.com/stretchr/testify/assert"
22)
23
24func getUpdateFileOptions() *api.UpdateFileOptions {
25 content := "This is updated text"
26 contentEncoded := base64.StdEncoding.EncodeToString([]byte(content))
27 return &api.UpdateFileOptions{
28 DeleteFileOptions: *getDeleteFileOptions(),
29 Content: contentEncoded,
30 }
31}
32
33func getExpectedFileResponseForUpdate(commitID, treePath string) *api.FileResponse {
34 sha := "08bd14b2e2852529157324de9c226b3364e76136"
35 return &api.FileResponse{
36 Content: &api.FileContentResponse{
37 Name: filepath.Base(treePath),
38 Path: treePath,
39 SHA: sha,
40 Size: 20,
41 URL: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
42 HTMLURL: setting.AppURL + "user2/repo1/blob/master/" + treePath,
43 GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
44 DownloadURL: setting.AppURL + "user2/repo1/raw/branch/master/" + treePath,
45 Type: "blob",
46 Links: &api.FileLinksResponse{
47 Self: setting.AppURL + "api/v1/repos/user2/repo1/contents/" + treePath,
48 GitURL: setting.AppURL + "api/v1/repos/user2/repo1/git/blobs/" + sha,
49 HTMLURL: setting.AppURL + "user2/repo1/blob/master/" + treePath,
50 },
51 },
52 Commit: &api.FileCommitResponse{
53 CommitMeta: api.CommitMeta{
54 URL: setting.AppURL + "api/v1/repos/user2/repo1/git/commits/" + commitID,
55 SHA: commitID,
56 },
57 HTMLURL: setting.AppURL + "user2/repo1/commit/" + commitID,
58 Author: &api.CommitUser{
59 Identity: api.Identity{
60 Name: "Jane Doe",
61 Email: "janedoe@example.com",
62 },
63 },
64 Committer: &api.CommitUser{
65 Identity: api.Identity{
66 Name: "John Doe",
67 Email: "johndoe@example.com",
68 },
69 },
70 Message: "Updates README.md\n",
71 },
72 Verification: &api.PayloadCommitVerification{
73 Verified: false,
74 Reason: "unsigned",
75 Signature: "",
76 Payload: "",
77 },
78 }
79}
80
81func TestAPIUpdateFile(t *testing.T) {
82 prepareTestEnv(t)
83 user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16
84 user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3, is an org
85 user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos
86 repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo
87 repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo
88 repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
89 fileID := 0
90
91 // Get user2's token
92 session := loginUser(t, user2.Name)
93 token2 := getTokenForLoggedInUser(t, session)
94 session = emptyTestSession(t)
95 // Get user4's token
96 session = loginUser(t, user4.Name)
97 token4 := getTokenForLoggedInUser(t, session)
98 session = emptyTestSession(t)
99
100 // Test updating a file in repo1 which user2 owns, try both with branch and empty branch
101 for _, branch := range [...]string{
102 "master", // Branch
103 "", // Empty branch
104 } {
105 fileID++
106 treePath := fmt.Sprintf("update/file%d.txt", fileID)
107 createFile(user2, repo1, treePath)
108 updateFileOptions := getUpdateFileOptions()
109 updateFileOptions.BranchName = branch
110 url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
111 req := NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
112 resp := session.MakeRequest(t, req, http.StatusOK)
113 gitRepo, _ := git.OpenRepository(repo1.RepoPath())
114 commitID, _ := gitRepo.GetBranchCommitID(updateFileOptions.NewBranchName)
115 expectedFileResponse := getExpectedFileResponseForUpdate(commitID, treePath)
116 var fileResponse api.FileResponse
117 DecodeJSON(t, resp, &fileResponse)
118 assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
119 assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
120 assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
121 assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
122 assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
123 }
124
125 // Test updating a file in a new branch
126 updateFileOptions := getUpdateFileOptions()
127 updateFileOptions.BranchName = repo1.DefaultBranch
128 updateFileOptions.NewBranchName = "new_branch"
129 fileID++
130 treePath := fmt.Sprintf("update/file%d.txt", fileID)
131 createFile(user2, repo1, treePath)
132 url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
133 req := NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
134 resp := session.MakeRequest(t, req, http.StatusOK)
135 var fileResponse api.FileResponse
136 DecodeJSON(t, resp, &fileResponse)
137 expectedSHA := "08bd14b2e2852529157324de9c226b3364e76136"
138 expectedHTMLURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/blob/new_branch/update/file%d.txt", fileID)
139 expectedDownloadURL := fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/raw/branch/new_branch/update/file%d.txt", fileID)
140 assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
141 assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL)
142 assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL)
143
144 // Test updating a file and renaming it
145 updateFileOptions = getUpdateFileOptions()
146 updateFileOptions.BranchName = repo1.DefaultBranch
147 fileID++
148 treePath = fmt.Sprintf("update/file%d.txt", fileID)
149 createFile(user2, repo1, treePath)
150 updateFileOptions.FromPath = treePath
151 treePath = "rename/" + treePath
152 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
153 req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
154 resp = session.MakeRequest(t, req, http.StatusOK)
155 DecodeJSON(t, resp, &fileResponse)
156 expectedSHA = "08bd14b2e2852529157324de9c226b3364e76136"
157 expectedHTMLURL = fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/blob/master/rename/update/file%d.txt", fileID)
158 expectedDownloadURL = fmt.Sprintf("http://localhost:"+setting.HTTPPort+"/user2/repo1/raw/branch/master/rename/update/file%d.txt", fileID)
159 assert.EqualValues(t, expectedSHA, fileResponse.Content.SHA)
160 assert.EqualValues(t, expectedHTMLURL, fileResponse.Content.HTMLURL)
161 assert.EqualValues(t, expectedDownloadURL, fileResponse.Content.DownloadURL)
162
163 // Test updating a file with the wrong SHA
164 fileID++
165 treePath = fmt.Sprintf("update/file%d.txt", fileID)
166 createFile(user2, repo1, treePath)
167 updateFileOptions = getUpdateFileOptions()
168 correctSHA := updateFileOptions.SHA
169 updateFileOptions.SHA = "badsha"
170 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token2)
171 req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
172 resp = session.MakeRequest(t, req, http.StatusInternalServerError)
173 expectedAPIError := context.APIError{
174 Message: "sha does not match [given: " + updateFileOptions.SHA + ", expected: " + correctSHA + "]",
175 URL: base.DocURL,
176 }
177 var apiError context.APIError
178 DecodeJSON(t, resp, &apiError)
179 assert.Equal(t, expectedAPIError, apiError)
180
181 // Test creating a file in repo1 by user4 who does not have write access
182 fileID++
183 treePath = fmt.Sprintf("update/file%d.txt", fileID)
184 createFile(user2, repo16, treePath)
185 updateFileOptions = getUpdateFileOptions()
186 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token4)
187 req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
188 session.MakeRequest(t, req, http.StatusNotFound)
189
190 // Tests a repo with no token given so will fail
191 fileID++
192 treePath = fmt.Sprintf("update/file%d.txt", fileID)
193 createFile(user2, repo16, treePath)
194 updateFileOptions = getUpdateFileOptions()
195 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user2.Name, repo16.Name, treePath)
196 req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
197 session.MakeRequest(t, req, http.StatusNotFound)
198
199 // Test using access token for a private repo that the user of the token owns
200 fileID++
201 treePath = fmt.Sprintf("update/file%d.txt", fileID)
202 createFile(user2, repo16, treePath)
203 updateFileOptions = getUpdateFileOptions()
204 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo16.Name, treePath, token2)
205 req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
206 session.MakeRequest(t, req, http.StatusOK)
207
208 // Test using org repo "user3/repo3" where user2 is a collaborator
209 fileID++
210 treePath = fmt.Sprintf("update/file%d.txt", fileID)
211 createFile(user3, repo3, treePath)
212 updateFileOptions = getUpdateFileOptions()
213 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user3.Name, repo3.Name, treePath, token2)
214 req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
215 session.MakeRequest(t, req, http.StatusOK)
216
217 // Test using org repo "user3/repo3" with no user token
218 fileID++
219 treePath = fmt.Sprintf("update/file%d.txt", fileID)
220 createFile(user3, repo3, treePath)
221 updateFileOptions = getUpdateFileOptions()
222 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s", user3.Name, repo3.Name, treePath)
223 req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
224 session.MakeRequest(t, req, http.StatusNotFound)
225
226 // Test using repo "user2/repo1" where user4 is a NOT collaborator
227 fileID++
228 treePath = fmt.Sprintf("update/file%d.txt", fileID)
229 createFile(user2, repo1, treePath)
230 updateFileOptions = getUpdateFileOptions()
231 url = fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", user2.Name, repo1.Name, treePath, token4)
232 req = NewRequestWithJSON(t, "PUT", url, &updateFileOptions)
233 session.MakeRequest(t, req, http.StatusForbidden)
234}
integrations/api_repo_git_blobs_test.go+76−0
@@ -0,0 +1,76 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package integrations
6
7import (
8 "net/http"
9 "testing"
10
11 "code.gitea.io/gitea/models"
12 api "code.gitea.io/sdk/gitea"
13
14 "github.com/stretchr/testify/assert"
15)
16
17func TestAPIReposGitBlobs(t *testing.T) {
18 prepareTestEnv(t)
19 user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16
20 user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3
21 user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos
22 repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo
23 repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo
24 repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
25 repo1ReadmeSHA := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
26 repo3ReadmeSHA := "d56a3073c1dbb7b15963110a049d50cdb5db99fc"
27 repo16ReadmeSHA := "f90451c72ef61a7645293d17b47be7a8e983da57"
28 badSHA := "0000000000000000000000000000000000000000"
29
30 // Login as User2.
31 session := loginUser(t, user2.Name)
32 token := getTokenForLoggedInUser(t, session)
33 session = emptyTestSession(t) // don't want anyone logged in for this
34
35 // Test a public repo that anyone can GET the blob of
36 req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo1.Name, repo1ReadmeSHA)
37 resp := session.MakeRequest(t, req, http.StatusOK)
38 var gitBlobResponse api.GitBlobResponse
39 DecodeJSON(t, resp, &gitBlobResponse)
40 assert.NotNil(t, gitBlobResponse)
41 expectedContent := "Y29tbWl0IDY1ZjFiZjI3YmMzYmY3MGY2NDY1NzY1ODYzNWU2NjA5NGVkYmNiNGQKQXV0aG9yOiB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+CkRhdGU6ICAgU3VuIE1hciAxOSAxNjo0Nzo1OSAyMDE3IC0wNDAwCgogICAgSW5pdGlhbCBjb21taXQKCmRpZmYgLS1naXQgYS9SRUFETUUubWQgYi9SRUFETUUubWQKbmV3IGZpbGUgbW9kZSAxMDA2NDQKaW5kZXggMDAwMDAwMC4uNGI0ODUxYQotLS0gL2Rldi9udWxsCisrKyBiL1JFQURNRS5tZApAQCAtMCwwICsxLDMgQEAKKyMgcmVwbzEKKworRGVzY3JpcHRpb24gZm9yIHJlcG8xClwgTm8gbmV3bGluZSBhdCBlbmQgb2YgZmlsZQo="
42 assert.Equal(t, expectedContent, gitBlobResponse.Content)
43
44 // Tests a private repo with no token so will fail
45 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo16.Name, repo16ReadmeSHA)
46 session.MakeRequest(t, req, http.StatusNotFound)
47
48 // Test using access token for a private repo that the user of the token owns
49 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s?token=%s", user2.Name, repo16.Name, repo16ReadmeSHA, token)
50 session.MakeRequest(t, req, http.StatusOK)
51
52 // Test using bad sha
53 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user2.Name, repo1.Name, badSHA)
54 session.MakeRequest(t, req, http.StatusBadRequest)
55
56 // Test using org repo "user3/repo3" where user2 is a collaborator
57 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s?token=%s", user3.Name, repo3.Name, repo3ReadmeSHA, token)
58 session.MakeRequest(t, req, http.StatusOK)
59
60 // Test using org repo "user3/repo3" where user2 is a collaborator
61 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s?token=%s", user3.Name, repo3.Name, repo3ReadmeSHA, token)
62 session.MakeRequest(t, req, http.StatusOK)
63
64 // Test using org repo "user3/repo3" with no user token
65 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/%s", user3.Name, repo3ReadmeSHA, repo3.Name)
66 session.MakeRequest(t, req, http.StatusNotFound)
67
68 // Login as User4.
69 session = loginUser(t, user4.Name)
70 token4 := getTokenForLoggedInUser(t, session)
71 session = emptyTestSession(t) // don't want anyone logged in for this
72
73 // Test using org repo "user3/repo3" where user4 is a NOT collaborator
74 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/blobs/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", user3.Name, repo3.Name, token4)
75 session.MakeRequest(t, req, http.StatusNotFound)
76}
integrations/api_repo_git_trees_test.go+74−0
@@ -0,0 +1,74 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package integrations
6
7import (
8 "net/http"
9 "testing"
10
11 "code.gitea.io/gitea/models"
12)
13
14func TestAPIReposGitTrees(t *testing.T) {
15 prepareTestEnv(t)
16 user2 := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) // owner of the repo1 & repo16
17 user3 := models.AssertExistsAndLoadBean(t, &models.User{ID: 3}).(*models.User) // owner of the repo3
18 user4 := models.AssertExistsAndLoadBean(t, &models.User{ID: 4}).(*models.User) // owner of neither repos
19 repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) // public repo
20 repo3 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 3}).(*models.Repository) // public repo
21 repo16 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 16}).(*models.Repository) // private repo
22 repo1TreeSHA := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
23 repo3TreeSHA := "2a47ca4b614a9f5a43abbd5ad851a54a616ffee6"
24 repo16TreeSHA := "69554a64c1e6030f051e5c3f94bfbd773cd6a324"
25 badSHA := "0000000000000000000000000000000000000000"
26
27 // Login as User2.
28 session := loginUser(t, user2.Name)
29 token := getTokenForLoggedInUser(t, session)
30 session = emptyTestSession(t) // don't want anyone logged in for this
31
32 // Test a public repo that anyone can GET the tree of
33 for _, ref := range [...]string{
34 "master", // Branch
35 repo1TreeSHA, // Tree SHA
36 } {
37 req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user2.Name, repo1.Name, ref)
38 session.MakeRequest(t, req, http.StatusOK)
39 }
40
41 // Tests a private repo with no token so will fail
42 for _, ref := range [...]string{
43 "master", // Branch
44 repo1TreeSHA, // Tag
45 } {
46 req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user2.Name, repo16.Name, ref)
47 session.MakeRequest(t, req, http.StatusNotFound)
48 }
49
50 // Test using access token for a private repo that the user of the token owns
51 req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s?token=%s", user2.Name, repo16.Name, repo16TreeSHA, token)
52 session.MakeRequest(t, req, http.StatusOK)
53
54 // Test using bad sha
55 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user2.Name, repo1.Name, badSHA)
56 session.MakeRequest(t, req, http.StatusBadRequest)
57
58 // Test using org repo "user3/repo3" where user2 is a collaborator
59 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s?token=%s", user3.Name, repo3.Name, repo3TreeSHA, token)
60 session.MakeRequest(t, req, http.StatusOK)
61
62 // Test using org repo "user3/repo3" with no user token
63 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/%s", user3.Name, repo3TreeSHA, repo3.Name)
64 session.MakeRequest(t, req, http.StatusNotFound)
65
66 // Login as User4.
67 session = loginUser(t, user4.Name)
68 token4 := getTokenForLoggedInUser(t, session)
69 session = emptyTestSession(t) // don't want anyone logged in for this
70
71 // Test using org repo "user3/repo3" where user4 is a NOT collaborator
72 req = NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/trees/d56a3073c1dbb7b15963110a049d50cdb5db99fc?access=%s", user3.Name, repo3.Name, token4)
73 session.MakeRequest(t, req, http.StatusNotFound)
74}
integrations/integration_test.go+2−1
@@ -22,6 +22,7 @@ import ( · 21 unmodified lines
22 "testing"22 "testing"
23 23
24 "code.gitea.io/gitea/models"24 "code.gitea.io/gitea/models"
25 "code.gitea.io/gitea/modules/base"
25 "code.gitea.io/gitea/modules/setting"26 "code.gitea.io/gitea/modules/setting"
26 "code.gitea.io/gitea/routers"27 "code.gitea.io/gitea/routers"
27 "code.gitea.io/gitea/routers/routes"28 "code.gitea.io/gitea/routers/routes"
@@ -96,7 +97,7 @@ func TestMain(m *testing.M) { · 68 unmodified lines
96}97}
97 98
98func initIntegrationTest() {99func initIntegrationTest() {
99 giteaRoot := os.Getenv("GITEA_ROOT")100 giteaRoot := base.SetupGiteaRoot()
100 if giteaRoot == "" {101 if giteaRoot == "" {
101 fmt.Println("Environment variable $GITEA_ROOT not set")102 fmt.Println("Environment variable $GITEA_ROOT not set")
102 os.Exit(1)103 os.Exit(1)
integrations/migration-test/migration_test.go+2−1
@@ -18,6 +18,7 @@ import ( · 17 unmodified lines
18 "code.gitea.io/gitea/integrations"18 "code.gitea.io/gitea/integrations"
19 "code.gitea.io/gitea/models"19 "code.gitea.io/gitea/models"
20 "code.gitea.io/gitea/models/migrations"20 "code.gitea.io/gitea/models/migrations"
21 "code.gitea.io/gitea/modules/base"
21 "code.gitea.io/gitea/modules/setting"22 "code.gitea.io/gitea/modules/setting"
22 23
23 "github.com/go-xorm/xorm"24 "github.com/go-xorm/xorm"
@@ -28,7 +29,7 @@ var currentEngine *xorm.Engine · 4 unmodified lines
28 29
29func initMigrationTest(t *testing.T) {30func initMigrationTest(t *testing.T) {
30 integrations.PrintCurrentTest(t, 2)31 integrations.PrintCurrentTest(t, 2)
31 giteaRoot := os.Getenv("GITEA_ROOT")32 giteaRoot := base.SetupGiteaRoot()
32 if giteaRoot == "" {33 if giteaRoot == "" {
33 integrations.Printf("Environment variable $GITEA_ROOT not set\n")34 integrations.Printf("Environment variable $GITEA_ROOT not set\n")
34 os.Exit(1)35 os.Exit(1)
models/error.go+150−18
@@ -1,10 +1,15 @@
1// Copyright 2015 The Gogs Authors. All rights reserved.1// Copyright 2015 The Gogs Authors. All rights reserved.
2// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style3// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.4// license that can be found in the LICENSE file.
4 5
5package models6package models
6 7
7import "fmt"8import (
9 "fmt"
10
11 "code.gitea.io/gitea/modules/git"
12)
8 13
9// ErrNameReserved represents a "reserved name" error.14// ErrNameReserved represents a "reserved name" error.
10type ErrNameReserved struct {15type ErrNameReserved struct {
@@ -26,8 +31,7 @@ type ErrNamePatternNotAllowed struct { · 15 unmodified lines
26 Pattern string31 Pattern string
27}32}
28 33
29// IsErrNamePatternNotAllowed checks if an error is an34// IsErrNamePatternNotAllowed checks if an error is an ErrNamePatternNotAllowed.
30// ErrNamePatternNotAllowed.
31func IsErrNamePatternNotAllowed(err error) bool {35func IsErrNamePatternNotAllowed(err error) bool {
32 _, ok := err.(ErrNamePatternNotAllowed)36 _, ok := err.(ErrNamePatternNotAllowed)
33 return ok37 return ok
@@ -676,7 +680,7 @@ type ErrRepoRedirectNotExist struct { · 642 unmodified lines
676 RepoName string680 RepoName string
677}681}
678 682
679// IsErrRepoRedirectNotExist check if an error is an ErrRepoRedirectNotExist683// IsErrRepoRedirectNotExist check if an error is an ErrRepoRedirectNotExist.
680func IsErrRepoRedirectNotExist(err error) bool {684func IsErrRepoRedirectNotExist(err error) bool {
681 _, ok := err.(ErrRepoRedirectNotExist)685 _, ok := err.(ErrRepoRedirectNotExist)
682 return ok686 return ok
@@ -765,28 +769,95 @@ func (err ErrInvalidTagName) Error() string { · 82 unmodified lines
765 return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName)769 return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName)
766}770}
767 771
768// ErrRepoFileAlreadyExist represents a "RepoFileAlreadyExist" kind of error.772// ErrRepoFileAlreadyExists represents a "RepoFileAlreadyExist" kind of error.
769type ErrRepoFileAlreadyExist struct {773type ErrRepoFileAlreadyExists struct {
770 FileName string774 Path string
775}
776
777// IsErrRepoFileAlreadyExists checks if an error is a ErrRepoFileAlreadyExists.
778func IsErrRepoFileAlreadyExists(err error) bool {
779 _, ok := err.(ErrRepoFileAlreadyExists)
780 return ok
781}
782
783func (err ErrRepoFileAlreadyExists) Error() string {
784 return fmt.Sprintf("repository file already exists [path: %s]", err.Path)
785}
786
787// ErrRepoFileDoesNotExist represents a "RepoFileDoesNotExist" kind of error.
788type ErrRepoFileDoesNotExist struct {
789 Path string
790 Name string
791}
792
793// IsErrRepoFileDoesNotExist checks if an error is a ErrRepoDoesNotExist.
794func IsErrRepoFileDoesNotExist(err error) bool {
795 _, ok := err.(ErrRepoFileDoesNotExist)
796 return ok
797}
798
799func (err ErrRepoFileDoesNotExist) Error() string {
800 return fmt.Sprintf("repository file does not exist [path: %s]", err.Path)
801}
802
803// ErrFilenameInvalid represents a "FilenameInvalid" kind of error.
804type ErrFilenameInvalid struct {
805 Path string
806}
807
808// IsErrFilenameInvalid checks if an error is an ErrFilenameInvalid.
809func IsErrFilenameInvalid(err error) bool {
810 _, ok := err.(ErrFilenameInvalid)
811 return ok
812}
813
814func (err ErrFilenameInvalid) Error() string {
815 return fmt.Sprintf("path contains a malformed path component [path: %s]", err.Path)
816}
817
818// ErrUserCannotCommit represents "UserCannotCommit" kind of error.
819type ErrUserCannotCommit struct {
820 UserName string
821}
822
823// IsErrUserCannotCommit checks if an error is an ErrUserCannotCommit.
824func IsErrUserCannotCommit(err error) bool {
825 _, ok := err.(ErrUserCannotCommit)
826 return ok
827}
828
829func (err ErrUserCannotCommit) Error() string {
830 return fmt.Sprintf("user cannot commit to repo [user: %s]", err.UserName)
831}
832
833// ErrFilePathInvalid represents a "FilePathInvalid" kind of error.
834type ErrFilePathInvalid struct {
835 Message string
836 Path string
837 Name string
838 Type git.EntryMode
771}839}
772 840
773// IsErrRepoFileAlreadyExist checks if an error is a ErrRepoFileAlreadyExist.841// IsErrFilePathInvalid checks if an error is an ErrFilePathInvalid.
774func IsErrRepoFileAlreadyExist(err error) bool {842func IsErrFilePathInvalid(err error) bool {
775 _, ok := err.(ErrRepoFileAlreadyExist)843 _, ok := err.(ErrFilePathInvalid)
776 return ok844 return ok
777}845}
778 846
779func (err ErrRepoFileAlreadyExist) Error() string {847func (err ErrFilePathInvalid) Error() string {
780 return fmt.Sprintf("repository file already exists [file_name: %s]", err.FileName)848 if err.Message != "" {
849 return err.Message
850 }
851 return fmt.Sprintf("path is invalid [path: %s]", err.Path)
781}852}
782 853
783// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo854// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo.
784type ErrUserDoesNotHaveAccessToRepo struct {855type ErrUserDoesNotHaveAccessToRepo struct {
785 UserID int64856 UserID int64
786 RepoName string857 RepoName string
787}858}
788 859
789// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExist.860// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExists.
790func IsErrUserDoesNotHaveAccessToRepo(err error) bool {861func IsErrUserDoesNotHaveAccessToRepo(err error) bool {
791 _, ok := err.(ErrUserDoesNotHaveAccessToRepo)862 _, ok := err.(ErrUserDoesNotHaveAccessToRepo)
792 return ok863 return ok
@@ -818,7 +889,7 @@ func (err ErrBranchNotExist) Error() string { · 25 unmodified lines
818 return fmt.Sprintf("branch does not exist [name: %s]", err.Name)889 return fmt.Sprintf("branch does not exist [name: %s]", err.Name)
819}890}
820 891
821// ErrBranchAlreadyExists represents an error that branch with such name already exists892// ErrBranchAlreadyExists represents an error that branch with such name already exists.
822type ErrBranchAlreadyExists struct {893type ErrBranchAlreadyExists struct {
823 BranchName string894 BranchName string
824}895}
@@ -833,7 +904,7 @@ func (err ErrBranchAlreadyExists) Error() string { · 8 unmodified lines
833 return fmt.Sprintf("branch already exists [name: %s]", err.BranchName)904 return fmt.Sprintf("branch already exists [name: %s]", err.BranchName)
834}905}
835 906
836// ErrBranchNameConflict represents an error that branch name conflicts with other branch907// ErrBranchNameConflict represents an error that branch name conflicts with other branch.
837type ErrBranchNameConflict struct {908type ErrBranchNameConflict struct {
838 BranchName string909 BranchName string
839}910}
@@ -848,7 +919,7 @@ func (err ErrBranchNameConflict) Error() string { · 8 unmodified lines
848 return fmt.Sprintf("branch conflicts with existing branch [name: %s]", err.BranchName)919 return fmt.Sprintf("branch conflicts with existing branch [name: %s]", err.BranchName)
849}920}
850 921
851// ErrNotAllowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it922// ErrNotAllowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it.
852type ErrNotAllowedToMerge struct {923type ErrNotAllowedToMerge struct {
853 Reason string924 Reason string
854}925}
@@ -863,7 +934,7 @@ func (err ErrNotAllowedToMerge) Error() string { · 8 unmodified lines
863 return fmt.Sprintf("not allowed to merge [reason: %s]", err.Reason)934 return fmt.Sprintf("not allowed to merge [reason: %s]", err.Reason)
864}935}
865 936
866// ErrTagAlreadyExists represents an error that tag with such name already exists937// ErrTagAlreadyExists represents an error that tag with such name already exists.
867type ErrTagAlreadyExists struct {938type ErrTagAlreadyExists struct {
868 TagName string939 TagName string
869}940}
@@ -878,6 +949,67 @@ func (err ErrTagAlreadyExists) Error() string { · 8 unmodified lines
878 return fmt.Sprintf("tag already exists [name: %s]", err.TagName)949 return fmt.Sprintf("tag already exists [name: %s]", err.TagName)
879}950}
880 951
952// ErrSHADoesNotMatch represents a "SHADoesNotMatch" kind of error.
953type ErrSHADoesNotMatch struct {
954 Path string
955 GivenSHA string
956 CurrentSHA string
957}
958
959// IsErrSHADoesNotMatch checks if an error is a ErrSHADoesNotMatch.
960func IsErrSHADoesNotMatch(err error) bool {
961 _, ok := err.(ErrSHADoesNotMatch)
962 return ok
963}
964
965func (err ErrSHADoesNotMatch) Error() string {
966 return fmt.Sprintf("sha does not match [given: %s, expected: %s]", err.GivenSHA, err.CurrentSHA)
967}
968
969// ErrSHANotFound represents a "SHADoesNotMatch" kind of error.
970type ErrSHANotFound struct {
971 SHA string
972}
973
974// IsErrSHANotFound checks if an error is a ErrSHANotFound.
975func IsErrSHANotFound(err error) bool {
976 _, ok := err.(ErrSHANotFound)
977 return ok
978}
979
980func (err ErrSHANotFound) Error() string {
981 return fmt.Sprintf("sha not found [%s]", err.SHA)
982}
983
984// ErrCommitIDDoesNotMatch represents a "CommitIDDoesNotMatch" kind of error.
985type ErrCommitIDDoesNotMatch struct {
986 GivenCommitID string
987 CurrentCommitID string
988}
989
990// IsErrCommitIDDoesNotMatch checks if an error is a ErrCommitIDDoesNotMatch.
991func IsErrCommitIDDoesNotMatch(err error) bool {
992 _, ok := err.(ErrCommitIDDoesNotMatch)
993 return ok
994}
995
996func (err ErrCommitIDDoesNotMatch) Error() string {
997 return fmt.Sprintf("file CommitID does not match [given: %s, expected: %s]", err.GivenCommitID, err.CurrentCommitID)
998}
999
1000// ErrSHAOrCommitIDNotProvided represents a "SHAOrCommitIDNotProvided" kind of error.
1001type ErrSHAOrCommitIDNotProvided struct{}
1002
1003// IsErrSHAOrCommitIDNotProvided checks if an error is a ErrSHAOrCommitIDNotProvided.
1004func IsErrSHAOrCommitIDNotProvided(err error) bool {
1005 _, ok := err.(ErrSHAOrCommitIDNotProvided)
1006 return ok
1007}
1008
1009func (err ErrSHAOrCommitIDNotProvided) Error() string {
1010 return fmt.Sprintf("a SHA or commmit ID must be proved when updating a file")
1011}
1012
881// __ __ ___. .__ __1013// __ __ ___. .__ __
882// / \ / \ ____\_ |__ | |__ ____ ____ | | __1014// / \ / \ ____\_ |__ | |__ ____ ____ | | __
883// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /1015// \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /
models/unit_tests.go+2−0
@@ -14,6 +14,7 @@ import ( · 13 unmodified lines
14 "testing"14 "testing"
15 "time"15 "time"
16 16
17 "code.gitea.io/gitea/modules/base"
17 "code.gitea.io/gitea/modules/setting"18 "code.gitea.io/gitea/modules/setting"
18 19
19 "github.com/Unknwon/com"20 "github.com/Unknwon/com"
@@ -116,6 +117,7 @@ func PrepareTestEnv(t testing.TB) { · 96 unmodified lines
116 assert.NoError(t, removeAllWithRetry(setting.RepoRootPath))117 assert.NoError(t, removeAllWithRetry(setting.RepoRootPath))
117 metaPath := filepath.Join(giteaRoot, "integrations", "gitea-repositories-meta")118 metaPath := filepath.Join(giteaRoot, "integrations", "gitea-repositories-meta")
118 assert.NoError(t, com.CopyDir(metaPath, setting.RepoRootPath))119 assert.NoError(t, com.CopyDir(metaPath, setting.RepoRootPath))
120 base.SetupGiteaRoot() // Makes sure GITEA_ROOT is set
119}121}
120 122
121type testCond struct {123type testCond struct {
modules/auth/repo_form.go+1−0
@@ -590,6 +590,7 @@ type DeleteRepoFileForm struct { · 589 unmodified lines
590 CommitMessage string590 CommitMessage string
591 CommitChoice string `binding:"Required;MaxSize(50)"`591 CommitChoice string `binding:"Required;MaxSize(50)"`
592 NewBranchName string `binding:"GitRefName;MaxSize(100)"`592 NewBranchName string `binding:"GitRefName;MaxSize(100)"`
593 LastCommit string
593}594}
594 595
595// Validate validates the fields596// Validate validates the fields
modules/base/tool.go+25−0
@@ -16,7 +16,10 @@ import ( · 15 unmodified lines
16 "math"16 "math"
17 "net/http"17 "net/http"
18 "net/url"18 "net/url"
19 "os"
19 "path"20 "path"
21 "path/filepath"
22 "runtime"
20 "strconv"23 "strconv"
21 "strings"24 "strings"
22 "time"25 "time"
@@ -603,3 +606,25 @@ func EntryIcon(entry *git.TreeEntry) string { · 580 unmodified lines
603 606
604 return "file-text"607 return "file-text"
605}608}
609
610// SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value
611func SetupGiteaRoot() string {
612 giteaRoot := os.Getenv("GITEA_ROOT")
613 if giteaRoot == "" {
614 _, filename, _, _ := runtime.Caller(0)
615 giteaRoot = strings.TrimSuffix(filename, "modules/base/tool.go")
616 wd, err := os.Getwd()
617 if err != nil {
618 rel, err := filepath.Rel(giteaRoot, wd)
619 if err != nil && strings.HasPrefix(filepath.ToSlash(rel), "../") {
620 giteaRoot = wd
621 }
622 }
623 if _, err := os.Stat(filepath.Join(giteaRoot, "gitea")); os.IsNotExist(err) {
624 giteaRoot = ""
625 } else if err := os.Setenv("GITEA_ROOT", giteaRoot); err != nil {
626 giteaRoot = ""
627 }
628 }
629 return giteaRoot
630}
modules/context/repo.go+15−0
@@ -128,6 +128,21 @@ func (r *Repository) BranchNameSubURL() string { · 127 unmodified lines
128 return ""128 return ""
129}129}
130 130
131// FileExists returns true if a file exists in the given repo branch
132func (r *Repository) FileExists(path string, branch string) (bool, error) {
133 if branch == "" {
134 branch = r.Repository.DefaultBranch
135 }
136 commit, err := r.GitRepo.GetBranchCommit(branch)
137 if err != nil {
138 return false, err
139 }
140 if _, err := commit.GetTreeEntryByPath(path); err != nil {
141 return false, err
142 }
143 return true, nil
144}
145
131// GetEditorconfig returns the .editorconfig definition if found in the146// GetEditorconfig returns the .editorconfig definition if found in the
132// HEAD of the default repo branch.147// HEAD of the default repo branch.
133func (r *Repository) GetEditorconfig() (*editorconfig.Editorconfig, error) {148func (r *Repository) GetEditorconfig() (*editorconfig.Editorconfig, error) {
modules/git/blob.go+30−0
@@ -6,6 +6,7 @@ package git · 5 unmodified lines
6 6
7import (7import (
8 "bytes"8 "bytes"
9 "encoding/base64"
9 "fmt"10 "fmt"
10 "io"11 "io"
11 "io/ioutil"12 "io/ioutil"
@@ -71,3 +72,32 @@ func (b *Blob) DataAsync() (io.ReadCloser, error) { · 59 unmodified lines
71 72
72 return cmdReadCloser{stdout: stdout, cmd: cmd}, nil73 return cmdReadCloser{stdout: stdout, cmd: cmd}, nil
73}74}
75
76// GetBlobContentBase64 Reads the content of the blob with a base64 encode and returns the encoded string
77func (b *Blob) GetBlobContentBase64() (string, error) {
78 dataRc, err := b.DataAsync()
79 if err != nil {
80 return "", err
81 }
82 defer dataRc.Close()
83
84 pr, pw := io.Pipe()
85 encoder := base64.NewEncoder(base64.StdEncoding, pw)
86
87 go func() {
88 _, err := io.Copy(encoder, dataRc)
89 encoder.Close()
90
91 if err != nil {
92 pw.CloseWithError(err)
93 } else {
94 pw.Close()
95 }
96 }()
97
98 out, err := ioutil.ReadAll(pr)
99 if err != nil {
100 return "", err
101 }
102 return string(out), nil
103}
modules/git/commit.go+5−0
@@ -263,6 +263,11 @@ func (c *Commit) GetFilesChangedSinceCommit(pastCommit string) ([]string, error) · 262 unmodified lines
263 return c.repo.getFilesChanged(pastCommit, c.ID.String())263 return c.repo.getFilesChanged(pastCommit, c.ID.String())
264}264}
265 265
266// FileChangedSinceCommit Returns true if the file given has changed since the the past commit
267func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, error) {
268 return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String())
269}
270
266// GetSubModules get all the sub modules of current revision git tree271// GetSubModules get all the sub modules of current revision git tree
267func (c *Commit) GetSubModules() (*ObjectCache, error) {272func (c *Commit) GetSubModules() (*ObjectCache, error) {
268 if c.submoduleCache != nil {273 if c.submoduleCache != nil {
modules/git/repo_commit.go+11−2
@@ -10,7 +10,7 @@ import ( · 9 unmodified lines
10 "strconv"10 "strconv"
11 "strings"11 "strings"
12 12
13 version "github.com/mcuadros/go-version"13 "github.com/mcuadros/go-version"
14)14)
15 15
16// GetRefCommitID returns the last commit ID string of given reference (branch or tag).16// 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 · 253 unmodified lines
270 return repo.parsePrettyFormatLogToList(stdout)270 return repo.parsePrettyFormatLogToList(stdout)
271}271}
272 272
273func (repo *Repository) getFilesChanged(id1 string, id2 string) ([]string, error) {273func (repo *Repository) getFilesChanged(id1, id2 string) ([]string, error) {
274 stdout, err := NewCommand("diff", "--name-only", id1, id2).RunInDirBytes(repo.Path)274 stdout, err := NewCommand("diff", "--name-only", id1, id2).RunInDirBytes(repo.Path)
275 if err != nil {275 if err != nil {
276 return nil, err276 return nil, err
@@ -278,6 +278,15 @@ func (repo *Repository) getFilesChanged(id1 string, id2 string) ([]string, error · 1 unmodified line
278 return strings.Split(string(stdout), "\n"), nil278 return strings.Split(string(stdout), "\n"), nil
279}279}
280 280
281// FileChangedBetweenCommits Returns true if the file changed between commit IDs id1 and id2
282func (repo *Repository) FileChangedBetweenCommits(filename, id1, id2 string) (bool, error) {
283 stdout, err := NewCommand("diff", "--name-only", "-z", id1, id2, "--", filename).RunInDirBytes(repo.Path)
284 if err != nil {
285 return false, err
286 }
287 return len(strings.TrimSpace(string(stdout))) > 0, nil
288}
289
281// FileCommitsCount return the number of files at a revison290// FileCommitsCount return the number of files at a revison
282func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) {291func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) {
283 return commitsCount(repo.Path, revision, file)292 return commitsCount(repo.Path, revision, file)
modules/repofiles/blob.go+38−0
@@ -0,0 +1,38 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package repofiles
6
7import (
8 "code.gitea.io/gitea/models"
9 "code.gitea.io/gitea/modules/git"
10 "code.gitea.io/gitea/modules/setting"
11 api "code.gitea.io/sdk/gitea"
12)
13
14// GetBlobBySHA get the GitBlobResponse of a repository using a sha hash.
15func GetBlobBySHA(repo *models.Repository, sha string) (*api.GitBlobResponse, error) {
16 gitRepo, err := git.OpenRepository(repo.RepoPath())
17 if err != nil {
18 return nil, err
19 }
20 gitBlob, err := gitRepo.GetBlob(sha)
21 if err != nil {
22 return nil, err
23 }
24 content := ""
25 if gitBlob.Size() <= setting.API.DefaultMaxBlobSize {
26 content, err = gitBlob.GetBlobContentBase64()
27 if err != nil {
28 return nil, err
29 }
30 }
31 return &api.GitBlobResponse{
32 SHA: gitBlob.ID.String(),
33 URL: repo.APIURL() + "/git/blobs/" + gitBlob.ID.String(),
34 Size: gitBlob.Size(),
35 Encoding: "base64",
36 Content: content,
37 }, nil
38}
modules/repofiles/blob_test.go+38−0
@@ -0,0 +1,38 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package repofiles
6
7import (
8 "testing"
9
10 "code.gitea.io/gitea/models"
11 "code.gitea.io/gitea/modules/test"
12 api "code.gitea.io/sdk/gitea"
13
14 "github.com/stretchr/testify/assert"
15)
16
17func TestGetBlobBySHA(t *testing.T) {
18 models.PrepareTestEnv(t)
19 ctx := test.MockContext(t, "user2/repo1")
20 test.LoadRepo(t, ctx, 1)
21 test.LoadRepoCommit(t, ctx)
22 test.LoadUser(t, ctx, 2)
23 test.LoadGitRepo(t, ctx)
24 sha := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
25 ctx.SetParams(":id", "1")
26 ctx.SetParams(":sha", sha)
27
28 gbr, err := GetBlobBySHA(ctx.Repo.Repository, ctx.Params(":sha"))
29 expectedGBR := &api.GitBlobResponse{
30 Content: "Y29tbWl0IDY1ZjFiZjI3YmMzYmY3MGY2NDY1NzY1ODYzNWU2NjA5NGVkYmNiNGQKQXV0aG9yOiB1c2VyMSA8YWRkcmVzczFAZXhhbXBsZS5jb20+CkRhdGU6ICAgU3VuIE1hciAxOSAxNjo0Nzo1OSAyMDE3IC0wNDAwCgogICAgSW5pdGlhbCBjb21taXQKCmRpZmYgLS1naXQgYS9SRUFETUUubWQgYi9SRUFETUUubWQKbmV3IGZpbGUgbW9kZSAxMDA2NDQKaW5kZXggMDAwMDAwMC4uNGI0ODUxYQotLS0gL2Rldi9udWxsCisrKyBiL1JFQURNRS5tZApAQCAtMCwwICsxLDMgQEAKKyMgcmVwbzEKKworRGVzY3JpcHRpb24gZm9yIHJlcG8xClwgTm8gbmV3bGluZSBhdCBlbmQgb2YgZmlsZQo=",
31 Encoding: "base64",
32 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/65f1bf27bc3bf70f64657658635e66094edbcb4d",
33 SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
34 Size: 180,
35 }
36 assert.Nil(t, err)
37 assert.Equal(t, expectedGBR, gbr)
38}
modules/repofiles/content.go+73−0
@@ -0,0 +1,73 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package repofiles
6
7import (
8 "net/url"
9
10 "code.gitea.io/gitea/models"
11 "code.gitea.io/gitea/modules/git"
12 api "code.gitea.io/sdk/gitea"
13)
14
15// GetFileContents gets the meta data on a file's contents
16func GetFileContents(repo *models.Repository, treePath, ref string) (*api.FileContentResponse, error) {
17 if ref == "" {
18 ref = repo.DefaultBranch
19 }
20
21 // Check that the path given in opts.treePath is valid (not a git path)
22 treePath = CleanUploadFileName(treePath)
23 if treePath == "" {
24 return nil, models.ErrFilenameInvalid{
25 Path: treePath,
26 }
27 }
28
29 gitRepo, err := git.OpenRepository(repo.RepoPath())
30 if err != nil {
31 return nil, err
32 }
33
34 // Get the commit object for the ref
35 commit, err := gitRepo.GetCommit(ref)
36 if err != nil {
37 return nil, err
38 }
39
40 entry, err := commit.GetTreeEntryByPath(treePath)
41 if err != nil {
42 return nil, err
43 }
44
45 urlRef := ref
46 if _, err := gitRepo.GetBranchCommit(ref); err == nil {
47 urlRef = "branch/" + ref
48 }
49
50 selfURL, _ := url.Parse(repo.APIURL() + "/contents/" + treePath)
51 gitURL, _ := url.Parse(repo.APIURL() + "/git/blobs/" + entry.ID.String())
52 downloadURL, _ := url.Parse(repo.HTMLURL() + "/raw/" + urlRef + "/" + treePath)
53 htmlURL, _ := url.Parse(repo.HTMLURL() + "/blob/" + ref + "/" + treePath)
54
55 fileContent := &api.FileContentResponse{
56 Name: entry.Name(),
57 Path: treePath,
58 SHA: entry.ID.String(),
59 Size: entry.Size(),
60 URL: selfURL.String(),
61 HTMLURL: htmlURL.String(),
62 GitURL: gitURL.String(),
63 DownloadURL: downloadURL.String(),
64 Type: string(entry.Type),
65 Links: &api.FileLinksResponse{
66 Self: selfURL.String(),
67 GitURL: gitURL.String(),
68 HTMLURL: htmlURL.String(),
69 },
70 }
71
72 return fileContent, nil
73}
modules/repofiles/content_test.go+90−0
@@ -0,0 +1,90 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package repofiles
6
7import (
8 "path/filepath"
9 "testing"
10
11 "code.gitea.io/gitea/models"
12 "code.gitea.io/gitea/modules/test"
13 "code.gitea.io/sdk/gitea"
14
15 "github.com/stretchr/testify/assert"
16)
17
18func TestMain(m *testing.M) {
19 models.MainTest(m, filepath.Join("..", ".."))
20}
21
22func TestGetFileContents(t *testing.T) {
23 models.PrepareTestEnv(t)
24 ctx := test.MockContext(t, "user2/repo1")
25 ctx.SetParams(":id", "1")
26 test.LoadRepo(t, ctx, 1)
27 test.LoadRepoCommit(t, ctx)
28 test.LoadUser(t, ctx, 2)
29 test.LoadGitRepo(t, ctx)
30 treePath := "README.md"
31 ref := ctx.Repo.Repository.DefaultBranch
32
33 expectedFileContentResponse := &gitea.FileContentResponse{
34 Name: treePath,
35 Path: treePath,
36 SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
37 Size: 30,
38 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md",
39 HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/README.md",
40 GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/4b4851ad51df6a7d9f25c979345979eaeb5b349f",
41 DownloadURL: "https://try.gitea.io/user2/repo1/raw/branch/master/README.md",
42 Type: "blob",
43 Links: &gitea.FileLinksResponse{
44 Self: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md",
45 GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/4b4851ad51df6a7d9f25c979345979eaeb5b349f",
46 HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/README.md",
47 },
48 }
49
50 t.Run("Get README.md contents", func(t *testing.T) {
51 fileContentResponse, err := GetFileContents(ctx.Repo.Repository, treePath, ref)
52 assert.EqualValues(t, expectedFileContentResponse, fileContentResponse)
53 assert.Nil(t, err)
54 })
55
56 t.Run("Get REAMDE.md contents with ref as empty string (should then use the repo's default branch)", func(t *testing.T) {
57 fileContentResponse, err := GetFileContents(ctx.Repo.Repository, treePath, "")
58 assert.EqualValues(t, expectedFileContentResponse, fileContentResponse)
59 assert.Nil(t, err)
60 })
61}
62
63func TestGetFileContentsErrors(t *testing.T) {
64 models.PrepareTestEnv(t)
65 ctx := test.MockContext(t, "user2/repo1")
66 ctx.SetParams(":id", "1")
67 test.LoadRepo(t, ctx, 1)
68 test.LoadRepoCommit(t, ctx)
69 test.LoadUser(t, ctx, 2)
70 test.LoadGitRepo(t, ctx)
71 repo := ctx.Repo.Repository
72 treePath := "README.md"
73 ref := repo.DefaultBranch
74
75 t.Run("bad treePath", func(t *testing.T) {
76 badTreePath := "bad/tree.md"
77 fileContentResponse, err := GetFileContents(repo, badTreePath, ref)
78 assert.Error(t, err)
79 assert.EqualError(t, err, "object does not exist [id: , rel_path: bad]")
80 assert.Nil(t, fileContentResponse)
81 })
82
83 t.Run("bad ref", func(t *testing.T) {
84 badRef := "bad_ref"
85 fileContentResponse, err := GetFileContents(repo, treePath, badRef)
86 assert.Error(t, err)
87 assert.EqualError(t, err, "object does not exist [id: "+badRef+", rel_path: ]")
88 assert.Nil(t, fileContentResponse)
89 })
90}
modules/repofiles/delete.go+209−0
@@ -0,0 +1,209 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package repofiles
6
7import (
8 "fmt"
9 "strings"
10
11 "code.gitea.io/gitea/models"
12 "code.gitea.io/gitea/modules/git"
13 api "code.gitea.io/sdk/gitea"
14)
15
16// DeleteRepoFileOptions holds the repository delete file options
17type DeleteRepoFileOptions struct {
18 LastCommitID string
19 OldBranch string
20 NewBranch string
21 TreePath string
22 Message string
23 SHA string
24 Author *IdentityOptions
25 Committer *IdentityOptions
26}
27
28// DeleteRepoFile deletes a file in the given repository
29func DeleteRepoFile(repo *models.Repository, doer *models.User, opts *DeleteRepoFileOptions) (*api.FileResponse, error) {
30 // If no branch name is set, assume the repo's default branch
31 if opts.OldBranch == "" {
32 opts.OldBranch = repo.DefaultBranch
33 }
34 if opts.NewBranch == "" {
35 opts.NewBranch = opts.OldBranch
36 }
37
38 // oldBranch must exist for this operation
39 if _, err := repo.GetBranch(opts.OldBranch); err != nil {
40 return nil, err
41 }
42
43 // A NewBranch can be specified for the file to be created/updated in a new branch.
44 // Check to make sure the branch does not already exist, otherwise we can't proceed.
45 // If we aren't branching to a new branch, make sure user can commit to the given branch
46 if opts.NewBranch != opts.OldBranch {
47 newBranch, err := repo.GetBranch(opts.NewBranch)
48 if git.IsErrNotExist(err) {
49 return nil, err
50 }
51 if newBranch != nil {
52 return nil, models.ErrBranchAlreadyExists{
53 BranchName: opts.NewBranch,
54 }
55 }
56 } else {
57 if protected, _ := repo.IsProtectedBranchForPush(opts.OldBranch, doer); protected {
58 return nil, models.ErrUserCannotCommit{
59 UserName: doer.LowerName,
60 }
61 }
62 }
63
64 // Check that the path given in opts.treeName is valid (not a git path)
65 treePath := CleanUploadFileName(opts.TreePath)
66 if treePath == "" {
67 return nil, models.ErrFilenameInvalid{
68 Path: opts.TreePath,
69 }
70 }
71
72 message := strings.TrimSpace(opts.Message)
73
74 author, committer := GetAuthorAndCommitterUsers(opts.Committer, opts.Author, doer)
75
76 t, err := NewTemporaryUploadRepository(repo)
77 defer t.Close()
78 if err != nil {
79 return nil, err
80 }
81 if err := t.Clone(opts.OldBranch); err != nil {
82 return nil, err
83 }
84 if err := t.SetDefaultIndex(); err != nil {
85 return nil, err
86 }
87
88 // Get the commit of the original branch
89 commit, err := t.GetBranchCommit(opts.OldBranch)
90 if err != nil {
91 return nil, err // Couldn't get a commit for the branch
92 }
93
94 // Assigned LastCommitID in opts if it hasn't been set
95 if opts.LastCommitID == "" {
96 opts.LastCommitID = commit.ID.String()
97 }
98
99 // Get the files in the index
100 filesInIndex, err := t.LsFiles(opts.TreePath)
101 if err != nil {
102 return nil, fmt.Errorf("DeleteRepoFile: %v", err)
103 }
104
105 // Find the file we want to delete in the index
106 inFilelist := false
107 for _, file := range filesInIndex {
108 if file == opts.TreePath {
109 inFilelist = true
110 break
111 }
112 }
113 if !inFilelist {
114 return nil, models.ErrRepoFileDoesNotExist{
115 Path: opts.TreePath,
116 }
117 }
118
119 // Get the entry of treePath and check if the SHA given is the same as the file
120 entry, err := commit.GetTreeEntryByPath(treePath)
121 if err != nil {
122 return nil, err
123 }
124 if opts.SHA != "" {
125 // If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error
126 if opts.SHA != entry.ID.String() {
127 return nil, models.ErrSHADoesNotMatch{
128 Path: treePath,
129 GivenSHA: opts.SHA,
130 CurrentSHA: entry.ID.String(),
131 }
132 }
133 } else if opts.LastCommitID != "" {
134 // If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw
135 // an error, but only if we aren't creating a new branch.
136 if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch {
137 // CommitIDs don't match, but we don't want to throw a ErrCommitIDDoesNotMatch unless
138 // this specific file has been edited since opts.LastCommitID
139 if changed, err := commit.FileChangedSinceCommit(treePath, opts.LastCommitID); err != nil {
140 return nil, err
141 } else if changed {
142 return nil, models.ErrCommitIDDoesNotMatch{
143 GivenCommitID: opts.LastCommitID,
144 CurrentCommitID: opts.LastCommitID,
145 }
146 }
147 // The file wasn't modified, so we are good to delete it
148 }
149 } else {
150 // When deleting a file, a lastCommitID or SHA needs to be given to make sure other commits haven't been
151 // made. We throw an error if one wasn't provided.
152 return nil, models.ErrSHAOrCommitIDNotProvided{}
153 }
154
155 // Remove the file from the index
156 if err := t.RemoveFilesFromIndex(opts.TreePath); err != nil {
157 return nil, err
158 }
159
160 // Now write the tree
161 treeHash, err := t.WriteTree()
162 if err != nil {
163 return nil, err
164 }
165
166 // Now commit the tree
167 commitHash, err := t.CommitTree(author, committer, treeHash, message)
168 if err != nil {
169 return nil, err
170 }
171
172 // Then push this tree to NewBranch
173 if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
174 return nil, err
175 }
176
177 // Simulate push event.
178 oldCommitID := opts.LastCommitID
179 if opts.NewBranch != opts.OldBranch {
180 oldCommitID = git.EmptySHA
181 }
182
183 if err = repo.GetOwner(); err != nil {
184 return nil, fmt.Errorf("GetOwner: %v", err)
185 }
186 err = models.PushUpdate(
187 opts.NewBranch,
188 models.PushUpdateOptions{
189 PusherID: doer.ID,
190 PusherName: doer.Name,
191 RepoUserName: repo.Owner.Name,
192 RepoName: repo.Name,
193 RefFullName: git.BranchPrefix + opts.NewBranch,
194 OldCommitID: oldCommitID,
195 NewCommitID: commitHash,
196 },
197 )
198 if err != nil {
199 return nil, fmt.Errorf("PushUpdate: %v", err)
200 }
201
202 // FIXME: Should we UpdateRepoIndexer(repo) here?
203
204 file, err := GetFileResponseFromCommit(repo, commit, opts.NewBranch, treePath)
205 if err != nil {
206 return nil, err
207 }
208 return file, nil
209}
modules/repofiles/delete_test.go+183−0
@@ -0,0 +1,183 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package repofiles
6
7import (
8 "testing"
9
10 "code.gitea.io/gitea/models"
11 "code.gitea.io/gitea/modules/test"
12 api "code.gitea.io/sdk/gitea"
13
14 "github.com/stretchr/testify/assert"
15)
16
17func getDeleteRepoFileOptions(repo *models.Repository) *DeleteRepoFileOptions {
18 return &DeleteRepoFileOptions{
19 LastCommitID: "",
20 OldBranch: repo.DefaultBranch,
21 NewBranch: repo.DefaultBranch,
22 TreePath: "README.md",
23 Message: "Deletes README.md",
24 SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
25 Author: nil,
26 Committer: nil,
27 }
28}
29
30func getExpectedDeleteFileResponse() *api.FileResponse {
31 return &api.FileResponse{
32 Content: nil,
33 Commit: &api.FileCommitResponse{
34 CommitMeta: api.CommitMeta{
35 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d",
36 SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
37 },
38 HTMLURL: "https://try.gitea.io/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d",
39 Author: &api.CommitUser{
40 Identity: api.Identity{
41 Name: "user1",
42 Email: "address1@example.com",
43 },
44 Date: "2017-03-19T20:47:59Z",
45 },
46 Committer: &api.CommitUser{
47 Identity: api.Identity{
48 Name: "Ethan Koenig",
49 Email: "ethantkoenig@gmail.com",
50 },
51 Date: "2017-03-19T20:47:59Z",
52 },
53 Parents: []*api.CommitMeta{},
54 Message: "Initial commit\n",
55 Tree: &api.CommitMeta{
56 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/2a2f1d4670728a2e10049e345bd7a276468beab6",
57 SHA: "2a2f1d4670728a2e10049e345bd7a276468beab6",
58 },
59 },
60 Verification: &api.PayloadCommitVerification{
61 Verified: false,
62 Reason: "",
63 Signature: "",
64 Payload: "",
65 },
66 }
67}
68
69func TestDeleteRepoFile(t *testing.T) {
70 // setup
71 models.PrepareTestEnv(t)
72 ctx := test.MockContext(t, "user2/repo1")
73 ctx.SetParams(":id", "1")
74 test.LoadRepo(t, ctx, 1)
75 test.LoadRepoCommit(t, ctx)
76 test.LoadUser(t, ctx, 2)
77 test.LoadGitRepo(t, ctx)
78 repo := ctx.Repo.Repository
79 doer := ctx.User
80 opts := getDeleteRepoFileOptions(repo)
81
82 t.Run("Delete README.md file", func(t *testing.T) {
83 fileResponse, err := DeleteRepoFile(repo, doer, opts)
84 assert.Nil(t, err)
85 expectedFileResponse := getExpectedDeleteFileResponse()
86 assert.EqualValues(t, expectedFileResponse, fileResponse)
87 })
88
89 t.Run("Verify README.md has been deleted", func(t *testing.T) {
90 fileResponse, err := DeleteRepoFile(repo, doer, opts)
91 assert.Nil(t, fileResponse)
92 expectedError := "repository file does not exist [path: " + opts.TreePath + "]"
93 assert.EqualError(t, err, expectedError)
94 })
95}
96
97// Test opts with branch names removed, same results
98func TestDeleteRepoFileWithoutBranchNames(t *testing.T) {
99 // setup
100 models.PrepareTestEnv(t)
101 ctx := test.MockContext(t, "user2/repo1")
102 ctx.SetParams(":id", "1")
103 test.LoadRepo(t, ctx, 1)
104 test.LoadRepoCommit(t, ctx)
105 test.LoadUser(t, ctx, 2)
106 test.LoadGitRepo(t, ctx)
107 repo := ctx.Repo.Repository
108 doer := ctx.User
109 opts := getDeleteRepoFileOptions(repo)
110 opts.OldBranch = ""
111 opts.NewBranch = ""
112
113 t.Run("Delete README.md without Branch Name", func(t *testing.T) {
114 fileResponse, err := DeleteRepoFile(repo, doer, opts)
115 assert.Nil(t, err)
116 expectedFileResponse := getExpectedDeleteFileResponse()
117 assert.EqualValues(t, expectedFileResponse, fileResponse)
118 })
119}
120
121func TestDeleteRepoFileErrors(t *testing.T) {
122 // setup
123 models.PrepareTestEnv(t)
124 ctx := test.MockContext(t, "user2/repo1")
125 ctx.SetParams(":id", "1")
126 test.LoadRepo(t, ctx, 1)
127 test.LoadRepoCommit(t, ctx)
128 test.LoadUser(t, ctx, 2)
129 test.LoadGitRepo(t, ctx)
130 repo := ctx.Repo.Repository
131 doer := ctx.User
132
133 t.Run("Bad branch", func(t *testing.T) {
134 opts := getDeleteRepoFileOptions(repo)
135 opts.OldBranch = "bad_branch"
136 fileResponse, err := DeleteRepoFile(repo, doer, opts)
137 assert.Error(t, err)
138 assert.Nil(t, fileResponse)
139 expectedError := "branch does not exist [name: " + opts.OldBranch + "]"
140 assert.EqualError(t, err, expectedError)
141 })
142
143 t.Run("Bad SHA", func(t *testing.T) {
144 opts := getDeleteRepoFileOptions(repo)
145 origSHA := opts.SHA
146 opts.SHA = "bad_sha"
147 fileResponse, err := DeleteRepoFile(repo, doer, opts)
148 assert.Nil(t, fileResponse)
149 assert.Error(t, err)
150 expectedError := "sha does not match [given: " + opts.SHA + ", expected: " + origSHA + "]"
151 assert.EqualError(t, err, expectedError)
152 })
153
154 t.Run("New branch already exists", func(t *testing.T) {
155 opts := getDeleteRepoFileOptions(repo)
156 opts.NewBranch = "develop"
157 fileResponse, err := DeleteRepoFile(repo, doer, opts)
158 assert.Nil(t, fileResponse)
159 assert.Error(t, err)
160 expectedError := "branch already exists [name: " + opts.NewBranch + "]"
161 assert.EqualError(t, err, expectedError)
162 })
163
164 t.Run("TreePath is empty:", func(t *testing.T) {
165 opts := getDeleteRepoFileOptions(repo)
166 opts.TreePath = ""
167 fileResponse, err := DeleteRepoFile(repo, doer, opts)
168 assert.Nil(t, fileResponse)
169 assert.Error(t, err)
170 expectedError := "path contains a malformed path component [path: ]"
171 assert.EqualError(t, err, expectedError)
172 })
173
174 t.Run("TreePath is a git directory:", func(t *testing.T) {
175 opts := getDeleteRepoFileOptions(repo)
176 opts.TreePath = ".git"
177 fileResponse, err := DeleteRepoFile(repo, doer, opts)
178 assert.Nil(t, fileResponse)
179 assert.Error(t, err)
180 expectedError := "path contains a malformed path component [path: " + opts.TreePath + "]"
181 assert.EqualError(t, err, expectedError)
182 })
183}
modules/repofiles/diff.gorenamed from modules/uploader/diff.go+5−2
@@ -2,7 +2,7 @@ · 1 unmodified line
2// Use of this source code is governed by a MIT-style2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.3// license that can be found in the LICENSE file.
4 4
5package uploader5package repofiles
6 6
7import (7import (
8 "strings"8 "strings"
@@ -12,11 +12,14 @@ import ( · 3 unmodified lines
12 12
13// GetDiffPreview produces and returns diff result of a file which is not yet committed.13// GetDiffPreview produces and returns diff result of a file which is not yet committed.
14func GetDiffPreview(repo *models.Repository, branch, treePath, content string) (*models.Diff, error) {14func GetDiffPreview(repo *models.Repository, branch, treePath, content string) (*models.Diff, error) {
15 if branch == "" {
16 branch = repo.DefaultBranch
17 }
15 t, err := NewTemporaryUploadRepository(repo)18 t, err := NewTemporaryUploadRepository(repo)
16 defer t.Close()
17 if err != nil {19 if err != nil {
18 return nil, err20 return nil, err
19 }21 }
22 defer t.Close()
20 if err := t.Clone(branch); err != nil {23 if err := t.Clone(branch); err != nil {
21 return nil, err24 return nil, err
22 }25 }
modules/repofiles/diff_test.go+143−0
@@ -0,0 +1,143 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package repofiles
6
7import (
8 "testing"
9
10 "code.gitea.io/gitea/models"
11 "code.gitea.io/gitea/modules/test"
12
13 "github.com/stretchr/testify/assert"
14)
15
16func TestGetDiffPreview(t *testing.T) {
17 models.PrepareTestEnv(t)
18 ctx := test.MockContext(t, "user2/repo1")
19 ctx.SetParams(":id", "1")
20 test.LoadRepo(t, ctx, 1)
21 test.LoadRepoCommit(t, ctx)
22 test.LoadUser(t, ctx, 2)
23 test.LoadGitRepo(t, ctx)
24 branch := ctx.Repo.Repository.DefaultBranch
25 treePath := "README.md"
26 content := "# repo1\n\nDescription for repo1\nthis is a new line"
27
28 expectedDiff := &models.Diff{
29 TotalAddition: 2,
30 TotalDeletion: 1,
31 Files: []*models.DiffFile{
32 {
33 Name: "README.md",
34 OldName: "README.md",
35 Index: 1,
36 Addition: 2,
37 Deletion: 1,
38 Type: 2,
39 IsCreated: false,
40 IsDeleted: false,
41 IsBin: false,
42 IsLFSFile: false,
43 IsRenamed: false,
44 IsSubmodule: false,
45 Sections: []*models.DiffSection{
46 {
47 Name: "",
48 Lines: []*models.DiffLine{
49 {
50 LeftIdx: 0,
51 RightIdx: 0,
52 Type: 4,
53 Content: "@@ -1,3 +1,4 @@",
54 Comments: nil,
55 },
56 {
57 LeftIdx: 1,
58 RightIdx: 1,
59 Type: 1,
60 Content: " # repo1",
61 Comments: nil,
62 },
63 {
64 LeftIdx: 2,
65 RightIdx: 2,
66 Type: 1,
67 Content: " ",
68 Comments: nil,
69 },
70 {
71 LeftIdx: 3,
72 RightIdx: 0,
73 Type: 3,
74 Content: "-Description for repo1",
75 Comments: nil,
76 },
77 {
78 LeftIdx: 0,
79 RightIdx: 3,
80 Type: 2,
81 Content: "+Description for repo1",
82 Comments: nil,
83 },
84 {
85 LeftIdx: 0,
86 RightIdx: 4,
87 Type: 2,
88 Content: "+this is a new line",
89 Comments: nil,
90 },
91 },
92 },
93 },
94 IsIncomplete: false,
95 },
96 },
97 IsIncomplete: false,
98 }
99
100 t.Run("with given branch", func(t *testing.T) {
101 diff, err := GetDiffPreview(ctx.Repo.Repository, branch, treePath, content)
102 assert.Nil(t, err)
103 assert.EqualValues(t, expectedDiff, diff)
104 })
105
106 t.Run("empty branch, same results", func(t *testing.T) {
107 diff, err := GetDiffPreview(ctx.Repo.Repository, "", treePath, content)
108 assert.Nil(t, err)
109 assert.EqualValues(t, expectedDiff, diff)
110 })
111}
112
113func TestGetDiffPreviewErrors(t *testing.T) {
114 models.PrepareTestEnv(t)
115 ctx := test.MockContext(t, "user2/repo1")
116 ctx.SetParams(":id", "1")
117 test.LoadRepo(t, ctx, 1)
118 test.LoadRepoCommit(t, ctx)
119 test.LoadUser(t, ctx, 2)
120 test.LoadGitRepo(t, ctx)
121 branch := ctx.Repo.Repository.DefaultBranch
122 treePath := "README.md"
123 content := "# repo1\n\nDescription for repo1\nthis is a new line"
124
125 t.Run("empty repo", func(t *testing.T) {
126 diff, err := GetDiffPreview(&models.Repository{}, branch, treePath, content)
127 assert.Nil(t, diff)
128 assert.EqualError(t, err, "repository does not exist [id: 0, uid: 0, owner_name: , name: ]")
129 })
130
131 t.Run("bad branch", func(t *testing.T) {
132 badBranch := "bad_branch"
133 diff, err := GetDiffPreview(ctx.Repo.Repository, badBranch, treePath, content)
134 assert.Nil(t, diff)
135 assert.EqualError(t, err, "branch does not exist [name: "+badBranch+"]")
136 })
137
138 t.Run("empty treePath", func(t *testing.T) {
139 diff, err := GetDiffPreview(ctx.Repo.Repository, branch, "", content)
140 assert.Nil(t, diff)
141 assert.EqualError(t, err, "path is invalid [path: ]")
142 })
143}
modules/repofiles/file.go+125−0
@@ -0,0 +1,125 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package repofiles
6
7import (
8 "fmt"
9 "net/url"
10 "strings"
11 "time"
12
13 "code.gitea.io/gitea/models"
14 "code.gitea.io/gitea/modules/git"
15 api "code.gitea.io/sdk/gitea"
16)
17
18// GetFileResponseFromCommit Constructs a FileResponse from a Commit object
19func GetFileResponseFromCommit(repo *models.Repository, commit *git.Commit, branch, treeName string) (*api.FileResponse, error) {
20 fileContents, _ := GetFileContents(repo, treeName, branch) // ok if fails, then will be nil
21 fileCommitResponse, _ := GetFileCommitResponse(repo, commit) // ok if fails, then will be nil
22 verification := GetPayloadCommitVerification(commit)
23 fileResponse := &api.FileResponse{
24 Content: fileContents,
25 Commit: fileCommitResponse,
26 Verification: verification,
27 }
28 return fileResponse, nil
29}
30
31// GetFileCommitResponse Constructs a FileCommitResponse from a Commit object
32func GetFileCommitResponse(repo *models.Repository, commit *git.Commit) (*api.FileCommitResponse, error) {
33 if repo == nil {
34 return nil, fmt.Errorf("repo cannot be nil")
35 }
36 if commit == nil {
37 return nil, fmt.Errorf("commit cannot be nil")
38 }
39 commitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + commit.ID.String())
40 commitTreeURL, _ := url.Parse(repo.APIURL() + "/git/trees/" + commit.Tree.ID.String())
41 parents := make([]*api.CommitMeta, commit.ParentCount())
42 for i := 0; i <= commit.ParentCount(); i++ {
43 if parent, err := commit.Parent(i); err == nil && parent != nil {
44 parentCommitURL, _ := url.Parse(repo.APIURL() + "/git/commits/" + parent.ID.String())
45 parents[i] = &api.CommitMeta{
46 SHA: parent.ID.String(),
47 URL: parentCommitURL.String(),
48 }
49 }
50 }
51 commitHTMLURL, _ := url.Parse(repo.HTMLURL() + "/commit/" + commit.ID.String())
52 fileCommit := &api.FileCommitResponse{
53 CommitMeta: api.CommitMeta{
54 SHA: commit.ID.String(),
55 URL: commitURL.String(),
56 },
57 HTMLURL: commitHTMLURL.String(),
58 Author: &api.CommitUser{
59 Identity: api.Identity{
60 Name: commit.Author.Name,
61 Email: commit.Author.Email,
62 },
63 Date: commit.Author.When.UTC().Format(time.RFC3339),
64 },
65 Committer: &api.CommitUser{
66 Identity: api.Identity{
67 Name: commit.Committer.Name,
68 Email: commit.Committer.Email,
69 },
70 Date: commit.Committer.When.UTC().Format(time.RFC3339),
71 },
72 Message: commit.Message(),
73 Tree: &api.CommitMeta{
74 URL: commitTreeURL.String(),
75 SHA: commit.Tree.ID.String(),
76 },
77 Parents: parents,
78 }
79 return fileCommit, nil
80}
81
82// GetAuthorAndCommitterUsers Gets the author and committer user objects from the IdentityOptions
83func GetAuthorAndCommitterUsers(author, committer *IdentityOptions, doer *models.User) (committerUser, authorUser *models.User) {
84 // Committer and author are optional. If they are not the doer (not same email address)
85 // then we use bogus User objects for them to store their FullName and Email.
86 // If only one of the two are provided, we set both of them to it.
87 // If neither are provided, both are the doer.
88 if committer != nil && committer.Email != "" {
89 if doer != nil && strings.ToLower(doer.Email) == strings.ToLower(committer.Email) {
90 committerUser = doer // the committer is the doer, so will use their user object
91 if committer.Name != "" {
92 committerUser.FullName = committer.Name
93 }
94 } else {
95 committerUser = &models.User{
96 FullName: committer.Name,
97 Email: committer.Email,
98 }
99 }
100 }
101 if author != nil && author.Email != "" {
102 if doer != nil && strings.ToLower(doer.Email) == strings.ToLower(author.Email) {
103 authorUser = doer // the author is the doer, so will use their user object
104 if authorUser.Name != "" {
105 authorUser.FullName = author.Name
106 }
107 } else {
108 authorUser = &models.User{
109 FullName: author.Name,
110 Email: author.Email,
111 }
112 }
113 }
114 if authorUser == nil {
115 if committerUser != nil {
116 authorUser = committerUser // No valid author was given so use the committer
117 } else if doer != nil {
118 authorUser = doer // No valid author was given and no valid committer so use the doer
119 }
120 }
121 if committerUser == nil {
122 committerUser = authorUser // No valid committer so use the author as the committer (was set to a valid user above)
123 }
124 return authorUser, committerUser
125}
modules/repofiles/file_test.go+90−0
@@ -0,0 +1,90 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package repofiles
6
7import (
8 "testing"
9
10 "code.gitea.io/gitea/models"
11 "code.gitea.io/gitea/modules/git"
12 "code.gitea.io/gitea/modules/test"
13 api "code.gitea.io/sdk/gitea"
14
15 "github.com/stretchr/testify/assert"
16)
17
18func getExpectedFileResponse() *api.FileResponse {
19 return &api.FileResponse{
20 Content: &api.FileContentResponse{
21 Name: "README.md",
22 Path: "README.md",
23 SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
24 Size: 30,
25 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md",
26 HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/README.md",
27 GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/4b4851ad51df6a7d9f25c979345979eaeb5b349f",
28 DownloadURL: "https://try.gitea.io/user2/repo1/raw/branch/master/README.md",
29 Type: "blob",
30 Links: &api.FileLinksResponse{
31 Self: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md",
32 GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/4b4851ad51df6a7d9f25c979345979eaeb5b349f",
33 HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/README.md",
34 },
35 },
36 Commit: &api.FileCommitResponse{
37 CommitMeta: api.CommitMeta{
38 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d",
39 SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
40 },
41 HTMLURL: "https://try.gitea.io/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d",
42 Author: &api.CommitUser{
43 Identity: api.Identity{
44 Name: "user1",
45 Email: "address1@example.com",
46 },
47 Date: "2017-03-19T20:47:59Z",
48 },
49 Committer: &api.CommitUser{
50 Identity: api.Identity{
51 Name: "Ethan Koenig",
52 Email: "ethantkoenig@gmail.com",
53 },
54 Date: "2017-03-19T20:47:59Z",
55 },
56 Parents: []*api.CommitMeta{},
57 Message: "Initial commit\n",
58 Tree: &api.CommitMeta{
59 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/2a2f1d4670728a2e10049e345bd7a276468beab6",
60 SHA: "2a2f1d4670728a2e10049e345bd7a276468beab6",
61 },
62 },
63 Verification: &api.PayloadCommitVerification{
64 Verified: false,
65 Reason: "",
66 Signature: "",
67 Payload: "",
68 },
69 }
70}
71
72func TestGetFileResponseFromCommit(t *testing.T) {
73 models.PrepareTestEnv(t)
74 ctx := test.MockContext(t, "user2/repo1")
75 ctx.SetParams(":id", "1")
76 test.LoadRepo(t, ctx, 1)
77 test.LoadRepoCommit(t, ctx)
78 test.LoadUser(t, ctx, 2)
79 test.LoadGitRepo(t, ctx)
80 repo := ctx.Repo.Repository
81 branch := repo.DefaultBranch
82 treePath := "README.md"
83 gitRepo, _ := git.OpenRepository(repo.RepoPath())
84 commit, _ := gitRepo.GetBranchCommit(branch)
85 expectedFileResponse := getExpectedFileResponse()
86
87 fileResponse, err := GetFileResponseFromCommit(repo, commit, branch, treePath)
88 assert.Nil(t, err)
89 assert.EqualValues(t, expectedFileResponse, fileResponse)
90}
modules/repofiles/repofiles.go+23−0
@@ -0,0 +1,23 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.package repofiles
4
5package repofiles
6
7import (
8 "path"
9 "strings"
10)
11
12// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory
13func CleanUploadFileName(name string) string {
14 // Rebase the filename
15 name = strings.Trim(path.Clean("/"+name), " /")
16 // Git disallows any filenames to have a .git directory in them.
17 for _, part := range strings.Split(name, "/") {
18 if strings.ToLower(part) == ".git" {
19 return ""
20 }
21 }
22 return name
23}
modules/repofiles/repofiles_test.go+27−0
@@ -0,0 +1,27 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package repofiles
6
7import (
8 "testing"
9
10 "github.com/stretchr/testify/assert"
11)
12
13func TestCleanUploadFileName(t *testing.T) {
14 t.Run("Clean regular file", func(t *testing.T) {
15 name := "this/is/test"
16 cleanName := CleanUploadFileName(name)
17 expectedCleanName := name
18 assert.EqualValues(t, expectedCleanName, cleanName)
19 })
20
21 t.Run("Clean a .git path", func(t *testing.T) {
22 name := "this/is/test/.git"
23 cleanName := CleanUploadFileName(name)
24 expectedCleanName := ""
25 assert.EqualValues(t, expectedCleanName, cleanName)
26 })
27}
modules/repofiles/temp_repo.gorenamed from modules/uploader/repo.go+76−10
@@ -2,7 +2,7 @@ · 1 unmodified line
2// Use of this source code is governed by a MIT-style2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.3// license that can be found in the LICENSE file.
4 4
5package uploader5package repofiles
6 6
7import (7import (
8 "bytes"8 "bytes"
@@ -12,19 +12,22 @@ import ( · 3 unmodified lines
12 "os"12 "os"
13 "os/exec"13 "os/exec"
14 "path"14 "path"
15 "regexp"
15 "strings"16 "strings"
16 "time"17 "time"
17 18
18 "code.gitea.io/gitea/models"19 "code.gitea.io/gitea/models"
20 "code.gitea.io/gitea/modules/git"
19 "code.gitea.io/gitea/modules/process"21 "code.gitea.io/gitea/modules/process"
20 "code.gitea.io/gitea/modules/setting"22 "code.gitea.io/gitea/modules/setting"
21 23
22 "github.com/Unknwon/com"24 "github.com/Unknwon/com"
23)25)
24 26
25// TemporaryUploadRepository is a type to wrap our upload repositories27// TemporaryUploadRepository is a type to wrap our upload repositories as a shallow clone
26type TemporaryUploadRepository struct {28type TemporaryUploadRepository struct {
27 repo *models.Repository29 repo *models.Repository
30 gitRepo *git.Repository
28 basePath string31 basePath string
29}32}
30 33
@@ -33,7 +36,10 @@ func NewTemporaryUploadRepository(repo *models.Repository) (*TemporaryUploadRepo · 2 unmodified lines
33 timeStr := com.ToStr(time.Now().Nanosecond()) // SHOULD USE SOMETHING UNIQUE36 timeStr := com.ToStr(time.Now().Nanosecond()) // SHOULD USE SOMETHING UNIQUE
34 basePath := path.Join(models.LocalCopyPath(), "upload-"+timeStr+".git")37 basePath := path.Join(models.LocalCopyPath(), "upload-"+timeStr+".git")
35 if err := os.MkdirAll(path.Dir(basePath), os.ModePerm); err != nil {38 if err := os.MkdirAll(path.Dir(basePath), os.ModePerm); err != nil {
36 return nil, fmt.Errorf("Failed to create dir %s: %v", basePath, err)39 return nil, fmt.Errorf("failed to create dir %s: %v", basePath, err)
40 }
41 if repo.RepoPath() == "" {
42 return nil, fmt.Errorf("no path to repository on system")
37 }43 }
38 t := &TemporaryUploadRepository{repo: repo, basePath: basePath}44 t := &TemporaryUploadRepository{repo: repo, basePath: basePath}
39 return t, nil45 return t, nil
@@ -51,8 +57,26 @@ func (t *TemporaryUploadRepository) Clone(branch string) error { · 11 unmodified lines
51 if _, stderr, err := process.GetManager().ExecTimeout(5*time.Minute,57 if _, stderr, err := process.GetManager().ExecTimeout(5*time.Minute,
52 fmt.Sprintf("Clone (git clone -s --bare): %s", t.basePath),58 fmt.Sprintf("Clone (git clone -s --bare): %s", t.basePath),
53 "git", "clone", "-s", "--bare", "-b", branch, t.repo.RepoPath(), t.basePath); err != nil {59 "git", "clone", "-s", "--bare", "-b", branch, t.repo.RepoPath(), t.basePath); err != nil {
54 return fmt.Errorf("Clone: %v %s", err, stderr)60 if matched, _ := regexp.MatchString(".*Remote branch .* not found in upstream origin.*", stderr); matched {
61 return models.ErrBranchNotExist{
62 Name: branch,
63 }
64 } else if matched, _ := regexp.MatchString(".* repository .* does not exist.*", stderr); matched {
65 return models.ErrRepoNotExist{
66 ID: t.repo.ID,
67 UID: t.repo.OwnerID,
68 OwnerName: t.repo.OwnerName,
69 Name: t.repo.Name,
70 }
71 } else {
72 return fmt.Errorf("Clone: %v %s", err, stderr)
73 }
74 }
75 gitRepo, err := git.OpenRepository(t.basePath)
76 if err != nil {
77 return err
55 }78 }
79 t.gitRepo = gitRepo
56 return nil80 return nil
57}81}
58 82
@@ -186,6 +210,12 @@ func (t *TemporaryUploadRepository) AddObjectToIndex(mode, objectHash, objectPat · 127 unmodified lines
186 t.basePath,210 t.basePath,
187 fmt.Sprintf("addObjectToIndex (git update-index): %s", t.basePath),211 fmt.Sprintf("addObjectToIndex (git update-index): %s", t.basePath),
188 "git", "update-index", "--add", "--replace", "--cacheinfo", mode, objectHash, objectPath); err != nil {212 "git", "update-index", "--add", "--replace", "--cacheinfo", mode, objectHash, objectPath); err != nil {
213 if matched, _ := regexp.MatchString(".*Invalid path '.*", stderr); matched {
214 return models.ErrFilePathInvalid{
215 Message: objectPath,
216 Path: objectPath,
217 }
218 }
189 return fmt.Errorf("git update-index: %s", stderr)219 return fmt.Errorf("git update-index: %s", stderr)
190 }220 }
191 return nil221 return nil
@@ -201,22 +231,42 @@ func (t *TemporaryUploadRepository) WriteTree() (string, error) { · 9 unmodified lines
201 return "", fmt.Errorf("git write-tree: %s", stderr)231 return "", fmt.Errorf("git write-tree: %s", stderr)
202 }232 }
203 return strings.TrimSpace(treeHash), nil233 return strings.TrimSpace(treeHash), nil
234}
204 235
236// GetLastCommit gets the last commit ID SHA of the repo
237func (t *TemporaryUploadRepository) GetLastCommit() (string, error) {
238 return t.GetLastCommitByRef("HEAD")
239}
240
241// GetLastCommitByRef gets the last commit ID SHA of the repo by ref
242func (t *TemporaryUploadRepository) GetLastCommitByRef(ref string) (string, error) {
243 if ref == "" {
244 ref = "HEAD"
245 }
246 treeHash, stderr, err := process.GetManager().ExecDir(5*time.Minute,
247 t.basePath,
248 fmt.Sprintf("GetLastCommit (git rev-parse %s): %s", ref, t.basePath),
249 "git", "rev-parse", ref)
250 if err != nil {
251 return "", fmt.Errorf("git rev-parse %s: %s", ref, stderr)
252 }
253 return strings.TrimSpace(treeHash), nil
205}254}
206 255
207// CommitTree creates a commit from a given tree for the user with provided message256// CommitTree creates a commit from a given tree for the user with provided message
208func (t *TemporaryUploadRepository) CommitTree(doer *models.User, treeHash string, message string) (string, error) {257func (t *TemporaryUploadRepository) CommitTree(author, committer *models.User, treeHash string, message string) (string, error) {
209 commitTimeStr := time.Now().Format(time.UnixDate)258 commitTimeStr := time.Now().Format(time.UnixDate)
210 sig := doer.NewGitSig()259 authorSig := author.NewGitSig()
260 committerSig := committer.NewGitSig()
211 261
212 // FIXME: Should we add SSH_ORIGINAL_COMMAND to this262 // FIXME: Should we add SSH_ORIGINAL_COMMAND to this
213 // Because this may call hooks we should pass in the environment263 // Because this may call hooks we should pass in the environment
214 env := append(os.Environ(),264 env := append(os.Environ(),
215 "GIT_AUTHOR_NAME="+sig.Name,265 "GIT_AUTHOR_NAME="+authorSig.Name,
216 "GIT_AUTHOR_EMAIL="+sig.Email,266 "GIT_AUTHOR_EMAIL="+authorSig.Email,
217 "GIT_AUTHOR_DATE="+commitTimeStr,267 "GIT_AUTHOR_DATE="+commitTimeStr,
218 "GIT_COMMITTER_NAME="+sig.Name,268 "GIT_COMMITTER_NAME="+committerSig.Name,
219 "GIT_COMMITTER_EMAIL="+sig.Email,269 "GIT_COMMITTER_EMAIL="+committerSig.Email,
220 "GIT_COMMITTER_DATE="+commitTimeStr,270 "GIT_COMMITTER_DATE="+commitTimeStr,
221 )271 )
222 commitHash, stderr, err := process.GetManager().ExecDirEnv(5*time.Minute,272 commitHash, stderr, err := process.GetManager().ExecDirEnv(5*time.Minute,
@@ -357,3 +407,19 @@ func (t *TemporaryUploadRepository) CheckAttribute(attribute string, args ...str · 134 unmodified lines
357 407
358 return name2attribute2info, err408 return name2attribute2info, err
359}409}
410
411// GetBranchCommit Gets the commit object of the given branch
412func (t *TemporaryUploadRepository) GetBranchCommit(branch string) (*git.Commit, error) {
413 if t.gitRepo == nil {
414 return nil, fmt.Errorf("repository has not been cloned")
415 }
416 return t.gitRepo.GetBranchCommit(branch)
417}
418
419// GetCommit Gets the commit object of the given commit ID
420func (t *TemporaryUploadRepository) GetCommit(commitID string) (*git.Commit, error) {
421 if t.gitRepo == nil {
422 return nil, fmt.Errorf("repository has not been cloned")
423 }
424 return t.gitRepo.GetCommit(commitID)
425}
modules/repofiles/tree.go+92−0
@@ -0,0 +1,92 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package repofiles
6
7import (
8 "fmt"
9
10 "code.gitea.io/gitea/models"
11 "code.gitea.io/gitea/modules/git"
12 "code.gitea.io/gitea/modules/setting"
13 api "code.gitea.io/sdk/gitea"
14)
15
16// GetTreeBySHA get the GitTreeResponse of a repository using a sha hash.
17func GetTreeBySHA(repo *models.Repository, sha string, page, perPage int, recursive bool) (*api.GitTreeResponse, error) {
18 gitRepo, err := git.OpenRepository(repo.RepoPath())
19 gitTree, err := gitRepo.GetTree(sha)
20 if err != nil || gitTree == nil {
21 return nil, models.ErrSHANotFound{
22 SHA: sha,
23 }
24 }
25 tree := new(api.GitTreeResponse)
26 tree.SHA = gitTree.ID.String()
27 tree.URL = repo.APIURL() + "/git/trees/" + tree.SHA
28 var entries git.Entries
29 if recursive {
30 entries, err = gitTree.ListEntriesRecursive()
31 } else {
32 entries, err = gitTree.ListEntries()
33 }
34 if err != nil {
35 return nil, err
36 }
37 apiURL := repo.APIURL()
38 apiURLLen := len(apiURL)
39
40 // 51 is len(sha1) + len("/git/blobs/"). 40 + 11.
41 blobURL := make([]byte, apiURLLen+51)
42 copy(blobURL[:], apiURL)
43 copy(blobURL[apiURLLen:], "/git/blobs/")
44
45 // 51 is len(sha1) + len("/git/trees/"). 40 + 11.
46 treeURL := make([]byte, apiURLLen+51)
47 copy(treeURL[:], apiURL)
48 copy(treeURL[apiURLLen:], "/git/trees/")
49
50 // 40 is the size of the sha1 hash in hexadecimal format.
51 copyPos := len(treeURL) - 40
52
53 if perPage <= 0 || perPage > setting.API.DefaultGitTreesPerPage {
54 perPage = setting.API.DefaultGitTreesPerPage
55 }
56 if page <= 0 {
57 page = 1
58 }
59 tree.Page = page
60 tree.TotalCount = len(entries)
61 rangeStart := perPage * (page - 1)
62 if rangeStart >= len(entries) {
63 return tree, nil
64 }
65 var rangeEnd int
66 if len(entries) > perPage {
67 tree.Truncated = true
68 }
69 if rangeStart+perPage < len(entries) {
70 rangeEnd = rangeStart + perPage
71 } else {
72 rangeEnd = len(entries)
73 }
74 tree.Entries = make([]api.GitEntry, rangeEnd-rangeStart)
75 for e := rangeStart; e < rangeEnd; e++ {
76 i := e - rangeStart
77 tree.Entries[i].Path = entries[e].Name()
78 tree.Entries[i].Mode = fmt.Sprintf("%06x", entries[e].Mode())
79 tree.Entries[i].Type = string(entries[e].Type)
80 tree.Entries[i].Size = entries[e].Size()
81 tree.Entries[i].SHA = entries[e].ID.String()
82
83 if entries[e].IsDir() {
84 copy(treeURL[copyPos:], entries[e].ID.String())
85 tree.Entries[i].URL = string(treeURL[:])
86 } else {
87 copy(blobURL[copyPos:], entries[e].ID.String())
88 tree.Entries[i].URL = string(blobURL[:])
89 }
90 }
91 return tree, nil
92}
modules/repofiles/tree_test.gorenamed from routers/api/v1/repo/tree_test.go+13−11
@@ -2,34 +2,37 @@ · 1 unmodified line
2// Use of this source code is governed by a MIT-style2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.3// license that can be found in the LICENSE file.
4 4
5package repo5package repofiles
6 6
7import (7import (
8 "github.com/stretchr/testify/assert"
9 "testing"8 "testing"
10 9
11 "code.gitea.io/gitea/models"10 "code.gitea.io/gitea/models"
12 "code.gitea.io/gitea/modules/context"
13 "code.gitea.io/gitea/modules/test"11 "code.gitea.io/gitea/modules/test"
14 "code.gitea.io/sdk/gitea"12 api "code.gitea.io/sdk/gitea"
13
14 "github.com/stretchr/testify/assert"
15)15)
16 16
17func TestGetTreeBySHA(t *testing.T) {17func TestGetTreeBySHA(t *testing.T) {
18 models.PrepareTestEnv(t)18 models.PrepareTestEnv(t)
19 sha := "master"
20 ctx := test.MockContext(t, "user2/repo1")19 ctx := test.MockContext(t, "user2/repo1")
21 ctx.SetParams(":id", "1")
22 ctx.SetParams(":sha", sha)
23 test.LoadRepo(t, ctx, 1)20 test.LoadRepo(t, ctx, 1)
24 test.LoadRepoCommit(t, ctx)21 test.LoadRepoCommit(t, ctx)
25 test.LoadUser(t, ctx, 2)22 test.LoadUser(t, ctx, 2)
26 test.LoadGitRepo(t, ctx)23 test.LoadGitRepo(t, ctx)
24 sha := ctx.Repo.Repository.DefaultBranch
25 page := 1
26 perPage := 10
27 ctx.SetParams(":id", "1")
28 ctx.SetParams(":sha", sha)
27 29
28 tree := GetTreeBySHA(&context.APIContext{Context: ctx, Org: nil}, ctx.Params("sha"))30 tree, err := GetTreeBySHA(ctx.Repo.Repository, ctx.Params(":sha"), page, perPage, true)
29 expectedTree := &gitea.GitTreeResponse{31 assert.Nil(t, err)
32 expectedTree := &api.GitTreeResponse{
30 SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",33 SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
31 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/65f1bf27bc3bf70f64657658635e66094edbcb4d",34 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/65f1bf27bc3bf70f64657658635e66094edbcb4d",
32 Entries: []gitea.GitEntry{35 Entries: []api.GitEntry{
33 {36 {
34 Path: "README.md",37 Path: "README.md",
35 Mode: "100644",38 Mode: "100644",
@@ -43,6 +46,5 @@ func TestGetTreeBySHA(t *testing.T) { · 7 unmodified lines
43 Page: 1,46 Page: 1,
44 TotalCount: 1,47 TotalCount: 1,
45 }48 }
46
47 assert.EqualValues(t, tree, expectedTree)49 assert.EqualValues(t, tree, expectedTree)
48}50}
modules/repofiles/update.go+331−0
@@ -0,0 +1,331 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package repofiles
6
7import (
8 "fmt"
9 "path"
10 "strings"
11
12 "code.gitea.io/gitea/models"
13 "code.gitea.io/gitea/modules/git"
14 "code.gitea.io/gitea/modules/lfs"
15 "code.gitea.io/gitea/modules/setting"
16 "code.gitea.io/sdk/gitea"
17)
18
19// IdentityOptions for a person's identity like an author or committer
20type IdentityOptions struct {
21 Name string
22 Email string
23}
24
25// UpdateRepoFileOptions holds the repository file update options
26type UpdateRepoFileOptions struct {
27 LastCommitID string
28 OldBranch string
29 NewBranch string
30 TreePath string
31 FromTreePath string
32 Message string
33 Content string
34 SHA string
35 IsNewFile bool
36 Author *IdentityOptions
37 Committer *IdentityOptions
38}
39
40// CreateOrUpdateRepoFile adds or updates a file in the given repository
41func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *UpdateRepoFileOptions) (*gitea.FileResponse, error) {
42 // If no branch name is set, assume master
43 if opts.OldBranch == "" {
44 opts.OldBranch = repo.DefaultBranch
45 }
46 if opts.NewBranch == "" {
47 opts.NewBranch = opts.OldBranch
48 }
49
50 // oldBranch must exist for this operation
51 if _, err := repo.GetBranch(opts.OldBranch); err != nil {
52 return nil, err
53 }
54
55 // A NewBranch can be specified for the file to be created/updated in a new branch.
56 // Check to make sure the branch does not already exist, otherwise we can't proceed.
57 // If we aren't branching to a new branch, make sure user can commit to the given branch
58 if opts.NewBranch != opts.OldBranch {
59 existingBranch, err := repo.GetBranch(opts.NewBranch)
60 if existingBranch != nil {
61 return nil, models.ErrBranchAlreadyExists{
62 BranchName: opts.NewBranch,
63 }
64 }
65 if err != nil && !models.IsErrBranchNotExist(err) {
66 return nil, err
67 }
68 } else {
69 if protected, _ := repo.IsProtectedBranchForPush(opts.OldBranch, doer); protected {
70 return nil, models.ErrUserCannotCommit{UserName: doer.LowerName}
71 }
72 }
73
74 // If FromTreePath is not set, set it to the opts.TreePath
75 if opts.TreePath != "" && opts.FromTreePath == "" {
76 opts.FromTreePath = opts.TreePath
77 }
78
79 // Check that the path given in opts.treePath is valid (not a git path)
80 treePath := CleanUploadFileName(opts.TreePath)
81 if treePath == "" {
82 return nil, models.ErrFilenameInvalid{
83 Path: opts.TreePath,
84 }
85 }
86 // If there is a fromTreePath (we are copying it), also clean it up
87 fromTreePath := CleanUploadFileName(opts.FromTreePath)
88 if fromTreePath == "" && opts.FromTreePath != "" {
89 return nil, models.ErrFilenameInvalid{
90 Path: opts.FromTreePath,
91 }
92 }
93
94 message := strings.TrimSpace(opts.Message)
95
96 author, committer := GetAuthorAndCommitterUsers(opts.Committer, opts.Author, doer)
97
98 t, err := NewTemporaryUploadRepository(repo)
99 defer t.Close()
100 if err != nil {
101 return nil, err
102 }
103 if err := t.Clone(opts.OldBranch); err != nil {
104 return nil, err
105 }
106 if err := t.SetDefaultIndex(); err != nil {
107 return nil, err
108 }
109
110 // Get the commit of the original branch
111 commit, err := t.GetBranchCommit(opts.OldBranch)
112 if err != nil {
113 return nil, err // Couldn't get a commit for the branch
114 }
115
116 // Assigned LastCommitID in opts if it hasn't been set
117 if opts.LastCommitID == "" {
118 opts.LastCommitID = commit.ID.String()
119 }
120
121 if !opts.IsNewFile {
122 fromEntry, err := commit.GetTreeEntryByPath(fromTreePath)
123 if err != nil {
124 return nil, err
125 }
126 if opts.SHA != "" {
127 // If a SHA was given and the SHA given doesn't match the SHA of the fromTreePath, throw error
128 if opts.SHA != fromEntry.ID.String() {
129 return nil, models.ErrSHADoesNotMatch{
130 Path: treePath,
131 GivenSHA: opts.SHA,
132 CurrentSHA: fromEntry.ID.String(),
133 }
134 }
135 } else if opts.LastCommitID != "" {
136 // If a lastCommitID was given and it doesn't match the commitID of the head of the branch throw
137 // an error, but only if we aren't creating a new branch.
138 if commit.ID.String() != opts.LastCommitID && opts.OldBranch == opts.NewBranch {
139 if changed, err := commit.FileChangedSinceCommit(treePath, opts.LastCommitID); err != nil {
140 return nil, err
141 } else if changed {
142 return nil, models.ErrCommitIDDoesNotMatch{
143 GivenCommitID: opts.LastCommitID,
144 CurrentCommitID: opts.LastCommitID,
145 }
146 }
147 // The file wasn't modified, so we are good to delete it
148 }
149 } else {
150 // When updating a file, a lastCommitID or SHA needs to be given to make sure other commits
151 // haven't been made. We throw an error if one wasn't provided.
152 return nil, models.ErrSHAOrCommitIDNotProvided{}
153 }
154 }
155
156 // For the path where this file will be created/updated, we need to make
157 // sure no parts of the path are existing files or links except for the last
158 // item in the path which is the file name, and that shouldn't exist IF it is
159 // a new file OR is being moved to a new path.
160 treePathParts := strings.Split(treePath, "/")
161 subTreePath := ""
162 for index, part := range treePathParts {
163 subTreePath = path.Join(subTreePath, part)
164 entry, err := commit.GetTreeEntryByPath(subTreePath)
165 if err != nil {
166 if git.IsErrNotExist(err) {
167 // Means there is no item with that name, so we're good
168 break
169 }
170 return nil, err
171 }
172 if index < len(treePathParts)-1 {
173 if !entry.IsDir() {
174 return nil, models.ErrFilePathInvalid{
175 Message: fmt.Sprintf("a file exists where you’re trying to create a subdirectory [path: %s]", subTreePath),
176 Path: subTreePath,
177 Name: part,
178 Type: git.EntryModeBlob,
179 }
180 }
181 } else if entry.IsLink() {
182 return nil, models.ErrFilePathInvalid{
183 Message: fmt.Sprintf("a symbolic link exists where you’re trying to create a subdirectory [path: %s]", subTreePath),
184 Path: subTreePath,
185 Name: part,
186 Type: git.EntryModeSymlink,
187 }
188 } else if entry.IsDir() {
189 return nil, models.ErrFilePathInvalid{
190 Message: fmt.Sprintf("a directory exists where you’re trying to create a file [path: %s]", subTreePath),
191 Path: subTreePath,
192 Name: part,
193 Type: git.EntryModeTree,
194 }
195 } else if fromTreePath != treePath || opts.IsNewFile {
196 // The entry shouldn't exist if we are creating new file or moving to a new path
197 return nil, models.ErrRepoFileAlreadyExists{
198 Path: treePath,
199 }
200 }
201
202 }
203
204 // Get the two paths (might be the same if not moving) from the index if they exist
205 filesInIndex, err := t.LsFiles(opts.TreePath, opts.FromTreePath)
206 if err != nil {
207 return nil, fmt.Errorf("UpdateRepoFile: %v", err)
208 }
209 // If is a new file (not updating) then the given path shouldn't exist
210 if opts.IsNewFile {
211 for _, file := range filesInIndex {
212 if file == opts.TreePath {
213 return nil, models.ErrRepoFileAlreadyExists{
214 Path: opts.TreePath,
215 }
216 }
217 }
218 }
219
220 // Remove the old path from the tree
221 if fromTreePath != treePath && len(filesInIndex) > 0 {
222 for _, file := range filesInIndex {
223 if file == fromTreePath {
224 if err := t.RemoveFilesFromIndex(opts.FromTreePath); err != nil {
225 return nil, err
226 }
227 }
228 }
229 }
230
231 // Check there is no way this can return multiple infos
232 filename2attribute2info, err := t.CheckAttribute("filter", treePath)
233 if err != nil {
234 return nil, err
235 }
236
237 content := opts.Content
238 var lfsMetaObject *models.LFSMetaObject
239
240 if filename2attribute2info[treePath] != nil && filename2attribute2info[treePath]["filter"] == "lfs" {
241 // OK so we are supposed to LFS this data!
242 oid, err := models.GenerateLFSOid(strings.NewReader(opts.Content))
243 if err != nil {
244 return nil, err
245 }
246 lfsMetaObject = &models.LFSMetaObject{Oid: oid, Size: int64(len(opts.Content)), RepositoryID: repo.ID}
247 content = lfsMetaObject.Pointer()
248 }
249
250 // Add the object to the database
251 objectHash, err := t.HashObject(strings.NewReader(content))
252 if err != nil {
253 return nil, err
254 }
255
256 // Add the object to the index
257 if err := t.AddObjectToIndex("100644", objectHash, treePath); err != nil {
258 return nil, err
259 }
260
261 // Now write the tree
262 treeHash, err := t.WriteTree()
263 if err != nil {
264 return nil, err
265 }
266
267 // Now commit the tree
268 commitHash, err := t.CommitTree(author, committer, treeHash, message)
269 if err != nil {
270 return nil, err
271 }
272
273 if lfsMetaObject != nil {
274 // We have an LFS object - create it
275 lfsMetaObject, err = models.NewLFSMetaObject(lfsMetaObject)
276 if err != nil {
277 return nil, err
278 }
279 contentStore := &lfs.ContentStore{BasePath: setting.LFS.ContentPath}
280 if !contentStore.Exists(lfsMetaObject) {
281 if err := contentStore.Put(lfsMetaObject, strings.NewReader(opts.Content)); err != nil {
282 if err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil {
283 return nil, fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err)
284 }
285 return nil, err
286 }
287 }
288 }
289
290 // Then push this tree to NewBranch
291 if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
292 return nil, err
293 }
294
295 // Simulate push event.
296 oldCommitID := opts.LastCommitID
297 if opts.NewBranch != opts.OldBranch || oldCommitID == "" {
298 oldCommitID = git.EmptySHA
299 }
300
301 if err = repo.GetOwner(); err != nil {
302 return nil, fmt.Errorf("GetOwner: %v", err)
303 }
304 err = models.PushUpdate(
305 opts.NewBranch,
306 models.PushUpdateOptions{
307 PusherID: doer.ID,
308 PusherName: doer.Name,
309 RepoUserName: repo.Owner.Name,
310 RepoName: repo.Name,
311 RefFullName: git.BranchPrefix + opts.NewBranch,
312 OldCommitID: oldCommitID,
313 NewCommitID: commitHash,
314 },
315 )
316 if err != nil {
317 return nil, fmt.Errorf("PushUpdate: %v", err)
318 }
319 models.UpdateRepoIndexer(repo)
320
321 commit, err = t.GetCommit(commitHash)
322 if err != nil {
323 return nil, err
324 }
325
326 file, err := GetFileResponseFromCommit(repo, commit, opts.NewBranch, treePath)
327 if err != nil {
328 return nil, err
329 }
330 return file, nil
331}
modules/repofiles/update_test.go+357−0
@@ -0,0 +1,357 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package repofiles
6
7import (
8 "testing"
9 "time"
10
11 "code.gitea.io/gitea/models"
12 "code.gitea.io/gitea/modules/git"
13 "code.gitea.io/gitea/modules/test"
14 api "code.gitea.io/sdk/gitea"
15
16 "github.com/stretchr/testify/assert"
17)
18
19func getCreateRepoFileOptions(repo *models.Repository) *UpdateRepoFileOptions {
20 return &UpdateRepoFileOptions{
21 OldBranch: repo.DefaultBranch,
22 NewBranch: repo.DefaultBranch,
23 TreePath: "new/file.txt",
24 Message: "Creates new/file.txt",
25 Content: "This is a NEW file",
26 IsNewFile: true,
27 Author: nil,
28 Committer: nil,
29 }
30}
31
32func getUpdateRepoFileOptions(repo *models.Repository) *UpdateRepoFileOptions {
33 return &UpdateRepoFileOptions{
34 OldBranch: repo.DefaultBranch,
35 NewBranch: repo.DefaultBranch,
36 TreePath: "README.md",
37 Message: "Updates README.md",
38 SHA: "4b4851ad51df6a7d9f25c979345979eaeb5b349f",
39 Content: "This is UPDATED content for the README file",
40 IsNewFile: false,
41 Author: nil,
42 Committer: nil,
43 }
44}
45
46func getExpectedFileResponseForCreate(commitID string) *api.FileResponse {
47 return &api.FileResponse{
48 Content: &api.FileContentResponse{
49 Name: "file.txt",
50 Path: "new/file.txt",
51 SHA: "103ff9234cefeee5ec5361d22b49fbb04d385885",
52 Size: 18,
53 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/new/file.txt",
54 HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/new/file.txt",
55 GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/103ff9234cefeee5ec5361d22b49fbb04d385885",
56 DownloadURL: "https://try.gitea.io/user2/repo1/raw/branch/master/new/file.txt",
57 Type: "blob",
58 Links: &api.FileLinksResponse{
59 Self: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/new/file.txt",
60 GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/103ff9234cefeee5ec5361d22b49fbb04d385885",
61 HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/new/file.txt",
62 },
63 },
64 Commit: &api.FileCommitResponse{
65 CommitMeta: api.CommitMeta{
66 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/commits/" + commitID,
67 SHA: commitID,
68 },
69 HTMLURL: "https://try.gitea.io/user2/repo1/commit/" + commitID,
70 Author: &api.CommitUser{
71 Identity: api.Identity{
72 Name: "User Two",
73 Email: "user2@",
74 },
75 Date: time.Now().UTC().Format(time.RFC3339),
76 },
77 Committer: &api.CommitUser{
78 Identity: api.Identity{
79 Name: "User Two",
80 Email: "user2@",
81 },
82 Date: time.Now().UTC().Format(time.RFC3339),
83 },
84 Parents: []*api.CommitMeta{
85 {
86 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d",
87 SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
88 },
89 },
90 Message: "Updates README.md\n",
91 Tree: &api.CommitMeta{
92 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/f93e3a1a1525fb5b91020da86e44810c87a2d7bc",
93 SHA: "f93e3a1a1525fb5b91020git dda86e44810c87a2d7bc",
94 },
95 },
96 Verification: &api.PayloadCommitVerification{
97 Verified: false,
98 Reason: "unsigned",
99 Signature: "",
100 Payload: "",
101 },
102 }
103}
104
105func getExpectedFileResponseForUpdate(commitID string) *api.FileResponse {
106 return &api.FileResponse{
107 Content: &api.FileContentResponse{
108 Name: "README.md",
109 Path: "README.md",
110 SHA: "dbf8d00e022e05b7e5cf7e535de857de57925647",
111 Size: 43,
112 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md",
113 HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/README.md",
114 GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/dbf8d00e022e05b7e5cf7e535de857de57925647",
115 DownloadURL: "https://try.gitea.io/user2/repo1/raw/branch/master/README.md",
116 Type: "blob",
117 Links: &api.FileLinksResponse{
118 Self: "https://try.gitea.io/api/v1/repos/user2/repo1/contents/README.md",
119 GitURL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/blobs/dbf8d00e022e05b7e5cf7e535de857de57925647",
120 HTMLURL: "https://try.gitea.io/user2/repo1/blob/master/README.md",
121 },
122 },
123 Commit: &api.FileCommitResponse{
124 CommitMeta: api.CommitMeta{
125 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/commits/" + commitID,
126 SHA: commitID,
127 },
128 HTMLURL: "https://try.gitea.io/user2/repo1/commit/" + commitID,
129 Author: &api.CommitUser{
130 Identity: api.Identity{
131 Name: "User Two",
132 Email: "user2@",
133 },
134 Date: time.Now().UTC().Format(time.RFC3339),
135 },
136 Committer: &api.CommitUser{
137 Identity: api.Identity{
138 Name: "User Two",
139 Email: "user2@",
140 },
141 Date: time.Now().UTC().Format(time.RFC3339),
142 },
143 Parents: []*api.CommitMeta{
144 {
145 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/commits/65f1bf27bc3bf70f64657658635e66094edbcb4d",
146 SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
147 },
148 },
149 Message: "Updates README.md\n",
150 Tree: &api.CommitMeta{
151 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/f93e3a1a1525fb5b91020da86e44810c87a2d7bc",
152 SHA: "f93e3a1a1525fb5b91020da86e44810c87a2d7bc",
153 },
154 },
155 Verification: &api.PayloadCommitVerification{
156 Verified: false,
157 Reason: "unsigned",
158 Signature: "",
159 Payload: "",
160 },
161 }
162}
163
164func TestCreateOrUpdateRepoFileForCreate(t *testing.T) {
165 // setup
166 models.PrepareTestEnv(t)
167 ctx := test.MockContext(t, "user2/repo1")
168 ctx.SetParams(":id", "1")
169 test.LoadRepo(t, ctx, 1)
170 test.LoadRepoCommit(t, ctx)
171 test.LoadUser(t, ctx, 2)
172 test.LoadGitRepo(t, ctx)
173 repo := ctx.Repo.Repository
174 doer := ctx.User
175 opts := getCreateRepoFileOptions(repo)
176
177 // test
178 fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts)
179
180 // asserts
181 assert.Nil(t, err)
182 gitRepo, _ := git.OpenRepository(repo.RepoPath())
183 commitID, _ := gitRepo.GetBranchCommitID(opts.NewBranch)
184 expectedFileResponse := getExpectedFileResponseForCreate(commitID)
185 assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
186 assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
187 assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
188 assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
189 assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
190}
191
192func TestCreateOrUpdateRepoFileForUpdate(t *testing.T) {
193 // setup
194 models.PrepareTestEnv(t)
195 ctx := test.MockContext(t, "user2/repo1")
196 ctx.SetParams(":id", "1")
197 test.LoadRepo(t, ctx, 1)
198 test.LoadRepoCommit(t, ctx)
199 test.LoadUser(t, ctx, 2)
200 test.LoadGitRepo(t, ctx)
201 repo := ctx.Repo.Repository
202 doer := ctx.User
203 opts := getUpdateRepoFileOptions(repo)
204
205 // test
206 fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts)
207
208 // asserts
209 assert.Nil(t, err)
210 gitRepo, _ := git.OpenRepository(repo.RepoPath())
211 commitID, _ := gitRepo.GetBranchCommitID(opts.NewBranch)
212 expectedFileResponse := getExpectedFileResponseForUpdate(commitID)
213 assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
214 assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
215 assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
216 assert.EqualValues(t, expectedFileResponse.Commit.Author.Email, fileResponse.Commit.Author.Email)
217 assert.EqualValues(t, expectedFileResponse.Commit.Author.Name, fileResponse.Commit.Author.Name)
218}
219
220func TestCreateOrUpdateRepoFileForUpdateWithFileMove(t *testing.T) {
221 // setup
222 models.PrepareTestEnv(t)
223 ctx := test.MockContext(t, "user2/repo1")
224 ctx.SetParams(":id", "1")
225 test.LoadRepo(t, ctx, 1)
226 test.LoadRepoCommit(t, ctx)
227 test.LoadUser(t, ctx, 2)
228 test.LoadGitRepo(t, ctx)
229 repo := ctx.Repo.Repository
230 doer := ctx.User
231 opts := getUpdateRepoFileOptions(repo)
232 suffix := "_new"
233 opts.FromTreePath = "README.md"
234 opts.TreePath = "README.md" + suffix // new file name, README.md_new
235
236 // test
237 fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts)
238
239 // asserts
240 assert.Nil(t, err)
241 gitRepo, _ := git.OpenRepository(repo.RepoPath())
242 commit, _ := gitRepo.GetBranchCommit(opts.NewBranch)
243 expectedFileResponse := getExpectedFileResponseForUpdate(commit.ID.String())
244 // assert that the old file no longer exists in the last commit of the branch
245 fromEntry, err := commit.GetTreeEntryByPath(opts.FromTreePath)
246 toEntry, err := commit.GetTreeEntryByPath(opts.TreePath)
247 assert.Nil(t, fromEntry) // Should no longer exist here
248 assert.NotNil(t, toEntry) // Should exist here
249 // assert SHA has remained the same but paths use the new file name
250 assert.EqualValues(t, expectedFileResponse.Content.SHA, fileResponse.Content.SHA)
251 assert.EqualValues(t, expectedFileResponse.Content.Name+suffix, fileResponse.Content.Name)
252 assert.EqualValues(t, expectedFileResponse.Content.Path+suffix, fileResponse.Content.Path)
253 assert.EqualValues(t, expectedFileResponse.Content.URL+suffix, fileResponse.Content.URL)
254 assert.EqualValues(t, expectedFileResponse.Commit.SHA, fileResponse.Commit.SHA)
255 assert.EqualValues(t, expectedFileResponse.Commit.HTMLURL, fileResponse.Commit.HTMLURL)
256}
257
258// Test opts with branch names removed, should get same results as above test
259func TestCreateOrUpdateRepoFileWithoutBranchNames(t *testing.T) {
260 // setup
261 models.PrepareTestEnv(t)
262 ctx := test.MockContext(t, "user2/repo1")
263 ctx.SetParams(":id", "1")
264 test.LoadRepo(t, ctx, 1)
265 test.LoadRepoCommit(t, ctx)
266 test.LoadUser(t, ctx, 2)
267 test.LoadGitRepo(t, ctx)
268 repo := ctx.Repo.Repository
269 doer := ctx.User
270 opts := getUpdateRepoFileOptions(repo)
271 opts.OldBranch = ""
272 opts.NewBranch = ""
273
274 // test
275 fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts)
276
277 // asserts
278 assert.Nil(t, err)
279 gitRepo, _ := git.OpenRepository(repo.RepoPath())
280 commitID, _ := gitRepo.GetBranchCommitID(repo.DefaultBranch)
281 expectedFileResponse := getExpectedFileResponseForUpdate(commitID)
282 assert.EqualValues(t, expectedFileResponse.Content, fileResponse.Content)
283}
284
285func TestCreateOrUpdateRepoFileErrors(t *testing.T) {
286 // setup
287 models.PrepareTestEnv(t)
288 ctx := test.MockContext(t, "user2/repo1")
289 ctx.SetParams(":id", "1")
290 test.LoadRepo(t, ctx, 1)
291 test.LoadRepoCommit(t, ctx)
292 test.LoadUser(t, ctx, 2)
293 test.LoadGitRepo(t, ctx)
294 repo := ctx.Repo.Repository
295 doer := ctx.User
296
297 t.Run("bad branch", func(t *testing.T) {
298 opts := getUpdateRepoFileOptions(repo)
299 opts.OldBranch = "bad_branch"
300 fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts)
301 assert.Error(t, err)
302 assert.Nil(t, fileResponse)
303 expectedError := "branch does not exist [name: " + opts.OldBranch + "]"
304 assert.EqualError(t, err, expectedError)
305 })
306
307 t.Run("bad SHA", func(t *testing.T) {
308 opts := getUpdateRepoFileOptions(repo)
309 origSHA := opts.SHA
310 opts.SHA = "bad_sha"
311 fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts)
312 assert.Nil(t, fileResponse)
313 assert.Error(t, err)
314 expectedError := "sha does not match [given: " + opts.SHA + ", expected: " + origSHA + "]"
315 assert.EqualError(t, err, expectedError)
316 })
317
318 t.Run("new branch already exists", func(t *testing.T) {
319 opts := getUpdateRepoFileOptions(repo)
320 opts.NewBranch = "develop"
321 fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts)
322 assert.Nil(t, fileResponse)
323 assert.Error(t, err)
324 expectedError := "branch already exists [name: " + opts.NewBranch + "]"
325 assert.EqualError(t, err, expectedError)
326 })
327
328 t.Run("treePath is empty:", func(t *testing.T) {
329 opts := getUpdateRepoFileOptions(repo)
330 opts.TreePath = ""
331 fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts)
332 assert.Nil(t, fileResponse)
333 assert.Error(t, err)
334 expectedError := "path contains a malformed path component [path: ]"
335 assert.EqualError(t, err, expectedError)
336 })
337
338 t.Run("treePath is a git directory:", func(t *testing.T) {
339 opts := getUpdateRepoFileOptions(repo)
340 opts.TreePath = ".git"
341 fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts)
342 assert.Nil(t, fileResponse)
343 assert.Error(t, err)
344 expectedError := "path contains a malformed path component [path: " + opts.TreePath + "]"
345 assert.EqualError(t, err, expectedError)
346 })
347
348 t.Run("create file that already exists", func(t *testing.T) {
349 opts := getCreateRepoFileOptions(repo)
350 opts.TreePath = "README.md" //already exists
351 fileResponse, err := CreateOrUpdateRepoFile(repo, doer, opts)
352 assert.Nil(t, fileResponse)
353 assert.Error(t, err)
354 expectedError := "repository file already exists [path: " + opts.TreePath + "]"
355 assert.EqualError(t, err, expectedError)
356 })
357}
modules/repofiles/upload.gorenamed from modules/uploader/upload.go+6−2
@@ -2,7 +2,7 @@ · 1 unmodified line
2// Use of this source code is governed by a MIT-style2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.3// license that can be found in the LICENSE file.
4 4
5package uploader5package repofiles
6 6
7import (7import (
8 "fmt"8 "fmt"
@@ -127,8 +127,12 @@ func UploadRepoFiles(repo *models.Repository, doer *models.User, opts *UploadRep · 118 unmodified lines
127 return err127 return err
128 }128 }
129 129
130 // make author and committer the doer
131 author := doer
132 committer := doer
133
130 // Now commit the tree134 // Now commit the tree
131 commitHash, err := t.CommitTree(doer, treeHash, opts.Message)135 commitHash, err := t.CommitTree(author, committer, treeHash, opts.Message)
132 if err != nil {136 if err != nil {
133 return err137 return err
134 }138 }
modules/repofiles/verification.go+29−0
@@ -0,0 +1,29 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package repofiles
6
7import (
8 "code.gitea.io/gitea/models"
9 "code.gitea.io/gitea/modules/git"
10 "code.gitea.io/sdk/gitea"
11)
12
13// GetPayloadCommitVerification returns the verification information of a commit
14func GetPayloadCommitVerification(commit *git.Commit) *gitea.PayloadCommitVerification {
15 verification := &gitea.PayloadCommitVerification{}
16 commitVerification := models.ParseCommitWithSignature(commit)
17 if commit.Signature != nil {
18 verification.Signature = commit.Signature.Signature
19 verification.Payload = commit.Signature.Payload
20 }
21 if verification.Reason != "" {
22 verification.Reason = commitVerification.Reason
23 } else {
24 if verification.Verified {
25 verification.Reason = "unsigned"
26 }
27 }
28 return verification
29}
modules/setting/setting.go+2−0
@@ -293,11 +293,13 @@ var ( · 292 unmodified lines
293 MaxResponseItems int293 MaxResponseItems int
294 DefaultPagingNum int294 DefaultPagingNum int
295 DefaultGitTreesPerPage int295 DefaultGitTreesPerPage int
296 DefaultMaxBlobSize int64
296 }{297 }{
297 EnableSwagger: true,298 EnableSwagger: true,
298 MaxResponseItems: 50,299 MaxResponseItems: 50,
299 DefaultPagingNum: 30,300 DefaultPagingNum: 30,
300 DefaultGitTreesPerPage: 1000,301 DefaultGitTreesPerPage: 1000,
302 DefaultMaxBlobSize: 10485760,
301 }303 }
302 304
303 OAuth2 = struct {305 OAuth2 = struct {
modules/uploader/delete.go+0−100
@@ -1,100 +0,0 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package uploader
6
7import (
8 "fmt"
9
10 "code.gitea.io/gitea/models"
11 "code.gitea.io/gitea/modules/git"
12)
13
14// DeleteRepoFileOptions holds the repository delete file options
15type DeleteRepoFileOptions struct {
16 LastCommitID string
17 OldBranch string
18 NewBranch string
19 TreePath string
20 Message string
21}
22
23// DeleteRepoFile deletes a file in the given repository
24func DeleteRepoFile(repo *models.Repository, doer *models.User, opts *DeleteRepoFileOptions) error {
25 t, err := NewTemporaryUploadRepository(repo)
26 defer t.Close()
27 if err != nil {
28 return err
29 }
30 if err := t.Clone(opts.OldBranch); err != nil {
31 return err
32 }
33 if err := t.SetDefaultIndex(); err != nil {
34 return err
35 }
36
37 filesInIndex, err := t.LsFiles(opts.TreePath)
38 if err != nil {
39 return fmt.Errorf("UpdateRepoFile: %v", err)
40 }
41
42 inFilelist := false
43 for _, file := range filesInIndex {
44 if file == opts.TreePath {
45 inFilelist = true
46 }
47 }
48 if !inFilelist {
49 return git.ErrNotExist{RelPath: opts.TreePath}
50 }
51
52 if err := t.RemoveFilesFromIndex(opts.TreePath); err != nil {
53 return err
54 }
55
56 // Now write the tree
57 treeHash, err := t.WriteTree()
58 if err != nil {
59 return err
60 }
61
62 // Now commit the tree
63 commitHash, err := t.CommitTree(doer, treeHash, opts.Message)
64 if err != nil {
65 return err
66 }
67
68 // Then push this tree to NewBranch
69 if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
70 return err
71 }
72
73 // Simulate push event.
74 oldCommitID := opts.LastCommitID
75 if opts.NewBranch != opts.OldBranch {
76 oldCommitID = git.EmptySHA
77 }
78
79 if err = repo.GetOwner(); err != nil {
80 return fmt.Errorf("GetOwner: %v", err)
81 }
82 err = models.PushUpdate(
83 opts.NewBranch,
84 models.PushUpdateOptions{
85 PusherID: doer.ID,
86 PusherName: doer.Name,
87 RepoUserName: repo.Owner.Name,
88 RepoName: repo.Name,
89 RefFullName: git.BranchPrefix + opts.NewBranch,
90 OldCommitID: oldCommitID,
91 NewCommitID: commitHash,
92 },
93 )
94 if err != nil {
95 return fmt.Errorf("PushUpdate: %v", err)
96 }
97
98 // FIXME: Should we UpdateRepoIndexer(repo) here?
99 return nil
100}
modules/uploader/update.go+0−159
@@ -1,159 +0,0 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package uploader
6
7import (
8 "fmt"
9 "strings"
10
11 "code.gitea.io/gitea/models"
12 "code.gitea.io/gitea/modules/git"
13 "code.gitea.io/gitea/modules/lfs"
14 "code.gitea.io/gitea/modules/setting"
15)
16
17// UpdateRepoFileOptions holds the repository file update options
18type UpdateRepoFileOptions struct {
19 LastCommitID string
20 OldBranch string
21 NewBranch string
22 OldTreeName string
23 NewTreeName string
24 Message string
25 Content string
26 IsNewFile bool
27}
28
29// UpdateRepoFile adds or updates a file in the given repository
30func UpdateRepoFile(repo *models.Repository, doer *models.User, opts *UpdateRepoFileOptions) error {
31 t, err := NewTemporaryUploadRepository(repo)
32 defer t.Close()
33 if err != nil {
34 return err
35 }
36 if err := t.Clone(opts.OldBranch); err != nil {
37 return err
38 }
39 if err := t.SetDefaultIndex(); err != nil {
40 return err
41 }
42
43 filesInIndex, err := t.LsFiles(opts.NewTreeName, opts.OldTreeName)
44 if err != nil {
45 return fmt.Errorf("UpdateRepoFile: %v", err)
46 }
47
48 if opts.IsNewFile {
49 for _, file := range filesInIndex {
50 if file == opts.NewTreeName {
51 return models.ErrRepoFileAlreadyExist{FileName: opts.NewTreeName}
52 }
53 }
54 }
55
56 //var stdout string
57 if opts.OldTreeName != opts.NewTreeName && len(filesInIndex) > 0 {
58 for _, file := range filesInIndex {
59 if file == opts.OldTreeName {
60 if err := t.RemoveFilesFromIndex(opts.OldTreeName); err != nil {
61 return err
62 }
63 }
64 }
65
66 }
67
68 // Check there is no way this can return multiple infos
69 filename2attribute2info, err := t.CheckAttribute("filter", opts.NewTreeName)
70 if err != nil {
71 return err
72 }
73
74 content := opts.Content
75 var lfsMetaObject *models.LFSMetaObject
76
77 if filename2attribute2info[opts.NewTreeName] != nil && filename2attribute2info[opts.NewTreeName]["filter"] == "lfs" {
78 // OK so we are supposed to LFS this data!
79 oid, err := models.GenerateLFSOid(strings.NewReader(opts.Content))
80 if err != nil {
81 return err
82 }
83 lfsMetaObject = &models.LFSMetaObject{Oid: oid, Size: int64(len(opts.Content)), RepositoryID: repo.ID}
84 content = lfsMetaObject.Pointer()
85 }
86
87 // Add the object to the database
88 objectHash, err := t.HashObject(strings.NewReader(content))
89 if err != nil {
90 return err
91 }
92
93 // Add the object to the index
94 if err := t.AddObjectToIndex("100644", objectHash, opts.NewTreeName); err != nil {
95 return err
96 }
97
98 // Now write the tree
99 treeHash, err := t.WriteTree()
100 if err != nil {
101 return err
102 }
103
104 // Now commit the tree
105 commitHash, err := t.CommitTree(doer, treeHash, opts.Message)
106 if err != nil {
107 return err
108 }
109
110 if lfsMetaObject != nil {
111 // We have an LFS object - create it
112 lfsMetaObject, err = models.NewLFSMetaObject(lfsMetaObject)
113 if err != nil {
114 return err
115 }
116 contentStore := &lfs.ContentStore{BasePath: setting.LFS.ContentPath}
117 if !contentStore.Exists(lfsMetaObject) {
118 if err := contentStore.Put(lfsMetaObject, strings.NewReader(opts.Content)); err != nil {
119 if err2 := repo.RemoveLFSMetaObjectByOid(lfsMetaObject.Oid); err2 != nil {
120 return fmt.Errorf("Error whilst removing failed inserted LFS object %s: %v (Prev Error: %v)", lfsMetaObject.Oid, err2, err)
121 }
122 return err
123 }
124 }
125 }
126
127 // Then push this tree to NewBranch
128 if err := t.Push(doer, commitHash, opts.NewBranch); err != nil {
129 return err
130 }
131
132 // Simulate push event.
133 oldCommitID := opts.LastCommitID
134 if opts.NewBranch != opts.OldBranch {
135 oldCommitID = git.EmptySHA
136 }
137
138 if err = repo.GetOwner(); err != nil {
139 return fmt.Errorf("GetOwner: %v", err)
140 }
141 err = models.PushUpdate(
142 opts.NewBranch,
143 models.PushUpdateOptions{
144 PusherID: doer.ID,
145 PusherName: doer.Name,
146 RepoUserName: repo.Owner.Name,
147 RepoName: repo.Name,
148 RefFullName: git.BranchPrefix + opts.NewBranch,
149 OldCommitID: oldCommitID,
150 NewCommitID: commitHash,
151 },
152 )
153 if err != nil {
154 return fmt.Errorf("PushUpdate: %v", err)
155 }
156 models.UpdateRepoIndexer(repo)
157
158 return nil
159}
options/locale/locale_en-US.ini+4−1
@@ -667,7 +667,7 @@ editor.filename_help = Add a directory by typing its name followed by a slash (' · 666 unmodified lines
667editor.or = or667editor.or = or
668editor.cancel_lower = Cancel668editor.cancel_lower = Cancel
669editor.commit_changes = Commit Changes669editor.commit_changes = Commit Changes
670editor.add_tmpl = Add '%s/<filename>'670editor.add_tmpl = Add '<filename>'
671editor.add = Add '%s'671editor.add = Add '%s'
672editor.update = Update '%s'672editor.update = Update '%s'
673editor.delete = Delete '%s'673editor.delete = Delete '%s'
@@ -677,11 +677,14 @@ editor.create_new_branch = Create a <strong>new branch</strong> for this commit · 3 unmodified lines
677editor.new_branch_name_desc = New branch name…677editor.new_branch_name_desc = New branch name…
678editor.cancel = Cancel678editor.cancel = Cancel
679editor.filename_cannot_be_empty = The filename cannot be empty.679editor.filename_cannot_be_empty = The filename cannot be empty.
680editor.filename_is_invalid = The filename is invalid: '%s'.
681editor.branch_does_not_exist = Branch '%s' does not exist in this repository.
680editor.branch_already_exists = Branch '%s' already exists in this repository.682editor.branch_already_exists = Branch '%s' already exists in this repository.
681editor.directory_is_a_file = Directory name '%s' is already used as a filename in this repository.683editor.directory_is_a_file = Directory name '%s' is already used as a filename in this repository.
682editor.file_is_a_symlink = '%s' is a symbolic link. Symbolic links cannot be edited in the web editor684editor.file_is_a_symlink = '%s' is a symbolic link. Symbolic links cannot be edited in the web editor
683editor.filename_is_a_directory = Filename '%s' is already used as a directory name in this repository.685editor.filename_is_a_directory = Filename '%s' is already used as a directory name in this repository.
684editor.file_editing_no_longer_exists = The file being edited, '%s', no longer exists in this repository.686editor.file_editing_no_longer_exists = The file being edited, '%s', no longer exists in this repository.
687editor.file_deleting_no_longer_exists = The file being deleted, '%s', no longer exists in this repository.
685editor.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.688editor.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.
686editor.file_already_exists = A file named '%s' already exists in this repository.689editor.file_already_exists = A file named '%s' already exists in this repository.
687editor.no_changes_to_show = There are no changes to show.690editor.no_changes_to_show = There are no changes to show.
routers/api/v1/api.go+10−1
@@ -659,7 +659,16 @@ func RegisterRoutes(m *macaron.Macaron) { · 658 unmodified lines
659 })659 })
660 m.Get("/refs", repo.GetGitAllRefs)660 m.Get("/refs", repo.GetGitAllRefs)
661 m.Get("/refs/*", repo.GetGitRefs)661 m.Get("/refs/*", repo.GetGitRefs)
662 m.Combo("/trees/:sha", context.RepoRef()).Get(repo.GetTree)662 m.Get("/trees/:sha", context.RepoRef(), repo.GetTree)
663 m.Get("/blobs/:sha", context.RepoRef(), repo.GetBlob)
664 }, reqRepoReader(models.UnitTypeCode))
665 m.Group("/contents", func() {
666 m.Get("/*", repo.GetFileContents)
667 m.Group("/*", func() {
668 m.Post("", bind(api.CreateFileOptions{}), repo.CreateFile)
669 m.Put("", bind(api.UpdateFileOptions{}), repo.UpdateFile)
670 m.Delete("", bind(api.DeleteFileOptions{}), repo.DeleteFile)
671 }, reqRepoWriter(models.UnitTypeCode), reqToken())
663 }, reqRepoReader(models.UnitTypeCode))672 }, reqRepoReader(models.UnitTypeCode))
664 }, repoAssignment())673 }, repoAssignment())
665 })674 })
routers/api/v1/repo/blob.go+51−0
@@ -0,0 +1,51 @@
1// Copyright 2019 The Gitea Authors. All rights reserved.
2// Use of this source code is governed by a MIT-style
3// license that can be found in the LICENSE file.
4
5package repo
6
7import (
8 "net/http"
9
10 "code.gitea.io/gitea/modules/context"
11 "code.gitea.io/gitea/modules/repofiles"
12)
13
14// GetBlob get the blob of a repository file.
15func GetBlob(ctx *context.APIContext) {
16 // swagger:operation GET /repos/{owner}/{repo}/git/blobs/{sha} repository GetBlob
17 // ---
18 // summary: Gets the blob of a repository.
19 // produces:
20 // - application/json
21 // parameters:
22 // - name: owner
23 // in: path
24 // description: owner of the repo
25 // type: string
26 // required: true
27 // - name: repo
28 // in: path
29 // description: name of the repo
30 // type: string
31 // required: true
32 // - name: sha
33 // in: path
34 // description: sha of the commit
35 // type: string
36 // required: true
37 // responses:
38 // "200":
39 // "$ref": "#/responses/GitBlobResponse"
40
41 sha := ctx.Params("sha")
42 if len(sha) == 0 {
43 ctx.Error(http.StatusBadRequest, "", "sha not provided")
44 return
45 }
46 if blob, err := repofiles.GetBlobBySHA(ctx.Repo.Repository, sha); err != nil {
47 ctx.Error(http.StatusBadRequest, "", err)
48 } else {
49 ctx.JSON(http.StatusOK, blob)
50 }
51}
routers/api/v1/repo/file.go+269−5
@@ -6,10 +6,15 @@ · 5 unmodified lines
6package repo6package repo
7 7
8import (8import (
9 "encoding/base64"
10 "net/http"
11
9 "code.gitea.io/gitea/models"12 "code.gitea.io/gitea/models"
10 "code.gitea.io/gitea/modules/context"13 "code.gitea.io/gitea/modules/context"
11 "code.gitea.io/gitea/modules/git"14 "code.gitea.io/gitea/modules/git"
15 "code.gitea.io/gitea/modules/repofiles"
12 "code.gitea.io/gitea/routers/repo"16 "code.gitea.io/gitea/routers/repo"
17 api "code.gitea.io/sdk/gitea"
13)18)
14 19
15// GetRawFile get a file by path on a repository20// GetRawFile get a file by path on a repository
@@ -48,12 +53,12 @@ func GetRawFile(ctx *context.APIContext) { · 32 unmodified lines
48 if git.IsErrNotExist(err) {53 if git.IsErrNotExist(err) {
49 ctx.NotFound()54 ctx.NotFound()
50 } else {55 } else {
51 ctx.Error(500, "GetBlobByPath", err)56 ctx.Error(http.StatusInternalServerError, "GetBlobByPath", err)
52 }57 }
53 return58 return
54 }59 }
55 if err = repo.ServeBlob(ctx.Context, blob); err != nil {60 if err = repo.ServeBlob(ctx.Context, blob); err != nil {
56 ctx.Error(500, "ServeBlob", err)61 ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
57 }62 }
58}63}
59 64
@@ -86,7 +91,7 @@ func GetArchive(ctx *context.APIContext) { · 26 unmodified lines
86 repoPath := models.RepoPath(ctx.Params(":username"), ctx.Params(":reponame"))91 repoPath := models.RepoPath(ctx.Params(":username"), ctx.Params(":reponame"))
87 gitRepo, err := git.OpenRepository(repoPath)92 gitRepo, err := git.OpenRepository(repoPath)
88 if err != nil {93 if err != nil {
89 ctx.Error(500, "OpenRepository", err)94 ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
90 return95 return
91 }96 }
92 ctx.Repo.GitRepo = gitRepo97 ctx.Repo.GitRepo = gitRepo
@@ -125,7 +130,7 @@ func GetEditorconfig(ctx *context.APIContext) { · 32 unmodified lines
125 if git.IsErrNotExist(err) {130 if git.IsErrNotExist(err) {
126 ctx.NotFound(err)131 ctx.NotFound(err)
127 } else {132 } else {
128 ctx.Error(500, "GetEditorconfig", err)133 ctx.Error(http.StatusInternalServerError, "GetEditorconfig", err)
129 }134 }
130 return135 return
131 }136 }
@@ -136,5 +141,264 @@ func GetEditorconfig(ctx *context.APIContext) { · 4 unmodified lines
136 ctx.NotFound(err)141 ctx.NotFound(err)
137 return142 return
138 }143 }
139 ctx.JSON(200, def)144 ctx.JSON(http.StatusOK, def)
145}
146
147// CanWriteFiles returns true if repository is editable and user has proper access level.
148func CanWriteFiles(r *context.Repository) bool {
149 return r.Permission.CanWrite(models.UnitTypeCode) && !r.Repository.IsMirror && !r.Repository.IsArchived
150}
151
152// CanReadFiles returns true if repository is readable and user has proper access level.
153func CanReadFiles(r *context.Repository) bool {
154 return r.Permission.CanRead(models.UnitTypeCode)
155}
156
157// CreateFile handles API call for creating a file
158func CreateFile(ctx *context.APIContext, apiOpts api.CreateFileOptions) {
159 // swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
160 // ---
161 // summary: Create a file in a repository
162 // consumes:
163 // - application/json
164 // produces:
165 // - application/json
166 // parameters:
167 // - name: owner
168 // in: path
169 // description: owner of the repo
170 // type: string
171 // required: true
172 // - name: repo
173 // in: path
174 // description: name of the repo
175 // type: string
176 // required: true
177 // - name: filepath
178 // in: path
179 // description: path of the file to create
180 // type: string
181 // required: true
182 // - name: body
183 // in: body
184 // 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"
185 // schema:
186 // "$ref": "#/definitions/CreateFileOptions"
187 // responses:
188 // "201":
189 // "$ref": "#/responses/FileResponse"
190
191 opts := &repofiles.UpdateRepoFileOptions{
192 Content: apiOpts.Content,
193 IsNewFile: true,
194 Message: apiOpts.Message,
195 TreePath: ctx.Params("*"),
196 OldBranch: apiOpts.BranchName,
197 NewBranch: apiOpts.NewBranchName,
198 Committer: &repofiles.IdentityOptions{
199 Name: apiOpts.Committer.Name,
200 Email: apiOpts.Committer.Email,
201 },
202 Author: &repofiles.IdentityOptions{
203 Name: apiOpts.Author.Name,
204 Email: apiOpts.Author.Email,
205 },
206 }
207 if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
208 ctx.Error(http.StatusInternalServerError, "CreateFile", err)
209 } else {
210 ctx.JSON(http.StatusCreated, fileResponse)
211 }
212}
213
214// UpdateFile handles API call for updating a file
215func UpdateFile(ctx *context.APIContext, apiOpts api.UpdateFileOptions) {
216 // swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
217 // ---
218 // summary: Update a file in a repository
219 // consumes:
220 // - application/json
221 // produces:
222 // - application/json
223 // parameters:
224 // - name: owner
225 // in: path
226 // description: owner of the repo
227 // type: string
228 // required: true
229 // - name: repo
230 // in: path
231 // description: name of the repo
232 // type: string
233 // required: true
234 // - name: filepath
235 // in: path
236 // description: path of the file to update
237 // type: string
238 // required: true
239 // - name: body
240 // in: body
241 // 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"
242 // schema:
243 // "$ref": "#/definitions/UpdateFileOptions"
244 // responses:
245 // "200":
246 // "$ref": "#/responses/FileResponse"
247
248 opts := &repofiles.UpdateRepoFileOptions{
249 Content: apiOpts.Content,
250 SHA: apiOpts.SHA,
251 IsNewFile: false,
252 Message: apiOpts.Message,
253 FromTreePath: apiOpts.FromPath,
254 TreePath: ctx.Params("*"),
255 OldBranch: apiOpts.BranchName,
256 NewBranch: apiOpts.NewBranchName,
257 Committer: &repofiles.IdentityOptions{
258 Name: apiOpts.Committer.Name,
259 Email: apiOpts.Committer.Email,
260 },
261 Author: &repofiles.IdentityOptions{
262 Name: apiOpts.Author.Name,
263 Email: apiOpts.Author.Email,
264 },
265 }
266
267 if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
268 ctx.Error(http.StatusInternalServerError, "UpdateFile", err)
269 } else {
270 ctx.JSON(http.StatusOK, fileResponse)
271 }
272}
273
274// Called from both CreateFile or UpdateFile to handle both
275func createOrUpdateFile(ctx *context.APIContext, opts *repofiles.UpdateRepoFileOptions) (*api.FileResponse, error) {
276 if !CanWriteFiles(ctx.Repo) {
277 return nil, models.ErrUserDoesNotHaveAccessToRepo{
278 UserID: ctx.User.ID,
279 RepoName: ctx.Repo.Repository.LowerName,
280 }
281 }
282
283 content, err := base64.StdEncoding.DecodeString(opts.Content)
284 if err != nil {
285 return nil, err
286 }
287 opts.Content = string(content)
288
289 return repofiles.CreateOrUpdateRepoFile(ctx.Repo.Repository, ctx.User, opts)
290}
291
292// DeleteFile Delete a fle in a repository
293func DeleteFile(ctx *context.APIContext, apiOpts api.DeleteFileOptions) {
294 // swagger:operation DELETE /repos/{owner}/{repo}/contents/{filepath} repository repoDeleteFile
295 // ---
296 // summary: Delete a file in a repository
297 // consumes:
298 // - application/json
299 // produces:
300 // - application/json
301 // parameters:
302 // - name: owner
303 // in: path
304 // description: owner of the repo
305 // type: string
306 // required: true
307 // - name: repo
308 // in: path
309 // description: name of the repo
310 // type: string
311 // required: true
312 // - name: filepath
313 // in: path
314 // description: path of the file to delete
315 // type: string
316 // required: true
317 // - name: body
318 // in: body
319 // 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"
320 // schema:
321 // "$ref": "#/definitions/DeleteFileOptions"
322 // responses:
323 // "200":
324 // "$ref": "#/responses/FileDeleteResponse"
325 if !CanWriteFiles(ctx.Repo) {
326 ctx.Error(http.StatusInternalServerError, "DeleteFile", models.ErrUserDoesNotHaveAccessToRepo{
327 UserID: ctx.User.ID,
328 RepoName: ctx.Repo.Repository.LowerName,
329 })
330 return
331 }
332
333 opts := &repofiles.DeleteRepoFileOptions{
334 Message: apiOpts.Message,
335 OldBranch: apiOpts.BranchName,
336 NewBranch: apiOpts.NewBranchName,
337 SHA: apiOpts.SHA,
338 TreePath: ctx.Params("*"),
339 Committer: &repofiles.IdentityOptions{
340 Name: apiOpts.Committer.Name,
341 Email: apiOpts.Committer.Email,
342 },
343 Author: &repofiles.IdentityOptions{
344 Name: apiOpts.Author.Name,
345 Email: apiOpts.Author.Email,
346 },
347 }
348
349 if fileResponse, err := repofiles.DeleteRepoFile(ctx.Repo.Repository, ctx.User, opts); err != nil {
350 ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
351 } else {
352 ctx.JSON(http.StatusOK, fileResponse)
353 }
354}
355
356// GetFileContents Get the contents of a fle in a repository
357func GetFileContents(ctx *context.APIContext) {
358 // swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetFileContents
359 // ---
360 // summary: Gets the contents of a file or directory in a repository
361 // produces:
362 // - application/json
363 // parameters:
364 // - name: owner
365 // in: path
366 // description: owner of the repo
367 // type: string
368 // required: true
369 // - name: repo
370 // in: path
371 // description: name of the repo
372 // type: string
373 // required: true
374 // - name: filepath
375 // in: path
376 // description: path of the file to delete
377 // type: string
378 // required: true
379 // - name: ref
380 // in: query
381 // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
382 // required: false
383 // type: string
384 // responses:
385 // "200":
386 // "$ref": "#/responses/FileContentResponse"
387
388 if !CanReadFiles(ctx.Repo) {
389 ctx.Error(http.StatusInternalServerError, "GetFileContents", models.ErrUserDoesNotHaveAccessToRepo{
390 UserID: ctx.User.ID,
391 RepoName: ctx.Repo.Repository.LowerName,
392 })
393 return
394 }
395
396 treePath := ctx.Params("*")
397 ref := ctx.QueryTrim("ref")
398
399 if fileContents, err := repofiles.GetFileContents(ctx.Repo.Repository, treePath, ref); err != nil {
400 ctx.Error(http.StatusInternalServerError, "GetFileContents", err)
401 } else {
402 ctx.JSON(http.StatusOK, fileContents)
403 }
140}404}
routers/api/v1/repo/tree.go+6−88
@@ -5,13 +5,8 @@ · 4 unmodified lines
5package repo5package repo
6 6
7import (7import (
8 "fmt"
9 "strings"
10
11 "code.gitea.io/gitea/modules/context"8 "code.gitea.io/gitea/modules/context"
12 "code.gitea.io/gitea/modules/git"9 "code.gitea.io/gitea/modules/repofiles"
13 "code.gitea.io/gitea/modules/setting"
14 "code.gitea.io/sdk/gitea"
15)10)
16 11
17// GetTree get the tree of a repository.12// GetTree get the tree of a repository.
@@ -55,92 +50,15 @@ func GetTree(ctx *context.APIContext) { · 37 unmodified lines
55 // responses:50 // responses:
56 // "200":51 // "200":
57 // "$ref": "#/responses/GitTreeResponse"52 // "$ref": "#/responses/GitTreeResponse"
58 sha := ctx.Params("sha")53
54 sha := ctx.Params(":sha")
59 if len(sha) == 0 {55 if len(sha) == 0 {
60 ctx.Error(400, "", "sha not provided")56 ctx.Error(400, "", "sha not provided")
61 return57 return
62 }58 }
63 tree := GetTreeBySHA(ctx, sha)59 if tree, err := repofiles.GetTreeBySHA(ctx.Repo.Repository, sha, ctx.QueryInt("page"), ctx.QueryInt("per_page"), ctx.QueryBool("recursive")); err != nil {
64 if tree != nil {60 ctx.Error(400, "", err.Error())
65 ctx.JSON(200, tree)
66 } else {
67 ctx.Error(400, "", "sha invalid")
68 }
69}
70
71// GetTreeBySHA get the GitTreeResponse of a repository using a sha hash.
72func GetTreeBySHA(ctx *context.APIContext, sha string) *gitea.GitTreeResponse {
73 gitTree, err := ctx.Repo.GitRepo.GetTree(sha)
74 if err != nil || gitTree == nil {
75 return nil
76 }
77 tree := new(gitea.GitTreeResponse)
78 repoID := strings.TrimRight(setting.AppURL, "/") + "/api/v1/repos/" + ctx.Repo.Repository.Owner.Name + "/" + ctx.Repo.Repository.Name
79 tree.SHA = gitTree.ID.String()
80 tree.URL = repoID + "/git/trees/" + tree.SHA
81 var entries git.Entries
82 if ctx.QueryBool("recursive") {
83 entries, err = gitTree.ListEntriesRecursive()
84 } else {
85 entries, err = gitTree.ListEntries()
86 }
87 if err != nil {
88 return tree
89 }
90 repoIDLen := len(repoID)
91
92 // 51 is len(sha1) + len("/git/blobs/"). 40 + 11.
93 blobURL := make([]byte, repoIDLen+51)
94 copy(blobURL[:], repoID)
95 copy(blobURL[repoIDLen:], "/git/blobs/")
96
97 // 51 is len(sha1) + len("/git/trees/"). 40 + 11.
98 treeURL := make([]byte, repoIDLen+51)
99 copy(treeURL[:], repoID)
100 copy(treeURL[repoIDLen:], "/git/trees/")
101
102 // 40 is the size of the sha1 hash in hexadecimal format.
103 copyPos := len(treeURL) - 40
104
105 page := ctx.QueryInt("page")
106 perPage := ctx.QueryInt("per_page")
107 if perPage <= 0 || perPage > setting.API.DefaultGitTreesPerPage {
108 perPage = setting.API.DefaultGitTreesPerPage
109 }
110 if page <= 0 {
111 page = 1
112 }
113 tree.Page = page
114 tree.TotalCount = len(entries)
115 rangeStart := perPage * (page - 1)
116 if rangeStart >= len(entries) {
117 return tree
118 }
119 var rangeEnd int
120 if len(entries) > perPage {
121 tree.Truncated = true
122 }
123 if rangeStart+perPage < len(entries) {
124 rangeEnd = rangeStart + perPage
125 } else {61 } else {
126 rangeEnd = len(entries)62 ctx.JSON(200, tree)
127 }
128 tree.Entries = make([]gitea.GitEntry, rangeEnd-rangeStart)
129 for e := rangeStart; e < rangeEnd; e++ {
130 i := e - rangeStart
131 tree.Entries[i].Path = entries[e].Name()
132 tree.Entries[i].Mode = fmt.Sprintf("%06x", entries[e].Mode())
133 tree.Entries[i].Type = string(entries[e].Type)
134 tree.Entries[i].Size = entries[e].Size()
135 tree.Entries[i].SHA = entries[e].ID.String()
136
137 if entries[e].IsDir() {
138 copy(treeURL[copyPos:], entries[e].ID.String())
139 tree.Entries[i].URL = string(treeURL[:])
140 } else {
141 copy(blobURL[copyPos:], entries[e].ID.String())
142 tree.Entries[i].URL = string(blobURL[:])
143 }
144 }63 }
145 return tree
146}64}
routers/api/v1/swagger/options.go+10−0
@@ -97,6 +97,7 @@ type swaggerParameterBodies struct { · 96 unmodified lines
97 97
98 // in:body98 // in:body
99 CreateUserOption api.CreateUserOption99 CreateUserOption api.CreateUserOption
100
100 // in:body101 // in:body
101 EditUserOption api.EditUserOption102 EditUserOption api.EditUserOption
102 103
@@ -105,4 +106,13 @@ type swaggerParameterBodies struct { · 2 unmodified lines
105 106
106 // in:body107 // in:body
107 EditAttachmentOptions api.EditAttachmentOptions108 EditAttachmentOptions api.EditAttachmentOptions
109
110 // in:body
111 CreateFileOptions api.CreateFileOptions
112
113 // in:body
114 UpdateFileOptions api.UpdateFileOptions
115
116 // in:body
117 DeleteFileOptions api.DeleteFileOptions
108}118}
routers/api/v1/swagger/repo.go+28−0
@@ -162,9 +162,37 @@ type swaggerGitTreeResponse struct { · 161 unmodified lines
162 Body api.GitTreeResponse `json:"body"`162 Body api.GitTreeResponse `json:"body"`
163}163}
164 164
165// GitBlobResponse
166// swagger:response GitBlobResponse
167type swaggerGitBlobResponse struct {
168 //in: body
169 Body api.GitBlobResponse `json:"body"`
170}
171
165// Commit172// Commit
166// swagger:response Commit173// swagger:response Commit
167type swaggerCommit struct {174type swaggerCommit struct {
168 //in: body175 //in: body
169 Body api.Commit `json:"body"`176 Body api.Commit `json:"body"`
170}177}
178
179// FileResponse
180// swagger:response FileResponse
181type swaggerFileResponse struct {
182 //in: body
183 Body api.FileResponse `json:"body"`
184}
185
186// FileContentResponse
187// swagger:response FileContentResponse
188type swaggerFileContentResponse struct {
189 //in: body
190 Body api.FileContentResponse `json:"body"`
191}
192
193// FileDeleteResponse
194// swagger:response FileDeleteResponse
195type swaggerFileDeleteResponse struct {
196 //in: body
197 Body api.FileDeleteResponse `json:"body"`
198}
routers/repo/editor.go+134−156
@@ -17,9 +17,9 @@ import ( · 16 unmodified lines
17 "code.gitea.io/gitea/modules/context"17 "code.gitea.io/gitea/modules/context"
18 "code.gitea.io/gitea/modules/git"18 "code.gitea.io/gitea/modules/git"
19 "code.gitea.io/gitea/modules/log"19 "code.gitea.io/gitea/modules/log"
20 "code.gitea.io/gitea/modules/repofiles"
20 "code.gitea.io/gitea/modules/setting"21 "code.gitea.io/gitea/modules/setting"
21 "code.gitea.io/gitea/modules/templates"22 "code.gitea.io/gitea/modules/templates"
22 "code.gitea.io/gitea/modules/uploader"
23 "code.gitea.io/gitea/modules/util"23 "code.gitea.io/gitea/modules/util"
24)24)
25 25
@@ -139,7 +139,7 @@ func editFile(ctx *context.Context, isNewFile bool) { · 113 unmodified lines
139 ctx.Data["commit_choice"] = frmCommitChoiceNewBranch139 ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
140 }140 }
141 ctx.Data["new_branch_name"] = ""141 ctx.Data["new_branch_name"] = ""
142 ctx.Data["last_commit"] = ctx.Repo.Commit.ID142 ctx.Data["last_commit"] = ctx.Repo.CommitID
143 ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")143 ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
144 ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")144 ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
145 ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")145 ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
@@ -159,39 +159,27 @@ func NewFile(ctx *context.Context) { · 13 unmodified lines
159}159}
160 160
161func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bool) {161func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bool) {
162 ctx.Data["PageIsEdit"] = true
163 ctx.Data["IsNewFile"] = isNewFile
164 ctx.Data["RequireHighlightJS"] = true
165 ctx.Data["RequireSimpleMDE"] = true
166 canCommit := renderCommitRights(ctx)162 canCommit := renderCommitRights(ctx)
167 163 treeNames, treePaths := getParentTreeFields(form.TreePath)
168 oldBranchName := ctx.Repo.BranchName164 branchName := ctx.Repo.BranchName
169 branchName := oldBranchName
170 oldTreePath := cleanUploadFileName(ctx.Repo.TreePath)
171 lastCommit := form.LastCommit
172 form.LastCommit = ctx.Repo.Commit.ID.String()
173
174 if form.CommitChoice == frmCommitChoiceNewBranch {165 if form.CommitChoice == frmCommitChoiceNewBranch {
175 branchName = form.NewBranchName166 branchName = form.NewBranchName
176 }167 }
177 168
178 form.TreePath = cleanUploadFileName(form.TreePath)169 ctx.Data["PageIsEdit"] = true
179 if len(form.TreePath) == 0 {170 ctx.Data["IsNewFile"] = isNewFile
180 ctx.Error(500, "Upload file name is invalid")171 ctx.Data["RequireHighlightJS"] = true
181 return172 ctx.Data["RequireSimpleMDE"] = true
182 }
183 treeNames, treePaths := getParentTreeFields(form.TreePath)
184
185 ctx.Data["TreePath"] = form.TreePath173 ctx.Data["TreePath"] = form.TreePath
186 ctx.Data["TreeNames"] = treeNames174 ctx.Data["TreeNames"] = treeNames
187 ctx.Data["TreePaths"] = treePaths175 ctx.Data["TreePaths"] = treePaths
188 ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + branchName176 ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + ctx.Repo.BranchName
189 ctx.Data["FileContent"] = form.Content177 ctx.Data["FileContent"] = form.Content
190 ctx.Data["commit_summary"] = form.CommitSummary178 ctx.Data["commit_summary"] = form.CommitSummary
191 ctx.Data["commit_message"] = form.CommitMessage179 ctx.Data["commit_message"] = form.CommitMessage
192 ctx.Data["commit_choice"] = form.CommitChoice180 ctx.Data["commit_choice"] = form.CommitChoice
193 ctx.Data["new_branch_name"] = branchName181 ctx.Data["new_branch_name"] = form.NewBranchName
194 ctx.Data["last_commit"] = form.LastCommit182 ctx.Data["last_commit"] = ctx.Repo.CommitID
195 ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")183 ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
196 ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")184 ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
197 ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")185 ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
@@ -201,101 +189,16 @@ func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bo · 3 unmodified lines
201 return189 return
202 }190 }
203 191
204 if len(form.TreePath) == 0 {192 // Cannot commit to a an existing branch if user doesn't have rights
205 ctx.Data["Err_TreePath"] = true193 if branchName == ctx.Repo.BranchName && !canCommit {
206 ctx.RenderWithErr(ctx.Tr("repo.editor.filename_cannot_be_empty"), tplEditFile, &form)
207 return
208 }
209
210 if oldBranchName != branchName {
211 if _, err := ctx.Repo.Repository.GetBranch(branchName); err == nil {
212 ctx.Data["Err_NewBranchName"] = true
213 ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplEditFile, &form)
214 return
215 }
216 } else if !canCommit {
217 ctx.Data["Err_NewBranchName"] = true194 ctx.Data["Err_NewBranchName"] = true
218 ctx.Data["commit_choice"] = frmCommitChoiceNewBranch195 ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
219 ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)196 ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
220 return197 return
221 }198 }
222 199
223 var newTreePath string200 // CommitSummary is optional in the web form, if empty, give it a default message based on add or update
224 for index, part := range treeNames {201 // `message` will be both the summary and message combined
225 newTreePath = path.Join(newTreePath, part)
226 entry, err := ctx.Repo.Commit.GetTreeEntryByPath(newTreePath)
227 if err != nil {
228 if git.IsErrNotExist(err) {
229 // Means there is no item with that name, so we're good
230 break
231 }
232
233 ctx.ServerError("Repo.Commit.GetTreeEntryByPath", err)
234 return
235 }
236 if index != len(treeNames)-1 {
237 if !entry.IsDir() {
238 ctx.Data["Err_TreePath"] = true
239 ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", part), tplEditFile, &form)
240 return
241 }
242 } else {
243 if entry.IsLink() {
244 ctx.Data["Err_TreePath"] = true
245 ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", part), tplEditFile, &form)
246 return
247 }
248 if entry.IsDir() {
249 ctx.Data["Err_TreePath"] = true
250 ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", part), tplEditFile, &form)
251 return
252 }
253 }
254 }
255
256 if !isNewFile {
257 _, err := ctx.Repo.Commit.GetTreeEntryByPath(oldTreePath)
258 if err != nil {
259 if git.IsErrNotExist(err) {
260 ctx.Data["Err_TreePath"] = true
261 ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", oldTreePath), tplEditFile, &form)
262 } else {
263 ctx.ServerError("GetTreeEntryByPath", err)
264 }
265 return
266 }
267 if lastCommit != ctx.Repo.CommitID {
268 files, err := ctx.Repo.Commit.GetFilesChangedSinceCommit(lastCommit)
269 if err != nil {
270 ctx.ServerError("GetFilesChangedSinceCommit", err)
271 return
272 }
273
274 for _, file := range files {
275 if file == form.TreePath {
276 ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+lastCommit+"..."+ctx.Repo.CommitID), tplEditFile, &form)
277 return
278 }
279 }
280 }
281 }
282
283 if oldTreePath != form.TreePath {
284 // We have a new filename (rename or completely new file) so we need to make sure it doesn't already exist, can't clobber.
285 entry, err := ctx.Repo.Commit.GetTreeEntryByPath(form.TreePath)
286 if err != nil {
287 if !git.IsErrNotExist(err) {
288 ctx.ServerError("GetTreeEntryByPath", err)
289 return
290 }
291 }
292 if entry != nil {
293 ctx.Data["Err_TreePath"] = true
294 ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form)
295 return
296 }
297 }
298
299 message := strings.TrimSpace(form.CommitSummary)202 message := strings.TrimSpace(form.CommitSummary)
300 if len(message) == 0 {203 if len(message) == 0 {
301 if isNewFile {204 if isNewFile {
@@ -304,28 +207,74 @@ func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bo · 2 unmodified lines
304 message = ctx.Tr("repo.editor.update", form.TreePath)207 message = ctx.Tr("repo.editor.update", form.TreePath)
305 }208 }
306 }209 }
307
308 form.CommitMessage = strings.TrimSpace(form.CommitMessage)210 form.CommitMessage = strings.TrimSpace(form.CommitMessage)
309 if len(form.CommitMessage) > 0 {211 if len(form.CommitMessage) > 0 {
310 message += "\n\n" + form.CommitMessage212 message += "\n\n" + form.CommitMessage
311 }213 }
312 214
313 if err := uploader.UpdateRepoFile(ctx.Repo.Repository, ctx.User, &uploader.UpdateRepoFileOptions{215 if _, err := repofiles.CreateOrUpdateRepoFile(ctx.Repo.Repository, ctx.User, &repofiles.UpdateRepoFileOptions{
314 LastCommitID: lastCommit,216 LastCommitID: form.LastCommit,
315 OldBranch: oldBranchName,217 OldBranch: ctx.Repo.BranchName,
316 NewBranch: branchName,218 NewBranch: branchName,
317 OldTreeName: oldTreePath,219 FromTreePath: ctx.Repo.TreePath,
318 NewTreeName: form.TreePath,220 TreePath: form.TreePath,
319 Message: message,221 Message: message,
320 Content: strings.Replace(form.Content, "\r", "", -1),222 Content: strings.Replace(form.Content, "\r", "", -1),
321 IsNewFile: isNewFile,223 IsNewFile: isNewFile,
322 }); err != nil {224 }); err != nil {
323 ctx.Data["Err_TreePath"] = true225 // This is where we handle all the errors thrown by repofiles.CreateOrUpdateRepoFile
324 ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_update_file", form.TreePath, err), tplEditFile, &form)226 if git.IsErrNotExist(err) {
325 return227 ctx.RenderWithErr(ctx.Tr("repo.editor.file_editing_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
228 } else if models.IsErrFilenameInvalid(err) {
229 ctx.Data["Err_TreePath"] = true
230 ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", form.TreePath), tplEditFile, &form)
231 } else if models.IsErrFilePathInvalid(err) {
232 ctx.Data["Err_TreePath"] = true
233 if fileErr, ok := err.(models.ErrFilePathInvalid); ok {
234 switch fileErr.Type {
235 case git.EntryModeSymlink:
236 ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form)
237 break
238 case git.EntryModeTree:
239 ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form)
240 break
241 case git.EntryModeBlob:
242 ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form)
243 break
244 default:
245 ctx.Error(500, err.Error())
246 break
247 }
248 } else {
249 ctx.Error(500, err.Error())
250 }
251 } else if models.IsErrRepoFileAlreadyExists(err) {
252 ctx.Data["Err_TreePath"] = true
253 ctx.RenderWithErr(ctx.Tr("repo.editor.file_already_exists", form.TreePath), tplEditFile, &form)
254 } else if models.IsErrBranchNotExist(err) {
255 // For when a user adds/updates a file to a branch that no longer exists
256 if branchErr, ok := err.(models.ErrBranchNotExist); ok {
257 ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form)
258 } else {
259 ctx.Error(500, err.Error())
260 }
261 } else if models.IsErrBranchAlreadyExists(err) {
262 // For when a user specifies a new branch that already exists
263 ctx.Data["Err_NewBranchName"] = true
264 if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok {
265 ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
266 } else {
267 ctx.Error(500, err.Error())
268 }
269 } else if models.IsErrCommitIDDoesNotMatch(err) {
270 ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplEditFile, &form)
271 } else {
272 ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_update_file", form.TreePath, err), tplEditFile, &form)
273 }
274 } else {
275 ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + branchName + "/" + strings.NewReplacer("%", "%25", "#", "%23", " ", "%20", "?", "%3F").Replace(cleanUploadFileName(form.TreePath)))
276 ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(form.TreePath))
326 }277 }
327
328 ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(form.TreePath))
329}278}
330 279
331// EditFilePost response for editing file280// EditFilePost response for editing file
@@ -355,7 +304,7 @@ func DiffPreviewPost(ctx *context.Context, form auth.EditPreviewDiffForm) { · 23 unmodified lines
355 return304 return
356 }305 }
357 306
358 diff, err := uploader.GetDiffPreview(ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content)307 diff, err := repofiles.GetDiffPreview(ctx.Repo.Repository, ctx.Repo.BranchName, treePath, form.Content)
359 if err != nil {308 if err != nil {
360 ctx.Error(500, "GetDiffPreview: "+err.Error())309 ctx.Error(500, "GetDiffPreview: "+err.Error())
361 return310 return
@@ -386,6 +335,7 @@ func DeleteFile(ctx *context.Context) { · 24 unmodified lines
386 335
387 ctx.Data["commit_summary"] = ""336 ctx.Data["commit_summary"] = ""
388 ctx.Data["commit_message"] = ""337 ctx.Data["commit_message"] = ""
338 ctx.Data["last_commit"] = ctx.Repo.CommitID
389 if canCommit {339 if canCommit {
390 ctx.Data["commit_choice"] = frmCommitChoiceDirect340 ctx.Data["commit_choice"] = frmCommitChoiceDirect
391 } else {341 } else {
@@ -398,41 +348,27 @@ func DeleteFile(ctx *context.Context) { · 6 unmodified lines
398 348
399// DeleteFilePost response for deleting file349// DeleteFilePost response for deleting file
400func DeleteFilePost(ctx *context.Context, form auth.DeleteRepoFileForm) {350func DeleteFilePost(ctx *context.Context, form auth.DeleteRepoFileForm) {
401 ctx.Data["PageIsDelete"] = true
402 ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
403
404 ctx.Repo.TreePath = cleanUploadFileName(ctx.Repo.TreePath)
405 if len(ctx.Repo.TreePath) == 0 {
406 ctx.Error(500, "Delete file name is invalid")
407 return
408 }
409
410 ctx.Data["TreePath"] = ctx.Repo.TreePath
411 canCommit := renderCommitRights(ctx)351 canCommit := renderCommitRights(ctx)
412 352 branchName := ctx.Repo.BranchName
413 oldBranchName := ctx.Repo.BranchName
414 branchName := oldBranchName
415
416 if form.CommitChoice == frmCommitChoiceNewBranch {353 if form.CommitChoice == frmCommitChoiceNewBranch {
417 branchName = form.NewBranchName354 branchName = form.NewBranchName
418 }355 }
356
357 ctx.Data["PageIsDelete"] = true
358 ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
359 ctx.Data["TreePath"] = ctx.Repo.TreePath
419 ctx.Data["commit_summary"] = form.CommitSummary360 ctx.Data["commit_summary"] = form.CommitSummary
420 ctx.Data["commit_message"] = form.CommitMessage361 ctx.Data["commit_message"] = form.CommitMessage
421 ctx.Data["commit_choice"] = form.CommitChoice362 ctx.Data["commit_choice"] = form.CommitChoice
422 ctx.Data["new_branch_name"] = branchName363 ctx.Data["new_branch_name"] = form.NewBranchName
364 ctx.Data["last_commit"] = ctx.Repo.CommitID
423 365
424 if ctx.HasError() {366 if ctx.HasError() {
425 ctx.HTML(200, tplDeleteFile)367 ctx.HTML(200, tplDeleteFile)
426 return368 return
427 }369 }
428 370
429 if oldBranchName != branchName {371 if branchName != ctx.Repo.BranchName && !canCommit {
430 if _, err := ctx.Repo.Repository.GetBranch(branchName); err == nil {
431 ctx.Data["Err_NewBranchName"] = true
432 ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchName), tplDeleteFile, &form)
433 return
434 }
435 } else if !canCommit {
436 ctx.Data["Err_NewBranchName"] = true372 ctx.Data["Err_NewBranchName"] = true
437 ctx.Data["commit_choice"] = frmCommitChoiceNewBranch373 ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
438 ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplDeleteFile, &form)374 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) { · 4 unmodified lines
443 if len(message) == 0 {379 if len(message) == 0 {
444 message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath)380 message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath)
445 }381 }
446
447 form.CommitMessage = strings.TrimSpace(form.CommitMessage)382 form.CommitMessage = strings.TrimSpace(form.CommitMessage)
448 if len(form.CommitMessage) > 0 {383 if len(form.CommitMessage) > 0 {
449 message += "\n\n" + form.CommitMessage384 message += "\n\n" + form.CommitMessage
450 }385 }
451 386
452 if err := uploader.DeleteRepoFile(ctx.Repo.Repository, ctx.User, &uploader.DeleteRepoFileOptions{387 if _, err := repofiles.DeleteRepoFile(ctx.Repo.Repository, ctx.User, &repofiles.DeleteRepoFileOptions{
453 LastCommitID: ctx.Repo.CommitID,388 LastCommitID: form.LastCommit,
454 OldBranch: oldBranchName,389 OldBranch: ctx.Repo.BranchName,
455 NewBranch: branchName,390 NewBranch: branchName,
456 TreePath: ctx.Repo.TreePath,391 TreePath: ctx.Repo.TreePath,
457 Message: message,392 Message: message,
458 }); err != nil {393 }); err != nil {
459 ctx.ServerError("DeleteRepoFile", err)394 // This is where we handle all the errors thrown by repofiles.DeleteRepoFile
460 return395 if git.IsErrNotExist(err) || models.IsErrRepoFileDoesNotExist(err) {
396 ctx.RenderWithErr(ctx.Tr("repo.editor.file_deleting_no_longer_exists", ctx.Repo.TreePath), tplEditFile, &form)
397 } else if models.IsErrFilenameInvalid(err) {
398 ctx.Data["Err_TreePath"] = true
399 ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_invalid", ctx.Repo.TreePath), tplEditFile, &form)
400 } else if models.IsErrFilePathInvalid(err) {
401 ctx.Data["Err_TreePath"] = true
402 if fileErr, ok := err.(models.ErrFilePathInvalid); ok {
403 switch fileErr.Type {
404 case git.EntryModeSymlink:
405 ctx.RenderWithErr(ctx.Tr("repo.editor.file_is_a_symlink", fileErr.Path), tplEditFile, &form)
406 break
407 case git.EntryModeTree:
408 ctx.RenderWithErr(ctx.Tr("repo.editor.filename_is_a_directory", fileErr.Path), tplEditFile, &form)
409 break
410 case git.EntryModeBlob:
411 ctx.RenderWithErr(ctx.Tr("repo.editor.directory_is_a_file", fileErr.Path), tplEditFile, &form)
412 break
413 default:
414 ctx.ServerError("DeleteRepoFile", err)
415 break
416 }
417 } else {
418 ctx.ServerError("DeleteRepoFile", err)
419 }
420 } else if models.IsErrBranchNotExist(err) {
421 // For when a user deletes a file to a branch that no longer exists
422 if branchErr, ok := err.(models.ErrBranchNotExist); ok {
423 ctx.RenderWithErr(ctx.Tr("repo.editor.branch_does_not_exist", branchErr.Name), tplEditFile, &form)
424 } else {
425 ctx.Error(500, err.Error())
426 }
427 } else if models.IsErrBranchAlreadyExists(err) {
428 // For when a user specifies a new branch that already exists
429 if branchErr, ok := err.(models.ErrBranchAlreadyExists); ok {
430 ctx.RenderWithErr(ctx.Tr("repo.editor.branch_already_exists", branchErr.BranchName), tplEditFile, &form)
431 } else {
432 ctx.Error(500, err.Error())
433 }
434 } else if models.IsErrCommitIDDoesNotMatch(err) {
435 ctx.RenderWithErr(ctx.Tr("repo.editor.file_changed_while_editing", ctx.Repo.RepoLink+"/compare/"+form.LastCommit+"..."+ctx.Repo.CommitID), tplEditFile, &form)
436 } else {
437 ctx.ServerError("DeleteRepoFile", err)
438 }
439 } else {
440 ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
441 ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName))
461 }442 }
462
463 ctx.Flash.Success(ctx.Tr("repo.editor.file_delete_success", ctx.Repo.TreePath))
464 ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName))
465}443}
466 444
467func renderUploadSettings(ctx *context.Context) {445func renderUploadSettings(ctx *context.Context) {
@@ -584,7 +562,7 @@ func UploadFilePost(ctx *context.Context, form auth.UploadRepoFileForm) { · 116 unmodified lines
584 message += "\n\n" + form.CommitMessage562 message += "\n\n" + form.CommitMessage
585 }563 }
586 564
587 if err := uploader.UploadRepoFiles(ctx.Repo.Repository, ctx.User, &uploader.UploadRepoFileOptions{565 if err := repofiles.UploadRepoFiles(ctx.Repo.Repository, ctx.User, &repofiles.UploadRepoFileOptions{
588 LastCommitID: ctx.Repo.CommitID,566 LastCommitID: ctx.Repo.CommitID,
589 OldBranch: oldBranchName,567 OldBranch: oldBranchName,
590 NewBranch: branchName,568 NewBranch: branchName,
routers/repo/editor_test.go+1−0
@@ -8,6 +8,7 @@ import ( · 7 unmodified lines
8 "testing"8 "testing"
9 9
10 "code.gitea.io/gitea/models"10 "code.gitea.io/gitea/models"
11
11 "github.com/stretchr/testify/assert"12 "github.com/stretchr/testify/assert"
12)13)
13 14
templates/repo/editor/commit_form.tmpl+1−1
@@ -3,7 +3,7 @@ · 2 unmodified lines
3 <div class="commit-form">3 <div class="commit-form">
4 <h3>{{.i18n.Tr "repo.editor.commit_changes"}}</h3>4 <h3>{{.i18n.Tr "repo.editor.commit_changes"}}</h3>
5 <div class="field">5 <div class="field">
6 <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>6 <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>
7 </div>7 </div>
8 <div class="field">8 <div class="field">
9 <textarea name="commit_message" placeholder="{{.i18n.Tr "repo.editor.commit_message_desc"}}" rows="5">{{.commit_message}}</textarea>9 <textarea name="commit_message" placeholder="{{.i18n.Tr "repo.editor.commit_message_desc"}}" rows="5">{{.commit_message}}</textarea>
templates/repo/editor/delete.tmpl+2−1
@@ -5,8 +5,9 @@ · 4 unmodified lines
5 {{template "base/alert" .}}5 {{template "base/alert" .}}
6 <form class="ui form" method="post">6 <form class="ui form" method="post">
7 {{.CsrfTokenHtml}}7 {{.CsrfTokenHtml}}
8 <input type="hidden" name="last_commit" value="{{.last_commit}}">
8 {{template "repo/editor/commit_form" .}}9 {{template "repo/editor/commit_form" .}}
9 </form>10 </form>
10 </div>11 </div>
11</div>12</div>
12{{template "base/footer" .}}13{{template "base/footer" .}}
templates/swagger/v1_json.tmpl+535−1
@@ -1511,6 +1511,199 @@ · 1510 unmodified lines
1511 }1511 }
1512 }1512 }
1513 },1513 },
1514 "/repos/{owner}/{repo}/contents/{filepath}": {
1515 "get": {
1516 "produces": [
1517 "application/json"
1518 ],
1519 "tags": [
1520 "repository"
1521 ],
1522 "summary": "Gets the contents of a file or directory in a repository",
1523 "operationId": "repoGetFileContents",
1524 "parameters": [
1525 {
1526 "type": "string",
1527 "description": "owner of the repo",
1528 "name": "owner",
1529 "in": "path",
1530 "required": true
1531 },
1532 {
1533 "type": "string",
1534 "description": "name of the repo",
1535 "name": "repo",
1536 "in": "path",
1537 "required": true
1538 },
1539 {
1540 "type": "string",
1541 "description": "path of the file to delete",
1542 "name": "filepath",
1543 "in": "path",
1544 "required": true
1545 },
1546 {
1547 "type": "string",
1548 "description": "The name of the commit/branch/tag. Default the repository’s default branch (usually master)",
1549 "name": "ref",
1550 "in": "query"
1551 }
1552 ],
1553 "responses": {
1554 "200": {
1555 "$ref": "#/responses/FileContentResponse"
1556 }
1557 }
1558 },
1559 "put": {
1560 "consumes": [
1561 "application/json"
1562 ],
1563 "produces": [
1564 "application/json"
1565 ],
1566 "tags": [
1567 "repository"
1568 ],
1569 "summary": "Update a file in a repository",
1570 "operationId": "repoUpdateFile",
1571 "parameters": [
1572 {
1573 "type": "string",
1574 "description": "owner of the repo",
1575 "name": "owner",
1576 "in": "path",
1577 "required": true
1578 },
1579 {
1580 "type": "string",
1581 "description": "name of the repo",
1582 "name": "repo",
1583 "in": "path",
1584 "required": true
1585 },
1586 {
1587 "type": "string",
1588 "description": "path of the file to update",
1589 "name": "filepath",
1590 "in": "path",
1591 "required": true
1592 },
1593 {
1594 "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",
1595 "name": "body",
1596 "in": "body",
1597 "schema": {
1598 "$ref": "#/definitions/UpdateFileOptions"
1599 }
1600 }
1601 ],
1602 "responses": {
1603 "200": {
1604 "$ref": "#/responses/FileResponse"
1605 }
1606 }
1607 },
1608 "post": {
1609 "consumes": [
1610 "application/json"
1611 ],
1612 "produces": [
1613 "application/json"
1614 ],
1615 "tags": [
1616 "repository"
1617 ],
1618 "summary": "Create a file in a repository",
1619 "operationId": "repoCreateFile",
1620 "parameters": [
1621 {
1622 "type": "string",
1623 "description": "owner of the repo",
1624 "name": "owner",
1625 "in": "path",
1626 "required": true
1627 },
1628 {
1629 "type": "string",
1630 "description": "name of the repo",
1631 "name": "repo",
1632 "in": "path",
1633 "required": true
1634 },
1635 {
1636 "type": "string",
1637 "description": "path of the file to create",
1638 "name": "filepath",
1639 "in": "path",
1640 "required": true
1641 },
1642 {
1643 "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",
1644 "name": "body",
1645 "in": "body",
1646 "schema": {
1647 "$ref": "#/definitions/CreateFileOptions"
1648 }
1649 }
1650 ],
1651 "responses": {
1652 "201": {
1653 "$ref": "#/responses/FileResponse"
1654 }
1655 }
1656 },
1657 "delete": {
1658 "consumes": [
1659 "application/json"
1660 ],
1661 "produces": [
1662 "application/json"
1663 ],
1664 "tags": [
1665 "repository"
1666 ],
1667 "summary": "Delete a file in a repository",
1668 "operationId": "repoDeleteFile",
1669 "parameters": [
1670 {
1671 "type": "string",
1672 "description": "owner of the repo",
1673 "name": "owner",
1674 "in": "path",
1675 "required": true
1676 },
1677 {
1678 "type": "string",
1679 "description": "name of the repo",
1680 "name": "repo",
1681 "in": "path",
1682 "required": true
1683 },
1684 {
1685 "type": "string",
1686 "description": "path of the file to delete",
1687 "name": "filepath",
1688 "in": "path",
1689 "required": true
1690 },
1691 {
1692 "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",
1693 "name": "body",
1694 "in": "body",
1695 "schema": {
1696 "$ref": "#/definitions/DeleteFileOptions"
1697 }
1698 }
1699 ],
1700 "responses": {
1701 "200": {
1702 "$ref": "#/responses/FileDeleteResponse"
1703 }
1704 }
1705 }
1706 },
1514 "/repos/{owner}/{repo}/editorconfig/{filepath}": {1707 "/repos/{owner}/{repo}/editorconfig/{filepath}": {
1515 "get": {1708 "get": {
1516 "produces": [1709 "produces": [
@@ -1622,6 +1815,46 @@ · 105 unmodified lines
1622 }1815 }
1623 }1816 }
1624 },1817 },
1818 "/repos/{owner}/{repo}/git/blobs/{sha}": {
1819 "get": {
1820 "produces": [
1821 "application/json"
1822 ],
1823 "tags": [
1824 "repository"
1825 ],
1826 "summary": "Gets the blob of a repository.",
1827 "operationId": "GetBlob",
1828 "parameters": [
1829 {
1830 "type": "string",
1831 "description": "owner of the repo",
1832 "name": "owner",
1833 "in": "path",
1834 "required": true
1835 },
1836 {
1837 "type": "string",
1838 "description": "name of the repo",
1839 "name": "repo",
1840 "in": "path",
1841 "required": true
1842 },
1843 {
1844 "type": "string",
1845 "description": "sha of the commit",
1846 "name": "sha",
1847 "in": "path",
1848 "required": true
1849 }
1850 ],
1851 "responses": {
1852 "200": {
1853 "$ref": "#/responses/GitBlobResponse"
1854 }
1855 }
1856 }
1857 },
1625 "/repos/{owner}/{repo}/git/commits/{sha}": {1858 "/repos/{owner}/{repo}/git/commits/{sha}": {
1626 "get": {1859 "get": {
1627 "produces": [1860 "produces": [
@@ -6637,6 +6870,35 @@ · 5009 unmodified lines
6637 },6870 },
6638 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"6871 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
6639 },6872 },
6873 "CreateFileOptions": {
6874 "description": "CreateFileOptions options for creating files",
6875 "type": "object",
6876 "properties": {
6877 "author": {
6878 "$ref": "#/definitions/Identity"
6879 },
6880 "branch": {
6881 "type": "string",
6882 "x-go-name": "BranchName"
6883 },
6884 "committer": {
6885 "$ref": "#/definitions/Identity"
6886 },
6887 "content": {
6888 "type": "string",
6889 "x-go-name": "Content"
6890 },
6891 "message": {
6892 "type": "string",
6893 "x-go-name": "Message"
6894 },
6895 "new_branch": {
6896 "type": "string",
6897 "x-go-name": "NewBranchName"
6898 }
6899 },
6900 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
6901 },
6640 "CreateForkOption": {6902 "CreateForkOption": {
6641 "description": "CreateForkOption options for creating a fork",6903 "description": "CreateForkOption options for creating a fork",
6642 "type": "object",6904 "type": "object",
@@ -7129,6 +7391,35 @@ · 486 unmodified lines
7129 },7391 },
7130 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"7392 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
7131 },7393 },
7394 "DeleteFileOptions": {
7395 "description": "DeleteFileOptions options for deleting files (used for other File structs below)",
7396 "type": "object",
7397 "properties": {
7398 "author": {
7399 "$ref": "#/definitions/Identity"
7400 },
7401 "branch": {
7402 "type": "string",
7403 "x-go-name": "BranchName"
7404 },
7405 "committer": {
7406 "$ref": "#/definitions/Identity"
7407 },
7408 "message": {
7409 "type": "string",
7410 "x-go-name": "Message"
7411 },
7412 "new_branch": {
7413 "type": "string",
7414 "x-go-name": "NewBranchName"
7415 },
7416 "sha": {
7417 "type": "string",
7418 "x-go-name": "SHA"
7419 }
7420 },
7421 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
7422 },
7132 "DeployKey": {7423 "DeployKey": {
7133 "description": "DeployKey a deploy key",7424 "description": "DeployKey a deploy key",
7134 "type": "object",7425 "type": "object",
@@ -7567,6 +7858,144 @@ · 432 unmodified lines
7567 },7858 },
7568 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"7859 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
7569 },7860 },
7861 "FileCommitResponse": {
7862 "type": "object",
7863 "title": "FileCommitResponse contains information generated from a Git commit for a repo's file.",
7864 "properties": {
7865 "author": {
7866 "$ref": "#/definitions/CommitUser"
7867 },
7868 "committer": {
7869 "$ref": "#/definitions/CommitUser"
7870 },
7871 "html_url": {
7872 "type": "string",
7873 "x-go-name": "HTMLURL"
7874 },
7875 "message": {
7876 "type": "string",
7877 "x-go-name": "Message"
7878 },
7879 "parents": {
7880 "type": "array",
7881 "items": {
7882 "$ref": "#/definitions/CommitMeta"
7883 },
7884 "x-go-name": "Parents"
7885 },
7886 "sha": {
7887 "type": "string",
7888 "x-go-name": "SHA"
7889 },
7890 "tree": {
7891 "$ref": "#/definitions/CommitMeta"
7892 },
7893 "url": {
7894 "type": "string",
7895 "x-go-name": "URL"
7896 }
7897 },
7898 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
7899 },
7900 "FileContentResponse": {
7901 "description": "FileContentResponse contains information about a repo's file stats and content",
7902 "type": "object",
7903 "properties": {
7904 "_links": {
7905 "$ref": "#/definitions/FileLinksResponse"
7906 },
7907 "download_url": {
7908 "type": "string",
7909 "x-go-name": "DownloadURL"
7910 },
7911 "git_url": {
7912 "type": "string",
7913 "x-go-name": "GitURL"
7914 },
7915 "html_url": {
7916 "type": "string",
7917 "x-go-name": "HTMLURL"
7918 },
7919 "name": {
7920 "type": "string",
7921 "x-go-name": "Name"
7922 },
7923 "path": {
7924 "type": "string",
7925 "x-go-name": "Path"
7926 },
7927 "sha": {
7928 "type": "string",
7929 "x-go-name": "SHA"
7930 },
7931 "size": {
7932 "type": "integer",
7933 "format": "int64",
7934 "x-go-name": "Size"
7935 },
7936 "type": {
7937 "type": "string",
7938 "x-go-name": "Type"
7939 },
7940 "url": {
7941 "type": "string",
7942 "x-go-name": "URL"
7943 }
7944 },
7945 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
7946 },
7947 "FileDeleteResponse": {
7948 "description": "FileDeleteResponse contains information about a repo's file that was deleted",
7949 "type": "object",
7950 "properties": {
7951 "commit": {
7952 "$ref": "#/definitions/FileCommitResponse"
7953 },
7954 "content": {
7955 "type": "object",
7956 "x-go-name": "Content"
7957 },
7958 "verification": {
7959 "$ref": "#/definitions/PayloadCommitVerification"
7960 }
7961 },
7962 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
7963 },
7964 "FileLinksResponse": {
7965 "description": "FileLinksResponse contains the links for a repo's file",
7966 "type": "object",
7967 "properties": {
7968 "git_url": {
7969 "type": "string",
7970 "x-go-name": "GitURL"
7971 },
7972 "html_url": {
7973 "type": "string",
7974 "x-go-name": "HTMLURL"
7975 },
7976 "url": {
7977 "type": "string",
7978 "x-go-name": "Self"
7979 }
7980 },
7981 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
7982 },
7983 "FileResponse": {
7984 "description": "FileResponse contains information about a repo's file",
7985 "type": "object",
7986 "properties": {
7987 "commit": {
7988 "$ref": "#/definitions/FileCommitResponse"
7989 },
7990 "content": {
7991 "$ref": "#/definitions/FileContentResponse"
7992 },
7993 "verification": {
7994 "$ref": "#/definitions/PayloadCommitVerification"
7995 }
7996 },
7997 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
7998 },
7570 "GPGKey": {7999 "GPGKey": {
7571 "description": "GPGKey a user GPG key to sign commit and tag in repository",8000 "description": "GPGKey a user GPG key to sign commit and tag in repository",
7572 "type": "object",8001 "type": "object",
@@ -7646,6 +8075,34 @@ · 73 unmodified lines
7646 },8075 },
7647 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"8076 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
7648 },8077 },
8078 "GitBlobResponse": {
8079 "description": "GitBlobResponse represents a git blob",
8080 "type": "object",
8081 "properties": {
8082 "content": {
8083 "type": "string",
8084 "x-go-name": "Content"
8085 },
8086 "encoding": {
8087 "type": "string",
8088 "x-go-name": "Encoding"
8089 },
8090 "sha": {
8091 "type": "string",
8092 "x-go-name": "SHA"
8093 },
8094 "size": {
8095 "type": "integer",
8096 "format": "int64",
8097 "x-go-name": "Size"
8098 },
8099 "url": {
8100 "type": "string",
8101 "x-go-name": "URL"
8102 }
8103 },
8104 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
8105 },
7649 "GitEntry": {8106 "GitEntry": {
7650 "description": "GitEntry represents a git tree",8107 "description": "GitEntry represents a git tree",
7651 "type": "object",8108 "type": "object",
@@ -7796,6 +8253,22 @@ · 144 unmodified lines
7796 },8253 },
7797 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"8254 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
7798 },8255 },
8256 "Identity": {
8257 "description": "Identity for a person's identity like an author or committer",
8258 "type": "object",
8259 "properties": {
8260 "email": {
8261 "type": "string",
8262 "format": "email",
8263 "x-go-name": "Email"
8264 },
8265 "name": {
8266 "type": "string",
8267 "x-go-name": "Name"
8268 }
8269 },
8270 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
8271 },
7799 "Issue": {8272 "Issue": {
7800 "description": "Issue represents an issue in a repository",8273 "description": "Issue represents an issue in a repository",
7801 "type": "object",8274 "type": "object",
@@ -8855,6 +9328,43 @@ · 1053 unmodified lines
8855 },9328 },
8856 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"9329 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
8857 },9330 },
9331 "UpdateFileOptions": {
9332 "description": "UpdateFileOptions options for updating files",
9333 "type": "object",
9334 "properties": {
9335 "author": {
9336 "$ref": "#/definitions/Identity"
9337 },
9338 "branch": {
9339 "type": "string",
9340 "x-go-name": "BranchName"
9341 },
9342 "committer": {
9343 "$ref": "#/definitions/Identity"
9344 },
9345 "content": {
9346 "type": "string",
9347 "x-go-name": "Content"
9348 },
9349 "from_path": {
9350 "type": "string",
9351 "x-go-name": "FromPath"
9352 },
9353 "message": {
9354 "type": "string",
9355 "x-go-name": "Message"
9356 },
9357 "new_branch": {
9358 "type": "string",
9359 "x-go-name": "NewBranchName"
9360 },
9361 "sha": {
9362 "type": "string",
9363 "x-go-name": "SHA"
9364 }
9365 },
9366 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
9367 },
8858 "User": {9368 "User": {
8859 "description": "User represents a user",9369 "description": "User represents a user",
8860 "type": "object",9370 "type": "object",
@@ -9040,6 +9550,24 @@ · 179 unmodified lines
9040 }9550 }
9041 }9551 }
9042 },9552 },
9553 "FileContentResponse": {
9554 "description": "FileContentResponse",
9555 "schema": {
9556 "$ref": "#/definitions/FileContentResponse"
9557 }
9558 },
9559 "FileDeleteResponse": {
9560 "description": "FileDeleteResponse",
9561 "schema": {
9562 "$ref": "#/definitions/FileDeleteResponse"
9563 }
9564 },
9565 "FileResponse": {
9566 "description": "FileResponse",
9567 "schema": {
9568 "$ref": "#/definitions/FileResponse"
9569 }
9570 },
9043 "GPGKey": {9571 "GPGKey": {
9044 "description": "GPGKey",9572 "description": "GPGKey",
9045 "schema": {9573 "schema": {
@@ -9055,6 +9583,12 @@ · 9 unmodified lines
9055 }9583 }
9056 }9584 }
9057 },9585 },
9586 "GitBlobResponse": {
9587 "description": "GitBlobResponse",
9588 "schema": {
9589 "$ref": "#/definitions/GitBlobResponse"
9590 }
9591 },
9058 "GitHook": {9592 "GitHook": {
9059 "description": "GitHook",9593 "description": "GitHook",
9060 "schema": {9594 "schema": {
@@ -9362,7 +9896,7 @@ · 301 unmodified lines
9362 "parameterBodies": {9896 "parameterBodies": {
9363 "description": "parameterBodies",9897 "description": "parameterBodies",
9364 "schema": {9898 "schema": {
9365 "$ref": "#/definitions/EditAttachmentOptions"9899 "$ref": "#/definitions/DeleteFileOptions"
9366 }9900 }
9367 },9901 },
9368 "redirect": {9902 "redirect": {