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
672672 DEFAULT_PAGING_NUM = 30
673673 ; Default and maximum number of items per page for git trees api
674674 DEFAULT_GIT_TREES_PER_PAGE = 1000
675+; Default size of a blob returned by the blobs API (default is 10MiB)
676+DEFAULT_MAX_BLOB_SIZE = 10485760
675677
676678 [oauth2]
677679 ; 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
402402
403403 - `ENABLE_SWAGGER`: **true**: Enables /api/swagger, /api/v1/swagger etc. endpoints. True or false; default is true.
404404 - `MAX_RESPONSE_ITEMS`: **50**: Max number of items in a page.
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.
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.
407+- `DEFAULT_MAX_BLOB_SIZE`: **10485760**: Default max size of a blob that can be return by the blobs API.
407408
408409 ## OAuth2 (`oauth2`)
409410
docs/content/doc/advanced/config-cheat-sheet.zh-cn.md+2−1
@@ -215,7 +215,8 @@ menu: · 214 unmodified lines
215215 - `ENABLE_SWAGGER`: **true**: 是否启用swagger路由 /api/swagger, /api/v1/swagger etc. endpoints. True 或 false; 默认是 true.
216216 - `MAX_RESPONSE_ITEMS`: **50**: 一个页面最大的项目数。
217217 - `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默认最大大小.
219220
220221 ## Markup (`markup`)
221222
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+
5+package integrations
6+
7+import (
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+
21+func 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+
42+func 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+
5+package integrations
6+
7+import (
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+
24+func 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+
45+func 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+
93+func 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+
5+package integrations
6+
7+import (
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+
20+func 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+
39+func 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+
5+package integrations
6+
7+import (
8+ "code.gitea.io/gitea/models"
9+ "code.gitea.io/gitea/modules/repofiles"
10+ api "code.gitea.io/sdk/gitea"
11+)
12+
13+func 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+
25+func 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+
5+package integrations
6+
7+import (
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+
24+func 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+
33+func 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+
81+func 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+
5+package integrations
6+
7+import (
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+
17+func 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+
5+package integrations
6+
7+import (
8+ "net/http"
9+ "testing"
10+
11+ "code.gitea.io/gitea/models"
12+)
13+
14+func 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
2222 "testing"
2323
2424 "code.gitea.io/gitea/models"
25+ "code.gitea.io/gitea/modules/base"
2526 "code.gitea.io/gitea/modules/setting"
2627 "code.gitea.io/gitea/routers"
2728 "code.gitea.io/gitea/routers/routes"
@@ -96,7 +97,7 @@ func TestMain(m *testing.M) { · 68 unmodified lines
9697 }
9798
9899 func initIntegrationTest() {
99− giteaRoot := os.Getenv("GITEA_ROOT")
100+ giteaRoot := base.SetupGiteaRoot()
100101 if giteaRoot == "" {
101102 fmt.Println("Environment variable $GITEA_ROOT not set")
102103 os.Exit(1)
integrations/migration-test/migration_test.go+2−1
@@ -18,6 +18,7 @@ import ( · 17 unmodified lines
1818 "code.gitea.io/gitea/integrations"
1919 "code.gitea.io/gitea/models"
2020 "code.gitea.io/gitea/models/migrations"
21+ "code.gitea.io/gitea/modules/base"
2122 "code.gitea.io/gitea/modules/setting"
2223
2324 "github.com/go-xorm/xorm"
@@ -28,7 +29,7 @@ var currentEngine *xorm.Engine · 4 unmodified lines
2829
2930 func initMigrationTest(t *testing.T) {
3031 integrations.PrintCurrentTest(t, 2)
31− giteaRoot := os.Getenv("GITEA_ROOT")
32+ giteaRoot := base.SetupGiteaRoot()
3233 if giteaRoot == "" {
3334 integrations.Printf("Environment variable $GITEA_ROOT not set\n")
3435 os.Exit(1)
models/error.go+150−18
@@ -1,10 +1,15 @@
11 // Copyright 2015 The Gogs Authors. All rights reserved.
2+// Copyright 2019 The Gitea Authors. All rights reserved.
23 // Use of this source code is governed by a MIT-style
34 // license that can be found in the LICENSE file.
45
56 package models
67
7−import "fmt"
8+import (
9+ "fmt"
10+
11+ "code.gitea.io/gitea/modules/git"
12+)
813
914 // ErrNameReserved represents a "reserved name" error.
1015 type ErrNameReserved struct {
@@ -26,8 +31,7 @@ type ErrNamePatternNotAllowed struct { · 15 unmodified lines
2631 Pattern string
2732 }
2833
29−// IsErrNamePatternNotAllowed checks if an error is an
30−// ErrNamePatternNotAllowed.
34+// IsErrNamePatternNotAllowed checks if an error is an ErrNamePatternNotAllowed.
3135 func IsErrNamePatternNotAllowed(err error) bool {
3236 _, ok := err.(ErrNamePatternNotAllowed)
3337 return ok
@@ -676,7 +680,7 @@ type ErrRepoRedirectNotExist struct { · 642 unmodified lines
676680 RepoName string
677681 }
678682
679−// IsErrRepoRedirectNotExist check if an error is an ErrRepoRedirectNotExist
683+// IsErrRepoRedirectNotExist check if an error is an ErrRepoRedirectNotExist.
680684 func IsErrRepoRedirectNotExist(err error) bool {
681685 _, ok := err.(ErrRepoRedirectNotExist)
682686 return ok
@@ -765,28 +769,95 @@ func (err ErrInvalidTagName) Error() string { · 82 unmodified lines
765769 return fmt.Sprintf("release tag name is not valid [tag_name: %s]", err.TagName)
766770 }
767771
768−// ErrRepoFileAlreadyExist represents a "RepoFileAlreadyExist" kind of error.
769−type ErrRepoFileAlreadyExist struct {
770− FileName string
772+// ErrRepoFileAlreadyExists represents a "RepoFileAlreadyExist" kind of error.
773+type ErrRepoFileAlreadyExists struct {
774+ Path string
775+}
776+
777+// IsErrRepoFileAlreadyExists checks if an error is a ErrRepoFileAlreadyExists.
778+func IsErrRepoFileAlreadyExists(err error) bool {
779+ _, ok := err.(ErrRepoFileAlreadyExists)
780+ return ok
781+}
782+
783+func (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.
788+type ErrRepoFileDoesNotExist struct {
789+ Path string
790+ Name string
791+}
792+
793+// IsErrRepoFileDoesNotExist checks if an error is a ErrRepoDoesNotExist.
794+func IsErrRepoFileDoesNotExist(err error) bool {
795+ _, ok := err.(ErrRepoFileDoesNotExist)
796+ return ok
797+}
798+
799+func (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.
804+type ErrFilenameInvalid struct {
805+ Path string
806+}
807+
808+// IsErrFilenameInvalid checks if an error is an ErrFilenameInvalid.
809+func IsErrFilenameInvalid(err error) bool {
810+ _, ok := err.(ErrFilenameInvalid)
811+ return ok
812+}
813+
814+func (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.
819+type ErrUserCannotCommit struct {
820+ UserName string
821+}
822+
823+// IsErrUserCannotCommit checks if an error is an ErrUserCannotCommit.
824+func IsErrUserCannotCommit(err error) bool {
825+ _, ok := err.(ErrUserCannotCommit)
826+ return ok
827+}
828+
829+func (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.
834+type ErrFilePathInvalid struct {
835+ Message string
836+ Path string
837+ Name string
838+ Type git.EntryMode
771839 }
772840
773−// IsErrRepoFileAlreadyExist checks if an error is a ErrRepoFileAlreadyExist.
774−func IsErrRepoFileAlreadyExist(err error) bool {
775− _, ok := err.(ErrRepoFileAlreadyExist)
841+// IsErrFilePathInvalid checks if an error is an ErrFilePathInvalid.
842+func IsErrFilePathInvalid(err error) bool {
843+ _, ok := err.(ErrFilePathInvalid)
776844 return ok
777845 }
778846
779−func (err ErrRepoFileAlreadyExist) Error() string {
780− return fmt.Sprintf("repository file already exists [file_name: %s]", err.FileName)
847+func (err ErrFilePathInvalid) Error() string {
848+ if err.Message != "" {
849+ return err.Message
850+ }
851+ return fmt.Sprintf("path is invalid [path: %s]", err.Path)
781852 }
782853
783−// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo
854+// ErrUserDoesNotHaveAccessToRepo represets an error where the user doesn't has access to a given repo.
784855 type ErrUserDoesNotHaveAccessToRepo struct {
785856 UserID int64
786857 RepoName string
787858 }
788859
789−// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExist.
860+// IsErrUserDoesNotHaveAccessToRepo checks if an error is a ErrRepoFileAlreadyExists.
790861 func IsErrUserDoesNotHaveAccessToRepo(err error) bool {
791862 _, ok := err.(ErrUserDoesNotHaveAccessToRepo)
792863 return ok
@@ -818,7 +889,7 @@ func (err ErrBranchNotExist) Error() string { · 25 unmodified lines
818889 return fmt.Sprintf("branch does not exist [name: %s]", err.Name)
819890 }
820891
821−// ErrBranchAlreadyExists represents an error that branch with such name already exists
892+// ErrBranchAlreadyExists represents an error that branch with such name already exists.
822893 type ErrBranchAlreadyExists struct {
823894 BranchName string
824895 }
@@ -833,7 +904,7 @@ func (err ErrBranchAlreadyExists) Error() string { · 8 unmodified lines
833904 return fmt.Sprintf("branch already exists [name: %s]", err.BranchName)
834905 }
835906
836−// ErrBranchNameConflict represents an error that branch name conflicts with other branch
907+// ErrBranchNameConflict represents an error that branch name conflicts with other branch.
837908 type ErrBranchNameConflict struct {
838909 BranchName string
839910 }
@@ -848,7 +919,7 @@ func (err ErrBranchNameConflict) Error() string { · 8 unmodified lines
848919 return fmt.Sprintf("branch conflicts with existing branch [name: %s]", err.BranchName)
849920 }
850921
851−// ErrNotAllowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it
922+// ErrNotAllowedToMerge represents an error that a branch is protected and the current user is not allowed to modify it.
852923 type ErrNotAllowedToMerge struct {
853924 Reason string
854925 }
@@ -863,7 +934,7 @@ func (err ErrNotAllowedToMerge) Error() string { · 8 unmodified lines
863934 return fmt.Sprintf("not allowed to merge [reason: %s]", err.Reason)
864935 }
865936
866−// ErrTagAlreadyExists represents an error that tag with such name already exists
937+// ErrTagAlreadyExists represents an error that tag with such name already exists.
867938 type ErrTagAlreadyExists struct {
868939 TagName string
869940 }
@@ -878,6 +949,67 @@ func (err ErrTagAlreadyExists) Error() string { · 8 unmodified lines
878949 return fmt.Sprintf("tag already exists [name: %s]", err.TagName)
879950 }
880951
952+// ErrSHADoesNotMatch represents a "SHADoesNotMatch" kind of error.
953+type ErrSHADoesNotMatch struct {
954+ Path string
955+ GivenSHA string
956+ CurrentSHA string
957+}
958+
959+// IsErrSHADoesNotMatch checks if an error is a ErrSHADoesNotMatch.
960+func IsErrSHADoesNotMatch(err error) bool {
961+ _, ok := err.(ErrSHADoesNotMatch)
962+ return ok
963+}
964+
965+func (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.
970+type ErrSHANotFound struct {
971+ SHA string
972+}
973+
974+// IsErrSHANotFound checks if an error is a ErrSHANotFound.
975+func IsErrSHANotFound(err error) bool {
976+ _, ok := err.(ErrSHANotFound)
977+ return ok
978+}
979+
980+func (err ErrSHANotFound) Error() string {
981+ return fmt.Sprintf("sha not found [%s]", err.SHA)
982+}
983+
984+// ErrCommitIDDoesNotMatch represents a "CommitIDDoesNotMatch" kind of error.
985+type ErrCommitIDDoesNotMatch struct {
986+ GivenCommitID string
987+ CurrentCommitID string
988+}
989+
990+// IsErrCommitIDDoesNotMatch checks if an error is a ErrCommitIDDoesNotMatch.
991+func IsErrCommitIDDoesNotMatch(err error) bool {
992+ _, ok := err.(ErrCommitIDDoesNotMatch)
993+ return ok
994+}
995+
996+func (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.
1001+type ErrSHAOrCommitIDNotProvided struct{}
1002+
1003+// IsErrSHAOrCommitIDNotProvided checks if an error is a ErrSHAOrCommitIDNotProvided.
1004+func IsErrSHAOrCommitIDNotProvided(err error) bool {
1005+ _, ok := err.(ErrSHAOrCommitIDNotProvided)
1006+ return ok
1007+}
1008+
1009+func (err ErrSHAOrCommitIDNotProvided) Error() string {
1010+ return fmt.Sprintf("a SHA or commmit ID must be proved when updating a file")
1011+}
1012+
8811013 // __ __ ___. .__ __
8821014 // / \ / \ ____\_ |__ | |__ ____ ____ | | __
8831015 // \ \/\/ // __ \| __ \| | \ / _ \ / _ \| |/ /
models/unit_tests.go+2−0
@@ -14,6 +14,7 @@ import ( · 13 unmodified lines
1414 "testing"
1515 "time"
1616
17+ "code.gitea.io/gitea/modules/base"
1718 "code.gitea.io/gitea/modules/setting"
1819
1920 "github.com/Unknwon/com"
@@ -116,6 +117,7 @@ func PrepareTestEnv(t testing.TB) { · 96 unmodified lines
116117 assert.NoError(t, removeAllWithRetry(setting.RepoRootPath))
117118 metaPath := filepath.Join(giteaRoot, "integrations", "gitea-repositories-meta")
118119 assert.NoError(t, com.CopyDir(metaPath, setting.RepoRootPath))
120+ base.SetupGiteaRoot() // Makes sure GITEA_ROOT is set
119121 }
120122
121123 type testCond struct {
modules/auth/repo_form.go+1−0
@@ -590,6 +590,7 @@ type DeleteRepoFileForm struct { · 589 unmodified lines
590590 CommitMessage string
591591 CommitChoice string `binding:"Required;MaxSize(50)"`
592592 NewBranchName string `binding:"GitRefName;MaxSize(100)"`
593+ LastCommit string
593594 }
594595
595596 // Validate validates the fields
modules/base/tool.go+25−0
@@ -16,7 +16,10 @@ import ( · 15 unmodified lines
1616 "math"
1717 "net/http"
1818 "net/url"
19+ "os"
1920 "path"
21+ "path/filepath"
22+ "runtime"
2023 "strconv"
2124 "strings"
2225 "time"
@@ -603,3 +606,25 @@ func EntryIcon(entry *git.TreeEntry) string { · 580 unmodified lines
603606
604607 return "file-text"
605608 }
609+
610+// SetupGiteaRoot Sets GITEA_ROOT if it is not already set and returns the value
611+func 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
128128 return ""
129129 }
130130
131+// FileExists returns true if a file exists in the given repo branch
132+func (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+
131146 // GetEditorconfig returns the .editorconfig definition if found in the
132147 // HEAD of the default repo branch.
133148 func (r *Repository) GetEditorconfig() (*editorconfig.Editorconfig, error) {
modules/git/blob.go+30−0
@@ -6,6 +6,7 @@ package git · 5 unmodified lines
66
77 import (
88 "bytes"
9+ "encoding/base64"
910 "fmt"
1011 "io"
1112 "io/ioutil"
@@ -71,3 +72,32 @@ func (b *Blob) DataAsync() (io.ReadCloser, error) { · 59 unmodified lines
7172
7273 return cmdReadCloser{stdout: stdout, cmd: cmd}, nil
7374 }
75+
76+// GetBlobContentBase64 Reads the content of the blob with a base64 encode and returns the encoded string
77+func (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
263263 return c.repo.getFilesChanged(pastCommit, c.ID.String())
264264 }
265265
266+// FileChangedSinceCommit Returns true if the file given has changed since the the past commit
267+func (c *Commit) FileChangedSinceCommit(filename, pastCommit string) (bool, error) {
268+ return c.repo.FileChangedBetweenCommits(filename, pastCommit, c.ID.String())
269+}
270+
266271 // GetSubModules get all the sub modules of current revision git tree
267272 func (c *Commit) GetSubModules() (*ObjectCache, error) {
268273 if c.submoduleCache != nil {
modules/git/repo_commit.go+11−2
@@ -10,7 +10,7 @@ import ( · 9 unmodified lines
1010 "strconv"
1111 "strings"
1212
13− version "github.com/mcuadros/go-version"
13+ "github.com/mcuadros/go-version"
1414 )
1515
1616 // 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
270270 return repo.parsePrettyFormatLogToList(stdout)
271271 }
272272
273−func (repo *Repository) getFilesChanged(id1 string, id2 string) ([]string, error) {
273+func (repo *Repository) getFilesChanged(id1, id2 string) ([]string, error) {
274274 stdout, err := NewCommand("diff", "--name-only", id1, id2).RunInDirBytes(repo.Path)
275275 if err != nil {
276276 return nil, err
@@ -278,6 +278,15 @@ func (repo *Repository) getFilesChanged(id1 string, id2 string) ([]string, error · 1 unmodified line
278278 return strings.Split(string(stdout), "\n"), nil
279279 }
280280
281+// FileChangedBetweenCommits Returns true if the file changed between commit IDs id1 and id2
282+func (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+
281290 // FileCommitsCount return the number of files at a revison
282291 func (repo *Repository) FileCommitsCount(revision, file string) (int64, error) {
283292 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+
5+package repofiles
6+
7+import (
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.
15+func 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+
5+package repofiles
6+
7+import (
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+
17+func 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+
5+package repofiles
6+
7+import (
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
16+func 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+
5+package repofiles
6+
7+import (
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+
18+func TestMain(m *testing.M) {
19+ models.MainTest(m, filepath.Join("..", ".."))
20+}
21+
22+func 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+
63+func 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+
5+package repofiles
6+
7+import (
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
17+type 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
29+func 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+
5+package repofiles
6+
7+import (
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+
17+func 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+
30+func 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+
69+func 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
98+func 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+
121+func 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
22 // Use of this source code is governed by a MIT-style
33 // license that can be found in the LICENSE file.
44
5−package uploader
5+package repofiles
66
77 import (
88 "strings"
@@ -12,11 +12,14 @@ import ( · 3 unmodified lines
1212
1313 // GetDiffPreview produces and returns diff result of a file which is not yet committed.
1414 func GetDiffPreview(repo *models.Repository, branch, treePath, content string) (*models.Diff, error) {
15+ if branch == "" {
16+ branch = repo.DefaultBranch
17+ }
1518 t, err := NewTemporaryUploadRepository(repo)
16− defer t.Close()
1719 if err != nil {
1820 return nil, err
1921 }
22+ defer t.Close()
2023 if err := t.Clone(branch); err != nil {
2124 return nil, err
2225 }
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+
5+package repofiles
6+
7+import (
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+
16+func 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+
113+func 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+
5+package repofiles
6+
7+import (
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
19+func 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
32+func 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
83+func 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+
5+package repofiles
6+
7+import (
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+
18+func 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+
72+func 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+
5+package repofiles
6+
7+import (
8+ "path"
9+ "strings"
10+)
11+
12+// CleanUploadFileName Trims a filename and returns empty string if it is a .git directory
13+func 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+
5+package repofiles
6+
7+import (
8+ "testing"
9+
10+ "github.com/stretchr/testify/assert"
11+)
12+
13+func 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
22 // Use of this source code is governed by a MIT-style
33 // license that can be found in the LICENSE file.
44
5−package uploader
5+package repofiles
66
77 import (
88 "bytes"
@@ -12,19 +12,22 @@ import ( · 3 unmodified lines
1212 "os"
1313 "os/exec"
1414 "path"
15+ "regexp"
1516 "strings"
1617 "time"
1718
1819 "code.gitea.io/gitea/models"
20+ "code.gitea.io/gitea/modules/git"
1921 "code.gitea.io/gitea/modules/process"
2022 "code.gitea.io/gitea/modules/setting"
2123
2224 "github.com/Unknwon/com"
2325 )
2426
25−// TemporaryUploadRepository is a type to wrap our upload repositories
27+// TemporaryUploadRepository is a type to wrap our upload repositories as a shallow clone
2628 type TemporaryUploadRepository struct {
2729 repo *models.Repository
30+ gitRepo *git.Repository
2831 basePath string
2932 }
3033
@@ -33,7 +36,10 @@ func NewTemporaryUploadRepository(repo *models.Repository) (*TemporaryUploadRepo · 2 unmodified lines
3336 timeStr := com.ToStr(time.Now().Nanosecond()) // SHOULD USE SOMETHING UNIQUE
3437 basePath := path.Join(models.LocalCopyPath(), "upload-"+timeStr+".git")
3538 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")
3743 }
3844 t := &TemporaryUploadRepository{repo: repo, basePath: basePath}
3945 return t, nil
@@ -51,8 +57,26 @@ func (t *TemporaryUploadRepository) Clone(branch string) error { · 11 unmodified lines
5157 if _, stderr, err := process.GetManager().ExecTimeout(5*time.Minute,
5258 fmt.Sprintf("Clone (git clone -s --bare): %s", t.basePath),
5359 "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
5578 }
79+ t.gitRepo = gitRepo
5680 return nil
5781 }
5882
@@ -186,6 +210,12 @@ func (t *TemporaryUploadRepository) AddObjectToIndex(mode, objectHash, objectPat · 127 unmodified lines
186210 t.basePath,
187211 fmt.Sprintf("addObjectToIndex (git update-index): %s", t.basePath),
188212 "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+ }
189219 return fmt.Errorf("git update-index: %s", stderr)
190220 }
191221 return nil
@@ -201,22 +231,42 @@ func (t *TemporaryUploadRepository) WriteTree() (string, error) { · 9 unmodified lines
201231 return "", fmt.Errorf("git write-tree: %s", stderr)
202232 }
203233 return strings.TrimSpace(treeHash), nil
234+}
204235
236+// GetLastCommit gets the last commit ID SHA of the repo
237+func (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
242+func (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
205254 }
206255
207256 // CommitTree creates a commit from a given tree for the user with provided message
208−func (t *TemporaryUploadRepository) CommitTree(doer *models.User, treeHash string, message string) (string, error) {
257+func (t *TemporaryUploadRepository) CommitTree(author, committer *models.User, treeHash string, message string) (string, error) {
209258 commitTimeStr := time.Now().Format(time.UnixDate)
210− sig := doer.NewGitSig()
259+ authorSig := author.NewGitSig()
260+ committerSig := committer.NewGitSig()
211261
212262 // FIXME: Should we add SSH_ORIGINAL_COMMAND to this
213263 // Because this may call hooks we should pass in the environment
214264 env := append(os.Environ(),
215− "GIT_AUTHOR_NAME="+sig.Name,
216− "GIT_AUTHOR_EMAIL="+sig.Email,
265+ "GIT_AUTHOR_NAME="+authorSig.Name,
266+ "GIT_AUTHOR_EMAIL="+authorSig.Email,
217267 "GIT_AUTHOR_DATE="+commitTimeStr,
218− "GIT_COMMITTER_NAME="+sig.Name,
219− "GIT_COMMITTER_EMAIL="+sig.Email,
268+ "GIT_COMMITTER_NAME="+committerSig.Name,
269+ "GIT_COMMITTER_EMAIL="+committerSig.Email,
220270 "GIT_COMMITTER_DATE="+commitTimeStr,
221271 )
222272 commitHash, stderr, err := process.GetManager().ExecDirEnv(5*time.Minute,
@@ -357,3 +407,19 @@ func (t *TemporaryUploadRepository) CheckAttribute(attribute string, args ...str · 134 unmodified lines
357407
358408 return name2attribute2info, err
359409 }
410+
411+// GetBranchCommit Gets the commit object of the given branch
412+func (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
420+func (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+
5+package repofiles
6+
7+import (
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.
17+func 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
22 // Use of this source code is governed by a MIT-style
33 // license that can be found in the LICENSE file.
44
5−package repo
5+package repofiles
66
77 import (
8− "github.com/stretchr/testify/assert"
98 "testing"
109
1110 "code.gitea.io/gitea/models"
12− "code.gitea.io/gitea/modules/context"
1311 "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"
1515 )
1616
1717 func TestGetTreeBySHA(t *testing.T) {
1818 models.PrepareTestEnv(t)
19− sha := "master"
2019 ctx := test.MockContext(t, "user2/repo1")
21− ctx.SetParams(":id", "1")
22− ctx.SetParams(":sha", sha)
2320 test.LoadRepo(t, ctx, 1)
2421 test.LoadRepoCommit(t, ctx)
2522 test.LoadUser(t, ctx, 2)
2623 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)
2729
28− tree := GetTreeBySHA(&context.APIContext{Context: ctx, Org: nil}, ctx.Params("sha"))
29− expectedTree := &gitea.GitTreeResponse{
30+ tree, err := GetTreeBySHA(ctx.Repo.Repository, ctx.Params(":sha"), page, perPage, true)
31+ assert.Nil(t, err)
32+ expectedTree := &api.GitTreeResponse{
3033 SHA: "65f1bf27bc3bf70f64657658635e66094edbcb4d",
3134 URL: "https://try.gitea.io/api/v1/repos/user2/repo1/git/trees/65f1bf27bc3bf70f64657658635e66094edbcb4d",
32− Entries: []gitea.GitEntry{
35+ Entries: []api.GitEntry{
3336 {
3437 Path: "README.md",
3538 Mode: "100644",
@@ -43,6 +46,5 @@ func TestGetTreeBySHA(t *testing.T) { · 7 unmodified lines
4346 Page: 1,
4447 TotalCount: 1,
4548 }
46−
4749 assert.EqualValues(t, tree, expectedTree)
4850 }
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+
5+package repofiles
6+
7+import (
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
20+type IdentityOptions struct {
21+ Name string
22+ Email string
23+}
24+
25+// UpdateRepoFileOptions holds the repository file update options
26+type 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
41+func 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+
5+package repofiles
6+
7+import (
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+
19+func 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+
32+func 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+
46+func 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+
105+func 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+
164+func 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+
192+func 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+
220+func 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
259+func 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+
285+func 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
22 // Use of this source code is governed by a MIT-style
33 // license that can be found in the LICENSE file.
44
5−package uploader
5+package repofiles
66
77 import (
88 "fmt"
@@ -127,8 +127,12 @@ func UploadRepoFiles(repo *models.Repository, doer *models.User, opts *UploadRep · 118 unmodified lines
127127 return err
128128 }
129129
130+ // make author and committer the doer
131+ author := doer
132+ committer := doer
133+
130134 // Now commit the tree
131− commitHash, err := t.CommitTree(doer, treeHash, opts.Message)
135+ commitHash, err := t.CommitTree(author, committer, treeHash, opts.Message)
132136 if err != nil {
133137 return err
134138 }
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+
5+package repofiles
6+
7+import (
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
14+func 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
293293 MaxResponseItems int
294294 DefaultPagingNum int
295295 DefaultGitTreesPerPage int
296+ DefaultMaxBlobSize int64
296297 }{
297298 EnableSwagger: true,
298299 MaxResponseItems: 50,
299300 DefaultPagingNum: 30,
300301 DefaultGitTreesPerPage: 1000,
302+ DefaultMaxBlobSize: 10485760,
301303 }
302304
303305 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−
5−package uploader
6−
7−import (
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
15−type 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
24−func 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−
5−package uploader
6−
7−import (
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
18−type 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
30−func 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
667667 editor.or = or
668668 editor.cancel_lower = Cancel
669669 editor.commit_changes = Commit Changes
670−editor.add_tmpl = Add '%s/<filename>'
670+editor.add_tmpl = Add '<filename>'
671671 editor.add = Add '%s'
672672 editor.update = Update '%s'
673673 editor.delete = Delete '%s'
@@ -677,11 +677,14 @@ editor.create_new_branch = Create a <strong>new branch</strong> for this commit · 3 unmodified lines
677677 editor.new_branch_name_desc = New branch name…
678678 editor.cancel = Cancel
679679 editor.filename_cannot_be_empty = The filename cannot be empty.
680+editor.filename_is_invalid = The filename is invalid: '%s'.
681+editor.branch_does_not_exist = Branch '%s' does not exist in this repository.
680682 editor.branch_already_exists = Branch '%s' already exists in this repository.
681683 editor.directory_is_a_file = Directory name '%s' is already used as a filename in this repository.
682684 editor.file_is_a_symlink = '%s' is a symbolic link. Symbolic links cannot be edited in the web editor
683685 editor.filename_is_a_directory = Filename '%s' is already used as a directory name in this repository.
684686 editor.file_editing_no_longer_exists = The file being edited, '%s', no longer exists in this repository.
687+editor.file_deleting_no_longer_exists = The file being deleted, '%s', no longer exists in this repository.
685688 editor.file_changed_while_editing = The file contents have changed since you started editing. <a target="_blank" rel="noopener noreferrer" href="%s">Click here</a> to see them or <strong>Commit Changes again</strong> to overwrite them.
686689 editor.file_already_exists = A file named '%s' already exists in this repository.
687690 editor.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
659659 })
660660 m.Get("/refs", repo.GetGitAllRefs)
661661 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())
663672 }, reqRepoReader(models.UnitTypeCode))
664673 }, repoAssignment())
665674 })
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+
5+package repo
6+
7+import (
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.
15+func 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
66 package repo
77
88 import (
9+ "encoding/base64"
10+ "net/http"
11+
912 "code.gitea.io/gitea/models"
1013 "code.gitea.io/gitea/modules/context"
1114 "code.gitea.io/gitea/modules/git"
15+ "code.gitea.io/gitea/modules/repofiles"
1216 "code.gitea.io/gitea/routers/repo"
17+ api "code.gitea.io/sdk/gitea"
1318 )
1419
1520 // GetRawFile get a file by path on a repository
@@ -48,12 +53,12 @@ func GetRawFile(ctx *context.APIContext) { · 32 unmodified lines
4853 if git.IsErrNotExist(err) {
4954 ctx.NotFound()
5055 } else {
51− ctx.Error(500, "GetBlobByPath", err)
56+ ctx.Error(http.StatusInternalServerError, "GetBlobByPath", err)
5257 }
5358 return
5459 }
5560 if err = repo.ServeBlob(ctx.Context, blob); err != nil {
56− ctx.Error(500, "ServeBlob", err)
61+ ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
5762 }
5863 }
5964
@@ -86,7 +91,7 @@ func GetArchive(ctx *context.APIContext) { · 26 unmodified lines
8691 repoPath := models.RepoPath(ctx.Params(":username"), ctx.Params(":reponame"))
8792 gitRepo, err := git.OpenRepository(repoPath)
8893 if err != nil {
89− ctx.Error(500, "OpenRepository", err)
94+ ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
9095 return
9196 }
9297 ctx.Repo.GitRepo = gitRepo
@@ -125,7 +130,7 @@ func GetEditorconfig(ctx *context.APIContext) { · 32 unmodified lines
125130 if git.IsErrNotExist(err) {
126131 ctx.NotFound(err)
127132 } else {
128− ctx.Error(500, "GetEditorconfig", err)
133+ ctx.Error(http.StatusInternalServerError, "GetEditorconfig", err)
129134 }
130135 return
131136 }
@@ -136,5 +141,264 @@ func GetEditorconfig(ctx *context.APIContext) { · 4 unmodified lines
136141 ctx.NotFound(err)
137142 return
138143 }
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.
148+func 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.
153+func CanReadFiles(r *context.Repository) bool {
154+ return r.Permission.CanRead(models.UnitTypeCode)
155+}
156+
157+// CreateFile handles API call for creating a file
158+func 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
215+func 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
275+func 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
293+func 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
357+func 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+ }
140404 }
routers/api/v1/repo/tree.go+6−88
@@ -5,13 +5,8 @@ · 4 unmodified lines
55 package repo
66
77 import (
8− "fmt"
9− "strings"
10−
118 "code.gitea.io/gitea/modules/context"
12− "code.gitea.io/gitea/modules/git"
13− "code.gitea.io/gitea/modules/setting"
14− "code.gitea.io/sdk/gitea"
9+ "code.gitea.io/gitea/modules/repofiles"
1510 )
1611
1712 // GetTree get the tree of a repository.
@@ -55,92 +50,15 @@ func GetTree(ctx *context.APIContext) { · 37 unmodified lines
5550 // responses:
5651 // "200":
5752 // "$ref": "#/responses/GitTreeResponse"
58− sha := ctx.Params("sha")
53+
54+ sha := ctx.Params(":sha")
5955 if len(sha) == 0 {
6056 ctx.Error(400, "", "sha not provided")
6157 return
6258 }
63− tree := GetTreeBySHA(ctx, sha)
64− if tree != nil {
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.
72−func 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
59+ if tree, err := repofiles.GetTreeBySHA(ctx.Repo.Repository, sha, ctx.QueryInt("page"), ctx.QueryInt("per_page"), ctx.QueryBool("recursive")); err != nil {
60+ ctx.Error(400, "", err.Error())
12561 } else {
126− rangeEnd = len(entries)
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− }
62+ ctx.JSON(200, tree)
14463 }
145− return tree
14664 }
routers/api/v1/swagger/options.go+10−0
@@ -97,6 +97,7 @@ type swaggerParameterBodies struct { · 96 unmodified lines
9797
9898 // in:body
9999 CreateUserOption api.CreateUserOption
100+
100101 // in:body
101102 EditUserOption api.EditUserOption
102103
@@ -105,4 +106,13 @@ type swaggerParameterBodies struct { · 2 unmodified lines
105106
106107 // in:body
107108 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
108118 }
routers/api/v1/swagger/repo.go+28−0
@@ -162,9 +162,37 @@ type swaggerGitTreeResponse struct { · 161 unmodified lines
162162 Body api.GitTreeResponse `json:"body"`
163163 }
164164
165+// GitBlobResponse
166+// swagger:response GitBlobResponse
167+type swaggerGitBlobResponse struct {
168+ //in: body
169+ Body api.GitBlobResponse `json:"body"`
170+}
171+
165172 // Commit
166173 // swagger:response Commit
167174 type swaggerCommit struct {
168175 //in: body
169176 Body api.Commit `json:"body"`
170177 }
178+
179+// FileResponse
180+// swagger:response FileResponse
181+type swaggerFileResponse struct {
182+ //in: body
183+ Body api.FileResponse `json:"body"`
184+}
185+
186+// FileContentResponse
187+// swagger:response FileContentResponse
188+type swaggerFileContentResponse struct {
189+ //in: body
190+ Body api.FileContentResponse `json:"body"`
191+}
192+
193+// FileDeleteResponse
194+// swagger:response FileDeleteResponse
195+type 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
1717 "code.gitea.io/gitea/modules/context"
1818 "code.gitea.io/gitea/modules/git"
1919 "code.gitea.io/gitea/modules/log"
20+ "code.gitea.io/gitea/modules/repofiles"
2021 "code.gitea.io/gitea/modules/setting"
2122 "code.gitea.io/gitea/modules/templates"
22− "code.gitea.io/gitea/modules/uploader"
2323 "code.gitea.io/gitea/modules/util"
2424 )
2525
@@ -139,7 +139,7 @@ func editFile(ctx *context.Context, isNewFile bool) { · 113 unmodified lines
139139 ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
140140 }
141141 ctx.Data["new_branch_name"] = ""
142− ctx.Data["last_commit"] = ctx.Repo.Commit.ID
142+ ctx.Data["last_commit"] = ctx.Repo.CommitID
143143 ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
144144 ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
145145 ctx.Data["PreviewableFileModes"] = strings.Join(setting.Repository.Editor.PreviewableFileModes, ",")
@@ -159,39 +159,27 @@ func NewFile(ctx *context.Context) { · 13 unmodified lines
159159 }
160160
161161 func 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
166162 canCommit := renderCommitRights(ctx)
167−
168− oldBranchName := 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−
163+ treeNames, treePaths := getParentTreeFields(form.TreePath)
164+ branchName := ctx.Repo.BranchName
174165 if form.CommitChoice == frmCommitChoiceNewBranch {
175166 branchName = form.NewBranchName
176167 }
177168
178− form.TreePath = cleanUploadFileName(form.TreePath)
179− if len(form.TreePath) == 0 {
180− ctx.Error(500, "Upload file name is invalid")
181− return
182− }
183− treeNames, treePaths := getParentTreeFields(form.TreePath)
184−
169+ ctx.Data["PageIsEdit"] = true
170+ ctx.Data["IsNewFile"] = isNewFile
171+ ctx.Data["RequireHighlightJS"] = true
172+ ctx.Data["RequireSimpleMDE"] = true
185173 ctx.Data["TreePath"] = form.TreePath
186174 ctx.Data["TreeNames"] = treeNames
187175 ctx.Data["TreePaths"] = treePaths
188− ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + branchName
176+ ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/branch/" + ctx.Repo.BranchName
189177 ctx.Data["FileContent"] = form.Content
190178 ctx.Data["commit_summary"] = form.CommitSummary
191179 ctx.Data["commit_message"] = form.CommitMessage
192180 ctx.Data["commit_choice"] = form.CommitChoice
193− ctx.Data["new_branch_name"] = branchName
194− ctx.Data["last_commit"] = form.LastCommit
181+ ctx.Data["new_branch_name"] = form.NewBranchName
182+ ctx.Data["last_commit"] = ctx.Repo.CommitID
195183 ctx.Data["MarkdownFileExts"] = strings.Join(setting.Markdown.FileExtensions, ",")
196184 ctx.Data["LineWrapExtensions"] = strings.Join(setting.Repository.Editor.LineWrapExtensions, ",")
197185 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
201189 return
202190 }
203191
204− if len(form.TreePath) == 0 {
205− ctx.Data["Err_TreePath"] = true
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 {
192+ // Cannot commit to a an existing branch if user doesn't have rights
193+ if branchName == ctx.Repo.BranchName && !canCommit {
217194 ctx.Data["Err_NewBranchName"] = true
218195 ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
219196 ctx.RenderWithErr(ctx.Tr("repo.editor.cannot_commit_to_protected_branch", branchName), tplEditFile, &form)
220197 return
221198 }
222199
223− var newTreePath string
224− for index, part := range treeNames {
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−
200+ // CommitSummary is optional in the web form, if empty, give it a default message based on add or update
201+ // `message` will be both the summary and message combined
299202 message := strings.TrimSpace(form.CommitSummary)
300203 if len(message) == 0 {
301204 if isNewFile {
@@ -304,28 +207,74 @@ func editFilePost(ctx *context.Context, form auth.EditRepoFileForm, isNewFile bo · 2 unmodified lines
304207 message = ctx.Tr("repo.editor.update", form.TreePath)
305208 }
306209 }
307−
308210 form.CommitMessage = strings.TrimSpace(form.CommitMessage)
309211 if len(form.CommitMessage) > 0 {
310212 message += "\n\n" + form.CommitMessage
311213 }
312214
313− if err := uploader.UpdateRepoFile(ctx.Repo.Repository, ctx.User, &uploader.UpdateRepoFileOptions{
314− LastCommitID: lastCommit,
315− OldBranch: oldBranchName,
215+ if _, err := repofiles.CreateOrUpdateRepoFile(ctx.Repo.Repository, ctx.User, &repofiles.UpdateRepoFileOptions{
216+ LastCommitID: form.LastCommit,
217+ OldBranch: ctx.Repo.BranchName,
316218 NewBranch: branchName,
317− OldTreeName: oldTreePath,
318− NewTreeName: form.TreePath,
219+ FromTreePath: ctx.Repo.TreePath,
220+ TreePath: form.TreePath,
319221 Message: message,
320222 Content: strings.Replace(form.Content, "\r", "", -1),
321223 IsNewFile: isNewFile,
322224 }); err != nil {
323− ctx.Data["Err_TreePath"] = true
324− ctx.RenderWithErr(ctx.Tr("repo.editor.fail_to_update_file", form.TreePath, err), tplEditFile, &form)
325− return
225+ // This is where we handle all the errors thrown by repofiles.CreateOrUpdateRepoFile
226+ if git.IsErrNotExist(err) {
227+ 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))
326277 }
327−
328− ctx.Redirect(ctx.Repo.RepoLink + "/src/branch/" + util.PathEscapeSegments(branchName) + "/" + util.PathEscapeSegments(form.TreePath))
329278 }
330279
331280 // EditFilePost response for editing file
@@ -355,7 +304,7 @@ func DiffPreviewPost(ctx *context.Context, form auth.EditPreviewDiffForm) { · 23 unmodified lines
355304 return
356305 }
357306
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)
359308 if err != nil {
360309 ctx.Error(500, "GetDiffPreview: "+err.Error())
361310 return
@@ -386,6 +335,7 @@ func DeleteFile(ctx *context.Context) { · 24 unmodified lines
386335
387336 ctx.Data["commit_summary"] = ""
388337 ctx.Data["commit_message"] = ""
338+ ctx.Data["last_commit"] = ctx.Repo.CommitID
389339 if canCommit {
390340 ctx.Data["commit_choice"] = frmCommitChoiceDirect
391341 } else {
@@ -398,41 +348,27 @@ func DeleteFile(ctx *context.Context) { · 6 unmodified lines
398348
399349 // DeleteFilePost response for deleting file
400350 func 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
411351 canCommit := renderCommitRights(ctx)
412−
413− oldBranchName := ctx.Repo.BranchName
414− branchName := oldBranchName
415−
352+ branchName := ctx.Repo.BranchName
416353 if form.CommitChoice == frmCommitChoiceNewBranch {
417354 branchName = form.NewBranchName
418355 }
356+
357+ ctx.Data["PageIsDelete"] = true
358+ ctx.Data["BranchLink"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL()
359+ ctx.Data["TreePath"] = ctx.Repo.TreePath
419360 ctx.Data["commit_summary"] = form.CommitSummary
420361 ctx.Data["commit_message"] = form.CommitMessage
421362 ctx.Data["commit_choice"] = form.CommitChoice
422− ctx.Data["new_branch_name"] = branchName
363+ ctx.Data["new_branch_name"] = form.NewBranchName
364+ ctx.Data["last_commit"] = ctx.Repo.CommitID
423365
424366 if ctx.HasError() {
425367 ctx.HTML(200, tplDeleteFile)
426368 return
427369 }
428370
429− if oldBranchName != branchName {
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 {
371+ if branchName != ctx.Repo.BranchName && !canCommit {
436372 ctx.Data["Err_NewBranchName"] = true
437373 ctx.Data["commit_choice"] = frmCommitChoiceNewBranch
438374 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
443379 if len(message) == 0 {
444380 message = ctx.Tr("repo.editor.delete", ctx.Repo.TreePath)
445381 }
446−
447382 form.CommitMessage = strings.TrimSpace(form.CommitMessage)
448383 if len(form.CommitMessage) > 0 {
449384 message += "\n\n" + form.CommitMessage
450385 }
451386
452− if err := uploader.DeleteRepoFile(ctx.Repo.Repository, ctx.User, &uploader.DeleteRepoFileOptions{
453− LastCommitID: ctx.Repo.CommitID,
454− OldBranch: oldBranchName,
387+ if _, err := repofiles.DeleteRepoFile(ctx.Repo.Repository, ctx.User, &repofiles.DeleteRepoFileOptions{
388+ LastCommitID: form.LastCommit,
389+ OldBranch: ctx.Repo.BranchName,
455390 NewBranch: branchName,
456391 TreePath: ctx.Repo.TreePath,
457392 Message: message,
458393 }); err != nil {
459− ctx.ServerError("DeleteRepoFile", err)
460− return
394+ // This is where we handle all the errors thrown by repofiles.DeleteRepoFile
395+ 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))
461442 }
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))
465443 }
466444
467445 func renderUploadSettings(ctx *context.Context) {
@@ -584,7 +562,7 @@ func UploadFilePost(ctx *context.Context, form auth.UploadRepoFileForm) { · 116 unmodified lines
584562 message += "\n\n" + form.CommitMessage
585563 }
586564
587− if err := uploader.UploadRepoFiles(ctx.Repo.Repository, ctx.User, &uploader.UploadRepoFileOptions{
565+ if err := repofiles.UploadRepoFiles(ctx.Repo.Repository, ctx.User, &repofiles.UploadRepoFileOptions{
588566 LastCommitID: ctx.Repo.CommitID,
589567 OldBranch: oldBranchName,
590568 NewBranch: branchName,
routers/repo/editor_test.go+1−0
@@ -8,6 +8,7 @@ import ( · 7 unmodified lines
88 "testing"
99
1010 "code.gitea.io/gitea/models"
11+
1112 "github.com/stretchr/testify/assert"
1213 )
1314
templates/repo/editor/commit_form.tmpl+1−1
@@ -3,7 +3,7 @@ · 2 unmodified lines
33 <div class="commit-form">
44 <h3>{{.i18n.Tr "repo.editor.commit_changes"}}</h3>
55 <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>
77 </div>
88 <div class="field">
99 <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
55 {{template "base/alert" .}}
66 <form class="ui form" method="post">
77 {{.CsrfTokenHtml}}
8+ <input type="hidden" name="last_commit" value="{{.last_commit}}">
89 {{template "repo/editor/commit_form" .}}
910 </form>
1011 </div>
1112 </div>
12−{{template "base/footer" .}}
13+{{template "base/footer" .}}
templates/swagger/v1_json.tmpl+535−1
@@ -1511,6 +1511,199 @@ · 1510 unmodified lines
15111511 }
15121512 }
15131513 },
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+ },
15141707 "/repos/{owner}/{repo}/editorconfig/{filepath}": {
15151708 "get": {
15161709 "produces": [
@@ -1622,6 +1815,46 @@ · 105 unmodified lines
16221815 }
16231816 }
16241817 },
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+ },
16251858 "/repos/{owner}/{repo}/git/commits/{sha}": {
16261859 "get": {
16271860 "produces": [
@@ -6637,6 +6870,35 @@ · 5009 unmodified lines
66376870 },
66386871 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
66396872 },
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+ },
66406902 "CreateForkOption": {
66416903 "description": "CreateForkOption options for creating a fork",
66426904 "type": "object",
@@ -7129,6 +7391,35 @@ · 486 unmodified lines
71297391 },
71307392 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
71317393 },
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+ },
71327423 "DeployKey": {
71337424 "description": "DeployKey a deploy key",
71347425 "type": "object",
@@ -7567,6 +7858,144 @@ · 432 unmodified lines
75677858 },
75687859 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
75697860 },
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+ },
75707999 "GPGKey": {
75718000 "description": "GPGKey a user GPG key to sign commit and tag in repository",
75728001 "type": "object",
@@ -7646,6 +8075,34 @@ · 73 unmodified lines
76468075 },
76478076 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
76488077 },
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+ },
76498106 "GitEntry": {
76508107 "description": "GitEntry represents a git tree",
76518108 "type": "object",
@@ -7796,6 +8253,22 @@ · 144 unmodified lines
77968253 },
77978254 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
77988255 },
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+ },
77998272 "Issue": {
78008273 "description": "Issue represents an issue in a repository",
78018274 "type": "object",
@@ -8855,6 +9328,43 @@ · 1053 unmodified lines
88559328 },
88569329 "x-go-package": "code.gitea.io/gitea/vendor/code.gitea.io/sdk/gitea"
88579330 },
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+ },
88589368 "User": {
88599369 "description": "User represents a user",
88609370 "type": "object",
@@ -9040,6 +9550,24 @@ · 179 unmodified lines
90409550 }
90419551 }
90429552 },
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+ },
90439571 "GPGKey": {
90449572 "description": "GPGKey",
90459573 "schema": {
@@ -9055,6 +9583,12 @@ · 9 unmodified lines
90559583 }
90569584 }
90579585 },
9586+ "GitBlobResponse": {
9587+ "description": "GitBlobResponse",
9588+ "schema": {
9589+ "$ref": "#/definitions/GitBlobResponse"
9590+ }
9591+ },
90589592 "GitHook": {
90599593 "description": "GitHook",
90609594 "schema": {
@@ -9362,7 +9896,7 @@ · 301 unmodified lines
93629896 "parameterBodies": {
93639897 "description": "parameterBodies",
93649898 "schema": {
9365− "$ref": "#/definitions/EditAttachmentOptions"
9899+ "$ref": "#/definitions/DeleteFileOptions"
93669900 }
93679901 },
93689902 "redirect": {