1// Copyright 2014 The Gogs Authors. All rights reserved.
2// Copyright 2018 The Gitea Authors. All rights reserved.
3// Use of this source code is governed by a MIT-style
4// license that can be found in the LICENSE file.
5
6package repo
7
8import (
9	"encoding/base64"
10	"fmt"
11	"net/http"
12	"time"
13
14	"code.gitea.io/gitea/models"
15	repo_model "code.gitea.io/gitea/models/repo"
16	"code.gitea.io/gitea/models/unit"
17	"code.gitea.io/gitea/modules/context"
18	"code.gitea.io/gitea/modules/git"
19	api "code.gitea.io/gitea/modules/structs"
20	"code.gitea.io/gitea/modules/web"
21	"code.gitea.io/gitea/routers/common"
22	"code.gitea.io/gitea/routers/web/repo"
23	files_service "code.gitea.io/gitea/services/repository/files"
24)
25
26// GetRawFile get a file by path on a repository
27func GetRawFile(ctx *context.APIContext) {
28	// swagger:operation GET /repos/{owner}/{repo}/raw/{filepath} repository repoGetRawFile
29	// ---
30	// summary: Get a file from a repository
31	// produces:
32	// - application/json
33	// parameters:
34	// - name: owner
35	//   in: path
36	//   description: owner of the repo
37	//   type: string
38	//   required: true
39	// - name: repo
40	//   in: path
41	//   description: name of the repo
42	//   type: string
43	//   required: true
44	// - name: filepath
45	//   in: path
46	//   description: filepath of the file to get
47	//   type: string
48	//   required: true
49	// - name: ref
50	//   in: query
51	//   description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
52	//   type: string
53	//   required: false
54	// responses:
55	//   200:
56	//     description: success
57	//   "404":
58	//     "$ref": "#/responses/notFound"
59
60	if ctx.Repo.Repository.IsEmpty {
61		ctx.NotFound()
62		return
63	}
64
65	commit := ctx.Repo.Commit
66
67	if ref := ctx.FormTrim("ref"); len(ref) > 0 {
68		var err error
69		commit, err = ctx.Repo.GitRepo.GetCommit(ref)
70		if err != nil {
71			if git.IsErrNotExist(err) {
72				ctx.NotFound()
73			} else {
74				ctx.Error(http.StatusInternalServerError, "GetBlobByPath", err)
75			}
76			return
77		}
78	}
79
80	blob, err := commit.GetBlobByPath(ctx.Repo.TreePath)
81	if err != nil {
82		if git.IsErrNotExist(err) {
83			ctx.NotFound()
84		} else {
85			ctx.Error(http.StatusInternalServerError, "GetBlobByPath", err)
86		}
87		return
88	}
89	if err = common.ServeBlob(ctx.Context, blob); err != nil {
90		ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
91	}
92}
93
94// GetArchive get archive of a repository
95func GetArchive(ctx *context.APIContext) {
96	// swagger:operation GET /repos/{owner}/{repo}/archive/{archive} repository repoGetArchive
97	// ---
98	// summary: Get an archive of a repository
99	// produces:
100	// - application/json
101	// parameters:
102	// - name: owner
103	//   in: path
104	//   description: owner of the repo
105	//   type: string
106	//   required: true
107	// - name: repo
108	//   in: path
109	//   description: name of the repo
110	//   type: string
111	//   required: true
112	// - name: archive
113	//   in: path
114	//   description: the git reference for download with attached archive format (e.g. master.zip)
115	//   type: string
116	//   required: true
117	// responses:
118	//   200:
119	//     description: success
120	//   "404":
121	//     "$ref": "#/responses/notFound"
122
123	repoPath := repo_model.RepoPath(ctx.Params(":username"), ctx.Params(":reponame"))
124	if ctx.Repo.GitRepo == nil {
125		gitRepo, err := git.OpenRepository(repoPath)
126		if err != nil {
127			ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
128			return
129		}
130		ctx.Repo.GitRepo = gitRepo
131		defer gitRepo.Close()
132	}
133
134	repo.Download(ctx.Context)
135}
136
137// GetEditorconfig get editor config of a repository
138func GetEditorconfig(ctx *context.APIContext) {
139	// swagger:operation GET /repos/{owner}/{repo}/editorconfig/{filepath} repository repoGetEditorConfig
140	// ---
141	// summary: Get the EditorConfig definitions of a file in a repository
142	// produces:
143	// - application/json
144	// parameters:
145	// - name: owner
146	//   in: path
147	//   description: owner of the repo
148	//   type: string
149	//   required: true
150	// - name: repo
151	//   in: path
152	//   description: name of the repo
153	//   type: string
154	//   required: true
155	// - name: filepath
156	//   in: path
157	//   description: filepath of file to get
158	//   type: string
159	//   required: true
160	// responses:
161	//   200:
162	//     description: success
163	//   "404":
164	//     "$ref": "#/responses/notFound"
165
166	ec, err := ctx.Repo.GetEditorconfig()
167	if err != nil {
168		if git.IsErrNotExist(err) {
169			ctx.NotFound(err)
170		} else {
171			ctx.Error(http.StatusInternalServerError, "GetEditorconfig", err)
172		}
173		return
174	}
175
176	fileName := ctx.Params("filename")
177	def, err := ec.GetDefinitionForFilename(fileName)
178	if def == nil {
179		ctx.NotFound(err)
180		return
181	}
182	ctx.JSON(http.StatusOK, def)
183}
184
185// canWriteFiles returns true if repository is editable and user has proper access level.
186func canWriteFiles(r *context.Repository) bool {
187	return r.Permission.CanWrite(unit.TypeCode) && !r.Repository.IsMirror && !r.Repository.IsArchived
188}
189
190// canReadFiles returns true if repository is readable and user has proper access level.
191func canReadFiles(r *context.Repository) bool {
192	return r.Permission.CanRead(unit.TypeCode)
193}
194
195// CreateFile handles API call for creating a file
196func CreateFile(ctx *context.APIContext) {
197	// swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
198	// ---
199	// summary: Create a file in a repository
200	// consumes:
201	// - application/json
202	// produces:
203	// - application/json
204	// parameters:
205	// - name: owner
206	//   in: path
207	//   description: owner of the repo
208	//   type: string
209	//   required: true
210	// - name: repo
211	//   in: path
212	//   description: name of the repo
213	//   type: string
214	//   required: true
215	// - name: filepath
216	//   in: path
217	//   description: path of the file to create
218	//   type: string
219	//   required: true
220	// - name: body
221	//   in: body
222	//   required: true
223	//   schema:
224	//     "$ref": "#/definitions/CreateFileOptions"
225	// responses:
226	//   "201":
227	//     "$ref": "#/responses/FileResponse"
228	//   "403":
229	//     "$ref": "#/responses/error"
230	//   "404":
231	//     "$ref": "#/responses/notFound"
232	//   "422":
233	//     "$ref": "#/responses/error"
234
235	apiOpts := web.GetForm(ctx).(*api.CreateFileOptions)
236	if ctx.Repo.Repository.IsEmpty {
237		ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
238	}
239
240	if apiOpts.BranchName == "" {
241		apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
242	}
243
244	opts := &files_service.UpdateRepoFileOptions{
245		Content:   apiOpts.Content,
246		IsNewFile: true,
247		Message:   apiOpts.Message,
248		TreePath:  ctx.Params("*"),
249		OldBranch: apiOpts.BranchName,
250		NewBranch: apiOpts.NewBranchName,
251		Committer: &files_service.IdentityOptions{
252			Name:  apiOpts.Committer.Name,
253			Email: apiOpts.Committer.Email,
254		},
255		Author: &files_service.IdentityOptions{
256			Name:  apiOpts.Author.Name,
257			Email: apiOpts.Author.Email,
258		},
259		Dates: &files_service.CommitDateOptions{
260			Author:    apiOpts.Dates.Author,
261			Committer: apiOpts.Dates.Committer,
262		},
263		Signoff: apiOpts.Signoff,
264	}
265	if opts.Dates.Author.IsZero() {
266		opts.Dates.Author = time.Now()
267	}
268	if opts.Dates.Committer.IsZero() {
269		opts.Dates.Committer = time.Now()
270	}
271
272	if opts.Message == "" {
273		opts.Message = ctx.Tr("repo.editor.add", opts.TreePath)
274	}
275
276	if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
277		handleCreateOrUpdateFileError(ctx, err)
278	} else {
279		ctx.JSON(http.StatusCreated, fileResponse)
280	}
281}
282
283// UpdateFile handles API call for updating a file
284func UpdateFile(ctx *context.APIContext) {
285	// swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
286	// ---
287	// summary: Update a file in a repository
288	// consumes:
289	// - application/json
290	// produces:
291	// - application/json
292	// parameters:
293	// - name: owner
294	//   in: path
295	//   description: owner of the repo
296	//   type: string
297	//   required: true
298	// - name: repo
299	//   in: path
300	//   description: name of the repo
301	//   type: string
302	//   required: true
303	// - name: filepath
304	//   in: path
305	//   description: path of the file to update
306	//   type: string
307	//   required: true
308	// - name: body
309	//   in: body
310	//   required: true
311	//   schema:
312	//     "$ref": "#/definitions/UpdateFileOptions"
313	// responses:
314	//   "200":
315	//     "$ref": "#/responses/FileResponse"
316	//   "403":
317	//     "$ref": "#/responses/error"
318	//   "404":
319	//     "$ref": "#/responses/notFound"
320	//   "422":
321	//     "$ref": "#/responses/error"
322	apiOpts := web.GetForm(ctx).(*api.UpdateFileOptions)
323	if ctx.Repo.Repository.IsEmpty {
324		ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
325	}
326
327	if apiOpts.BranchName == "" {
328		apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
329	}
330
331	opts := &files_service.UpdateRepoFileOptions{
332		Content:      apiOpts.Content,
333		SHA:          apiOpts.SHA,
334		IsNewFile:    false,
335		Message:      apiOpts.Message,
336		FromTreePath: apiOpts.FromPath,
337		TreePath:     ctx.Params("*"),
338		OldBranch:    apiOpts.BranchName,
339		NewBranch:    apiOpts.NewBranchName,
340		Committer: &files_service.IdentityOptions{
341			Name:  apiOpts.Committer.Name,
342			Email: apiOpts.Committer.Email,
343		},
344		Author: &files_service.IdentityOptions{
345			Name:  apiOpts.Author.Name,
346			Email: apiOpts.Author.Email,
347		},
348		Dates: &files_service.CommitDateOptions{
349			Author:    apiOpts.Dates.Author,
350			Committer: apiOpts.Dates.Committer,
351		},
352		Signoff: apiOpts.Signoff,
353	}
354	if opts.Dates.Author.IsZero() {
355		opts.Dates.Author = time.Now()
356	}
357	if opts.Dates.Committer.IsZero() {
358		opts.Dates.Committer = time.Now()
359	}
360
361	if opts.Message == "" {
362		opts.Message = ctx.Tr("repo.editor.update", opts.TreePath)
363	}
364
365	if fileResponse, err := createOrUpdateFile(ctx, opts); err != nil {
366		handleCreateOrUpdateFileError(ctx, err)
367	} else {
368		ctx.JSON(http.StatusOK, fileResponse)
369	}
370}
371
372func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) {
373	if models.IsErrUserCannotCommit(err) || models.IsErrFilePathProtected(err) {
374		ctx.Error(http.StatusForbidden, "Access", err)
375		return
376	}
377	if models.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) ||
378		models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) {
379		ctx.Error(http.StatusUnprocessableEntity, "Invalid", err)
380		return
381	}
382	if models.IsErrBranchDoesNotExist(err) || git.IsErrBranchNotExist(err) {
383		ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err)
384		return
385	}
386
387	ctx.Error(http.StatusInternalServerError, "UpdateFile", err)
388}
389
390// Called from both CreateFile or UpdateFile to handle both
391func createOrUpdateFile(ctx *context.APIContext, opts *files_service.UpdateRepoFileOptions) (*api.FileResponse, error) {
392	if !canWriteFiles(ctx.Repo) {
393		return nil, models.ErrUserDoesNotHaveAccessToRepo{
394			UserID:   ctx.User.ID,
395			RepoName: ctx.Repo.Repository.LowerName,
396		}
397	}
398
399	content, err := base64.StdEncoding.DecodeString(opts.Content)
400	if err != nil {
401		return nil, err
402	}
403	opts.Content = string(content)
404
405	return files_service.CreateOrUpdateRepoFile(ctx.Repo.Repository, ctx.User, opts)
406}
407
408// DeleteFile Delete a file in a repository
409func DeleteFile(ctx *context.APIContext) {
410	// swagger:operation DELETE /repos/{owner}/{repo}/contents/{filepath} repository repoDeleteFile
411	// ---
412	// summary: Delete a file in a repository
413	// consumes:
414	// - application/json
415	// produces:
416	// - application/json
417	// parameters:
418	// - name: owner
419	//   in: path
420	//   description: owner of the repo
421	//   type: string
422	//   required: true
423	// - name: repo
424	//   in: path
425	//   description: name of the repo
426	//   type: string
427	//   required: true
428	// - name: filepath
429	//   in: path
430	//   description: path of the file to delete
431	//   type: string
432	//   required: true
433	// - name: body
434	//   in: body
435	//   required: true
436	//   schema:
437	//     "$ref": "#/definitions/DeleteFileOptions"
438	// responses:
439	//   "200":
440	//     "$ref": "#/responses/FileDeleteResponse"
441	//   "400":
442	//     "$ref": "#/responses/error"
443	//   "403":
444	//     "$ref": "#/responses/error"
445	//   "404":
446	//     "$ref": "#/responses/error"
447
448	apiOpts := web.GetForm(ctx).(*api.DeleteFileOptions)
449	if !canWriteFiles(ctx.Repo) {
450		ctx.Error(http.StatusForbidden, "DeleteFile", models.ErrUserDoesNotHaveAccessToRepo{
451			UserID:   ctx.User.ID,
452			RepoName: ctx.Repo.Repository.LowerName,
453		})
454		return
455	}
456
457	if apiOpts.BranchName == "" {
458		apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
459	}
460
461	opts := &files_service.DeleteRepoFileOptions{
462		Message:   apiOpts.Message,
463		OldBranch: apiOpts.BranchName,
464		NewBranch: apiOpts.NewBranchName,
465		SHA:       apiOpts.SHA,
466		TreePath:  ctx.Params("*"),
467		Committer: &files_service.IdentityOptions{
468			Name:  apiOpts.Committer.Name,
469			Email: apiOpts.Committer.Email,
470		},
471		Author: &files_service.IdentityOptions{
472			Name:  apiOpts.Author.Name,
473			Email: apiOpts.Author.Email,
474		},
475		Dates: &files_service.CommitDateOptions{
476			Author:    apiOpts.Dates.Author,
477			Committer: apiOpts.Dates.Committer,
478		},
479		Signoff: apiOpts.Signoff,
480	}
481	if opts.Dates.Author.IsZero() {
482		opts.Dates.Author = time.Now()
483	}
484	if opts.Dates.Committer.IsZero() {
485		opts.Dates.Committer = time.Now()
486	}
487
488	if opts.Message == "" {
489		opts.Message = ctx.Tr("repo.editor.delete", opts.TreePath)
490	}
491
492	if fileResponse, err := files_service.DeleteRepoFile(ctx.Repo.Repository, ctx.User, opts); err != nil {
493		if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
494			ctx.Error(http.StatusNotFound, "DeleteFile", err)
495			return
496		} else if models.IsErrBranchAlreadyExists(err) ||
497			models.IsErrFilenameInvalid(err) ||
498			models.IsErrSHADoesNotMatch(err) ||
499			models.IsErrCommitIDDoesNotMatch(err) ||
500			models.IsErrSHAOrCommitIDNotProvided(err) {
501			ctx.Error(http.StatusBadRequest, "DeleteFile", err)
502			return
503		} else if models.IsErrUserCannotCommit(err) {
504			ctx.Error(http.StatusForbidden, "DeleteFile", err)
505			return
506		}
507		ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
508	} else {
509		ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
510	}
511}
512
513// GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
514func GetContents(ctx *context.APIContext) {
515	// swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents
516	// ---
517	// summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
518	// produces:
519	// - application/json
520	// parameters:
521	// - name: owner
522	//   in: path
523	//   description: owner of the repo
524	//   type: string
525	//   required: true
526	// - name: repo
527	//   in: path
528	//   description: name of the repo
529	//   type: string
530	//   required: true
531	// - name: filepath
532	//   in: path
533	//   description: path of the dir, file, symlink or submodule in the repo
534	//   type: string
535	//   required: true
536	// - name: ref
537	//   in: query
538	//   description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
539	//   type: string
540	//   required: false
541	// responses:
542	//   "200":
543	//     "$ref": "#/responses/ContentsResponse"
544	//   "404":
545	//     "$ref": "#/responses/notFound"
546
547	if !canReadFiles(ctx.Repo) {
548		ctx.Error(http.StatusInternalServerError, "GetContentsOrList", models.ErrUserDoesNotHaveAccessToRepo{
549			UserID:   ctx.User.ID,
550			RepoName: ctx.Repo.Repository.LowerName,
551		})
552		return
553	}
554
555	treePath := ctx.Params("*")
556	ref := ctx.FormTrim("ref")
557
558	if fileList, err := files_service.GetContentsOrList(ctx.Repo.Repository, treePath, ref); err != nil {
559		if git.IsErrNotExist(err) {
560			ctx.NotFound("GetContentsOrList", err)
561			return
562		}
563		ctx.Error(http.StatusInternalServerError, "GetContentsOrList", err)
564	} else {
565		ctx.JSON(http.StatusOK, fileList)
566	}
567}
568
569// GetContentsList Get the metadata of all the entries of the root dir
570func GetContentsList(ctx *context.APIContext) {
571	// swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList
572	// ---
573	// summary: Gets the metadata of all the entries of the root dir
574	// produces:
575	// - application/json
576	// parameters:
577	// - name: owner
578	//   in: path
579	//   description: owner of the repo
580	//   type: string
581	//   required: true
582	// - name: repo
583	//   in: path
584	//   description: name of the repo
585	//   type: string
586	//   required: true
587	// - name: ref
588	//   in: query
589	//   description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
590	//   type: string
591	//   required: false
592	// responses:
593	//   "200":
594	//     "$ref": "#/responses/ContentsListResponse"
595	//   "404":
596	//     "$ref": "#/responses/notFound"
597
598	// same as GetContents(), this function is here because swagger fails if path is empty in GetContents() interface
599	GetContents(ctx)
600}
601