2014-11-17 02:27:04 +01:00
|
|
|
|
// Copyright 2014 The Gogs Authors. All rights reserved.
|
2018-11-28 12:26:14 +01:00
|
|
|
|
// Copyright 2018 The Gitea Authors. All rights reserved.
|
2022-11-27 19:20:29 +01:00
|
|
|
|
// SPDX-License-Identifier: MIT
|
2014-11-17 02:27:04 +01:00
|
|
|
|
|
2015-12-04 23:16:42 +01:00
|
|
|
|
package repo
|
2014-11-17 03:32:26 +01:00
|
|
|
|
|
|
|
|
|
import (
|
2022-06-04 15:17:53 +02:00
|
|
|
|
"bytes"
|
2019-04-17 18:06:35 +02:00
|
|
|
|
"encoding/base64"
|
2022-08-29 11:45:20 +02:00
|
|
|
|
"errors"
|
2020-05-31 22:59:34 +02:00
|
|
|
|
"fmt"
|
2022-06-04 15:17:53 +02:00
|
|
|
|
"io"
|
2019-04-17 18:06:35 +02:00
|
|
|
|
"net/http"
|
2022-05-09 17:54:51 +02:00
|
|
|
|
"path"
|
2023-05-29 11:41:35 +02:00
|
|
|
|
"strings"
|
2019-12-24 03:33:52 +01:00
|
|
|
|
"time"
|
2019-04-17 18:06:35 +02:00
|
|
|
|
|
2016-11-10 17:24:48 +01:00
|
|
|
|
"code.gitea.io/gitea/models"
|
2022-06-12 17:51:54 +02:00
|
|
|
|
git_model "code.gitea.io/gitea/models/git"
|
2021-12-10 02:27:50 +01:00
|
|
|
|
repo_model "code.gitea.io/gitea/models/repo"
|
2021-11-09 20:57:58 +01:00
|
|
|
|
"code.gitea.io/gitea/models/unit"
|
2019-03-27 10:33:00 +01:00
|
|
|
|
"code.gitea.io/gitea/modules/git"
|
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
2024-01-27 21:09:51 +01:00
|
|
|
|
"code.gitea.io/gitea/modules/gitrepo"
|
2022-06-04 15:17:53 +02:00
|
|
|
|
"code.gitea.io/gitea/modules/httpcache"
|
|
|
|
|
"code.gitea.io/gitea/modules/lfs"
|
|
|
|
|
"code.gitea.io/gitea/modules/log"
|
2022-05-09 17:54:51 +02:00
|
|
|
|
"code.gitea.io/gitea/modules/setting"
|
2022-06-04 15:17:53 +02:00
|
|
|
|
"code.gitea.io/gitea/modules/storage"
|
2019-05-11 12:21:34 +02:00
|
|
|
|
api "code.gitea.io/gitea/modules/structs"
|
2021-01-26 16:36:53 +01:00
|
|
|
|
"code.gitea.io/gitea/modules/web"
|
2021-06-09 01:33:54 +02:00
|
|
|
|
"code.gitea.io/gitea/routers/common"
|
2024-02-27 08:12:22 +01:00
|
|
|
|
"code.gitea.io/gitea/services/context"
|
2022-08-29 11:45:20 +02:00
|
|
|
|
archiver_service "code.gitea.io/gitea/services/repository/archiver"
|
2021-11-24 08:56:24 +01:00
|
|
|
|
files_service "code.gitea.io/gitea/services/repository/files"
|
2014-11-17 03:32:26 +01:00
|
|
|
|
)
|
|
|
|
|
|
2023-01-14 08:16:30 +01:00
|
|
|
|
const (
|
|
|
|
|
giteaObjectTypeHeader = "X-Gitea-Object-Type"
|
|
|
|
|
forgejoObjectTypeHeader = "X-Forgejo-Object-Type"
|
|
|
|
|
)
|
2022-07-21 21:18:41 +02:00
|
|
|
|
|
2016-11-24 08:04:31 +01:00
|
|
|
|
// GetRawFile get a file by path on a repository
|
2016-03-13 23:49:16 +01:00
|
|
|
|
func GetRawFile(ctx *context.APIContext) {
|
2017-11-13 08:02:25 +01:00
|
|
|
|
// swagger:operation GET /repos/{owner}/{repo}/raw/{filepath} repository repoGetRawFile
|
|
|
|
|
// ---
|
|
|
|
|
// summary: Get a file from a repository
|
|
|
|
|
// produces:
|
|
|
|
|
// - application/json
|
|
|
|
|
// parameters:
|
|
|
|
|
// - name: owner
|
|
|
|
|
// in: path
|
|
|
|
|
// description: owner of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: repo
|
|
|
|
|
// in: path
|
|
|
|
|
// description: name of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: filepath
|
|
|
|
|
// in: path
|
|
|
|
|
// description: filepath of the file to get
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
2021-02-09 01:15:47 +01:00
|
|
|
|
// - name: ref
|
|
|
|
|
// in: query
|
|
|
|
|
// description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
|
|
|
|
|
// type: string
|
|
|
|
|
// required: false
|
2017-11-13 08:02:25 +01:00
|
|
|
|
// responses:
|
2018-06-01 07:51:49 +02:00
|
|
|
|
// 200:
|
2022-04-28 16:57:56 +02:00
|
|
|
|
// description: Returns raw file content.
|
2019-12-20 18:07:12 +01:00
|
|
|
|
// "404":
|
|
|
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
|
|
2019-01-18 01:01:04 +01:00
|
|
|
|
if ctx.Repo.Repository.IsEmpty {
|
2019-03-19 03:29:43 +01:00
|
|
|
|
ctx.NotFound()
|
2017-06-11 04:57:28 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-21 21:18:41 +02:00
|
|
|
|
blob, entry, lastModified := getBlobForEntry(ctx)
|
2022-05-09 17:54:51 +02:00
|
|
|
|
if ctx.Written() {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-21 21:18:41 +02:00
|
|
|
|
ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
|
2023-01-14 08:16:30 +01:00
|
|
|
|
ctx.RespHeader().Set(forgejoObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
|
2022-07-21 21:18:41 +02:00
|
|
|
|
|
2023-05-21 03:50:53 +02:00
|
|
|
|
if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
|
2022-05-09 17:54:51 +02:00
|
|
|
|
ctx.Error(http.StatusInternalServerError, "ServeBlob", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-04 15:17:53 +02:00
|
|
|
|
// GetRawFileOrLFS get a file by repo's path, redirecting to LFS if necessary.
|
|
|
|
|
func GetRawFileOrLFS(ctx *context.APIContext) {
|
|
|
|
|
// swagger:operation GET /repos/{owner}/{repo}/media/{filepath} repository repoGetRawFileOrLFS
|
|
|
|
|
// ---
|
|
|
|
|
// summary: Get a file or it's LFS object from a repository
|
|
|
|
|
// parameters:
|
|
|
|
|
// - name: owner
|
|
|
|
|
// in: path
|
|
|
|
|
// description: owner of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: repo
|
|
|
|
|
// in: path
|
|
|
|
|
// description: name of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: filepath
|
|
|
|
|
// in: path
|
|
|
|
|
// description: filepath of the file to get
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: ref
|
|
|
|
|
// in: query
|
|
|
|
|
// description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
|
|
|
|
|
// type: string
|
|
|
|
|
// required: false
|
|
|
|
|
// responses:
|
|
|
|
|
// 200:
|
|
|
|
|
// description: Returns raw file content.
|
|
|
|
|
// "404":
|
|
|
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
|
|
|
|
|
|
if ctx.Repo.Repository.IsEmpty {
|
|
|
|
|
ctx.NotFound()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-21 21:18:41 +02:00
|
|
|
|
blob, entry, lastModified := getBlobForEntry(ctx)
|
2022-06-04 15:17:53 +02:00
|
|
|
|
if ctx.Written() {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-21 21:18:41 +02:00
|
|
|
|
ctx.RespHeader().Set(giteaObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
|
2023-01-14 08:16:30 +01:00
|
|
|
|
ctx.RespHeader().Set(forgejoObjectTypeHeader, string(files_service.GetObjectTypeFromTreeEntry(entry)))
|
2022-07-21 21:18:41 +02:00
|
|
|
|
|
2022-06-04 15:17:53 +02:00
|
|
|
|
// LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file
|
|
|
|
|
if blob.Size() > 1024 {
|
|
|
|
|
// First handle caching for the blob
|
|
|
|
|
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OK not cached - serve!
|
2023-05-21 03:50:53 +02:00
|
|
|
|
if err := common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified); err != nil {
|
2022-06-04 15:17:53 +02:00
|
|
|
|
ctx.ServerError("ServeBlob", err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-03-26 08:48:53 +01:00
|
|
|
|
// OK, now the blob is known to have at most 1024 bytes we can simply read this in one go (This saves reading it twice)
|
2022-06-04 15:17:53 +02:00
|
|
|
|
dataRc, err := blob.DataAsync()
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.ServerError("DataAsync", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-09 09:34:36 +02:00
|
|
|
|
// FIXME: code from #19689, what if the file is large ... OOM ...
|
2022-06-04 15:17:53 +02:00
|
|
|
|
buf, err := io.ReadAll(dataRc)
|
|
|
|
|
if err != nil {
|
|
|
|
|
_ = dataRc.Close()
|
|
|
|
|
ctx.ServerError("DataAsync", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := dataRc.Close(); err != nil {
|
2023-05-21 03:50:53 +02:00
|
|
|
|
log.Error("Error whilst closing blob %s reader in %-v. Error: %v", blob.ID, ctx.Repo.Repository, err)
|
2022-06-04 15:17:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if the blob represents a pointer
|
|
|
|
|
pointer, _ := lfs.ReadPointer(bytes.NewReader(buf))
|
|
|
|
|
|
2023-05-09 09:34:36 +02:00
|
|
|
|
// if it's not a pointer, just serve the data directly
|
2022-06-04 15:17:53 +02:00
|
|
|
|
if !pointer.IsValid() {
|
|
|
|
|
// First handle caching for the blob
|
|
|
|
|
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OK not cached - serve!
|
2023-05-21 03:50:53 +02:00
|
|
|
|
common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
|
2022-06-04 15:17:53 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-09 09:34:36 +02:00
|
|
|
|
// Now check if there is a MetaObject for this pointer
|
2023-01-09 04:50:54 +01:00
|
|
|
|
meta, err := git_model.GetLFSMetaObjectByOid(ctx, ctx.Repo.Repository.ID, pointer.Oid)
|
2022-06-04 15:17:53 +02:00
|
|
|
|
|
2023-05-09 09:34:36 +02:00
|
|
|
|
// If there isn't one, just serve the data directly
|
2022-06-12 17:51:54 +02:00
|
|
|
|
if err == git_model.ErrLFSObjectNotExist {
|
2022-06-04 15:17:53 +02:00
|
|
|
|
// Handle caching for the blob SHA (not the LFS object OID)
|
|
|
|
|
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-21 03:50:53 +02:00
|
|
|
|
common.ServeContentByReader(ctx.Base, ctx.Repo.TreePath, blob.Size(), bytes.NewReader(buf))
|
2022-06-04 15:17:53 +02:00
|
|
|
|
return
|
|
|
|
|
} else if err != nil {
|
|
|
|
|
ctx.ServerError("GetLFSMetaObjectByOid", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Handle caching for the LFS object OID
|
|
|
|
|
if httpcache.HandleGenericETagCache(ctx.Req, ctx.Resp, `"`+pointer.Oid+`"`) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-14 05:42:38 +02:00
|
|
|
|
if setting.LFS.Storage.MinioConfig.ServeDirect {
|
2022-06-04 15:17:53 +02:00
|
|
|
|
// If we have a signed url (S3, object storage), redirect to this directly.
|
|
|
|
|
u, err := storage.LFS.URL(pointer.RelativePath(), blob.Name())
|
|
|
|
|
if u != nil && err == nil {
|
|
|
|
|
ctx.Redirect(u.String())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lfsDataRc, err := lfs.ReadMetaObject(meta.Pointer)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.ServerError("ReadMetaObject", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer lfsDataRc.Close()
|
|
|
|
|
|
2023-05-21 03:50:53 +02:00
|
|
|
|
common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, lfsDataRc)
|
2022-06-04 15:17:53 +02:00
|
|
|
|
}
|
|
|
|
|
|
2023-07-07 07:31:56 +02:00
|
|
|
|
func getBlobForEntry(ctx *context.APIContext) (blob *git.Blob, entry *git.TreeEntry, lastModified *time.Time) {
|
2022-05-09 17:54:51 +02:00
|
|
|
|
entry, err := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath)
|
2014-11-17 03:32:26 +01:00
|
|
|
|
if err != nil {
|
2015-12-10 02:46:05 +01:00
|
|
|
|
if git.IsErrNotExist(err) {
|
2019-03-19 03:29:43 +01:00
|
|
|
|
ctx.NotFound()
|
2014-11-17 03:32:26 +01:00
|
|
|
|
} else {
|
2022-05-09 17:54:51 +02:00
|
|
|
|
ctx.Error(http.StatusInternalServerError, "GetTreeEntryByPath", err)
|
2014-11-17 03:32:26 +01:00
|
|
|
|
}
|
2023-07-07 07:31:56 +02:00
|
|
|
|
return nil, nil, nil
|
2014-11-17 03:32:26 +01:00
|
|
|
|
}
|
2022-05-09 17:54:51 +02:00
|
|
|
|
|
|
|
|
|
if entry.IsDir() || entry.IsSubModule() {
|
|
|
|
|
ctx.NotFound("getBlobForEntry", nil)
|
2023-07-07 07:31:56 +02:00
|
|
|
|
return nil, nil, nil
|
2014-11-17 03:32:26 +01:00
|
|
|
|
}
|
2022-05-09 17:54:51 +02:00
|
|
|
|
|
2022-07-25 17:39:42 +02:00
|
|
|
|
info, _, err := git.Entries([]*git.TreeEntry{entry}).GetCommitsInfo(ctx, ctx.Repo.Commit, path.Dir("/" + ctx.Repo.TreePath)[1:])
|
2022-05-09 17:54:51 +02:00
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.Error(http.StatusInternalServerError, "GetCommitsInfo", err)
|
2023-07-07 07:31:56 +02:00
|
|
|
|
return nil, nil, nil
|
2022-05-09 17:54:51 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if len(info) == 1 {
|
|
|
|
|
// Not Modified
|
2023-07-07 07:31:56 +02:00
|
|
|
|
lastModified = &info[0].Commit.Committer.When
|
2022-05-09 17:54:51 +02:00
|
|
|
|
}
|
|
|
|
|
blob = entry.Blob()
|
|
|
|
|
|
2022-07-21 21:18:41 +02:00
|
|
|
|
return blob, entry, lastModified
|
2014-11-17 03:32:26 +01:00
|
|
|
|
}
|
2015-09-02 15:54:35 +02:00
|
|
|
|
|
2016-11-24 08:04:31 +01:00
|
|
|
|
// GetArchive get archive of a repository
|
2016-03-13 23:49:16 +01:00
|
|
|
|
func GetArchive(ctx *context.APIContext) {
|
2018-06-12 16:59:22 +02:00
|
|
|
|
// swagger:operation GET /repos/{owner}/{repo}/archive/{archive} repository repoGetArchive
|
2017-11-13 08:02:25 +01:00
|
|
|
|
// ---
|
|
|
|
|
// summary: Get an archive of a repository
|
|
|
|
|
// produces:
|
2024-01-19 01:14:49 +01:00
|
|
|
|
// - application/octet-stream
|
|
|
|
|
// - application/zip
|
|
|
|
|
// - application/gzip
|
2017-11-13 08:02:25 +01:00
|
|
|
|
// parameters:
|
|
|
|
|
// - name: owner
|
|
|
|
|
// in: path
|
|
|
|
|
// description: owner of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: repo
|
|
|
|
|
// in: path
|
|
|
|
|
// description: name of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: archive
|
|
|
|
|
// in: path
|
2020-09-06 18:23:47 +02:00
|
|
|
|
// description: the git reference for download with attached archive format (e.g. master.zip)
|
2017-11-13 08:02:25 +01:00
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// responses:
|
2018-06-01 07:51:49 +02:00
|
|
|
|
// 200:
|
|
|
|
|
// description: success
|
2019-12-20 18:07:12 +01:00
|
|
|
|
// "404":
|
|
|
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
|
|
2021-09-18 02:54:15 +02:00
|
|
|
|
if ctx.Repo.GitRepo == nil {
|
Simplify how git repositories are opened (#28937)
## Purpose
This is a refactor toward building an abstraction over managing git
repositories.
Afterwards, it does not matter anymore if they are stored on the local
disk or somewhere remote.
## What this PR changes
We used `git.OpenRepository` everywhere previously.
Now, we should split them into two distinct functions:
Firstly, there are temporary repositories which do not change:
```go
git.OpenRepository(ctx, diskPath)
```
Gitea managed repositories having a record in the database in the
`repository` table are moved into the new package `gitrepo`:
```go
gitrepo.OpenRepository(ctx, repo_model.Repo)
```
Why is `repo_model.Repository` the second parameter instead of file
path?
Because then we can easily adapt our repository storage strategy.
The repositories can be stored locally, however, they could just as well
be stored on a remote server.
## Further changes in other PRs
- A Git Command wrapper on package `gitrepo` could be created. i.e.
`NewCommand(ctx, repo_model.Repository, commands...)`. `git.RunOpts{Dir:
repo.RepoPath()}`, the directory should be empty before invoking this
method and it can be filled in the function only. #28940
- Remove the `RepoPath()`/`WikiPath()` functions to reduce the
possibility of mistakes.
---------
Co-authored-by: delvh <dev.lh@web.de>
2024-01-27 21:09:51 +01:00
|
|
|
|
gitRepo, err := gitrepo.OpenRepository(ctx, ctx.Repo.Repository)
|
2021-09-18 02:54:15 +02:00
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
ctx.Repo.GitRepo = gitRepo
|
|
|
|
|
defer gitRepo.Close()
|
2015-09-02 15:54:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
2022-08-29 11:45:20 +02:00
|
|
|
|
archiveDownload(ctx)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func archiveDownload(ctx *context.APIContext) {
|
|
|
|
|
uri := ctx.Params("*")
|
2024-04-02 16:34:57 +02:00
|
|
|
|
aReq, err := archiver_service.NewRequest(ctx, ctx.Repo.Repository.ID, ctx.Repo.GitRepo, uri)
|
2022-08-29 11:45:20 +02:00
|
|
|
|
if err != nil {
|
|
|
|
|
if errors.Is(err, archiver_service.ErrUnknownArchiveFormat{}) {
|
|
|
|
|
ctx.Error(http.StatusBadRequest, "unknown archive format", err)
|
|
|
|
|
} else if errors.Is(err, archiver_service.RepoRefNotFoundError{}) {
|
|
|
|
|
ctx.Error(http.StatusNotFound, "unrecognized reference", err)
|
|
|
|
|
} else {
|
|
|
|
|
ctx.ServerError("archiver_service.NewRequest", err)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
archiver, err := aReq.Await(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.ServerError("archiver.Await", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
download(ctx, aReq.GetArchiveName(), archiver)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func download(ctx *context.APIContext, archiveName string, archiver *repo_model.RepoArchiver) {
|
|
|
|
|
downloadName := ctx.Repo.Repository.Name + "-" + archiveName
|
|
|
|
|
|
Add an immutable tarball link to archive download headers for Nix
This allows `nix flake metadata` and nix in general to lock a *branch*
tarball link in a manner that causes it to fetch the correct commit even
if the branch is updated with a newer version.
For further context, Nix flakes are a feature that, among other things,
allows for "inputs" that are "github:someuser/somerepo",
"https://some-tarball-service/some-tarball.tar.gz",
"sourcehut:~meow/nya" or similar. This feature allows our users to fetch
tarballs of git-based inputs to their builds rather than using git to
fetch them, saving significant download time.
There is presently no gitea or forgejo specific fetcher in Nix, and we
don't particularly wish to have one. Ideally (as a developer on a Nix
implementation myself) we could just use the generic tarball fetcher and
not add specific forgejo support, but to do so, we need additional
metadata to know which commit a given *branch* tarball represents, which
is the purpose of the Link header added here.
The result of this patch is that a Nix user can specify `inputs.something.url =
"https://forgejo-host/some/project/archive/main.tar.gz"` in flake.nix
and get a link to some concrete tarball for the actual commit in the
lock file, then when they run `nix flake update` in the future, they
will get the latest commit in that branch.
Example of it working locally:
» nix flake metadata --refresh 'http://localhost:3000/api/v1/repos/jade/cats/archive/main.tar.gz?dir=configs/nix'
Resolved URL: http://localhost:3000/api/v1/repos/jade/cats/archive/main.tar.gz?dir=configs/nix
Locked URL: http://localhost:3000/api/v1/repos/jade/cats/archive/804ede182b6b66469b23ea4d21eece52766b7a06.tar.gz?dir=configs
/nix&narHash=sha256-yP7KkDVfuixZzs0fsqhSETXFC0y8m6nmPLw2GrAMxKQ%3D
Description: Computers with the nixos
Path: /nix/store/s856c6yqghyan4v0zy6jj19ksv0q22nx-source
Revision: 804ede182b6b66469b23ea4d21eece52766b7a06
Last modified: 2024-05-02 00:48:32
For details on the header value, see:
https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md
2024-05-03 02:51:26 +02:00
|
|
|
|
// Add nix format link header so tarballs lock correctly:
|
|
|
|
|
// https://github.com/nixos/nix/blob/56763ff918eb308db23080e560ed2ea3e00c80a7/doc/manual/src/protocols/tarball-fetcher.md
|
|
|
|
|
ctx.Resp.Header().Add("Link", fmt.Sprintf("<%s/archive/%s.tar.gz?rev=%s>; rel=\"immutable\"",
|
|
|
|
|
ctx.Repo.Repository.APIURL(),
|
|
|
|
|
archiver.CommitID, archiver.CommitID))
|
|
|
|
|
|
2022-08-29 11:45:20 +02:00
|
|
|
|
rPath := archiver.RelativePath()
|
2023-06-14 05:42:38 +02:00
|
|
|
|
if setting.RepoArchive.Storage.MinioConfig.ServeDirect {
|
2022-08-29 11:45:20 +02:00
|
|
|
|
// If we have a signed url (S3, object storage), redirect to this directly.
|
|
|
|
|
u, err := storage.RepoArchives.URL(rPath, downloadName)
|
|
|
|
|
if u != nil && err == nil {
|
|
|
|
|
ctx.Redirect(u.String())
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we have matched and access to release or issue
|
|
|
|
|
fr, err := storage.RepoArchives.Open(rPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.ServerError("Open", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer fr.Close()
|
2022-11-24 15:25:13 +01:00
|
|
|
|
|
2024-01-19 01:14:49 +01:00
|
|
|
|
contentType := ""
|
|
|
|
|
switch archiver.Type {
|
|
|
|
|
case git.ZIP:
|
|
|
|
|
contentType = "application/zip"
|
|
|
|
|
case git.TARGZ:
|
|
|
|
|
// Per RFC6713.
|
|
|
|
|
contentType = "application/gzip"
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-24 15:25:13 +01:00
|
|
|
|
ctx.ServeContent(fr, &context.ServeHeaderOptions{
|
2024-01-19 01:14:49 +01:00
|
|
|
|
ContentType: contentType,
|
2022-11-24 15:25:13 +01:00
|
|
|
|
Filename: downloadName,
|
|
|
|
|
LastModified: archiver.CreatedUnix.AsLocalTime(),
|
|
|
|
|
})
|
2015-09-02 15:54:35 +02:00
|
|
|
|
}
|
2016-08-31 01:18:40 +02:00
|
|
|
|
|
2016-11-24 08:04:31 +01:00
|
|
|
|
// GetEditorconfig get editor config of a repository
|
2016-08-31 01:18:40 +02:00
|
|
|
|
func GetEditorconfig(ctx *context.APIContext) {
|
2017-11-13 08:02:25 +01:00
|
|
|
|
// swagger:operation GET /repos/{owner}/{repo}/editorconfig/{filepath} repository repoGetEditorConfig
|
|
|
|
|
// ---
|
|
|
|
|
// summary: Get the EditorConfig definitions of a file in a repository
|
|
|
|
|
// produces:
|
|
|
|
|
// - application/json
|
|
|
|
|
// parameters:
|
|
|
|
|
// - name: owner
|
|
|
|
|
// in: path
|
|
|
|
|
// description: owner of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: repo
|
|
|
|
|
// in: path
|
|
|
|
|
// description: name of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: filepath
|
|
|
|
|
// in: path
|
|
|
|
|
// description: filepath of file to get
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
2022-04-21 17:17:57 +02:00
|
|
|
|
// - name: ref
|
|
|
|
|
// in: query
|
|
|
|
|
// description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
|
|
|
|
|
// type: string
|
|
|
|
|
// required: false
|
2017-11-13 08:02:25 +01:00
|
|
|
|
// responses:
|
2018-06-01 07:51:49 +02:00
|
|
|
|
// 200:
|
|
|
|
|
// description: success
|
2019-12-20 18:07:12 +01:00
|
|
|
|
// "404":
|
|
|
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
|
|
2023-04-06 22:01:20 +02:00
|
|
|
|
ec, _, err := ctx.Repo.GetEditorconfig(ctx.Repo.Commit)
|
2016-08-31 01:18:40 +02:00
|
|
|
|
if err != nil {
|
|
|
|
|
if git.IsErrNotExist(err) {
|
2019-03-19 03:29:43 +01:00
|
|
|
|
ctx.NotFound(err)
|
2016-08-31 01:18:40 +02:00
|
|
|
|
} else {
|
2019-04-17 18:06:35 +02:00
|
|
|
|
ctx.Error(http.StatusInternalServerError, "GetEditorconfig", err)
|
2016-08-31 01:18:40 +02:00
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fileName := ctx.Params("filename")
|
2019-10-17 02:15:02 +02:00
|
|
|
|
def, err := ec.GetDefinitionForFilename(fileName)
|
2016-08-31 01:18:40 +02:00
|
|
|
|
if def == nil {
|
2019-03-19 03:29:43 +01:00
|
|
|
|
ctx.NotFound(err)
|
2016-08-31 01:18:40 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
2019-04-17 18:06:35 +02:00
|
|
|
|
ctx.JSON(http.StatusOK, def)
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-24 18:20:22 +02:00
|
|
|
|
// canWriteFiles returns true if repository is editable and user has proper access level.
|
2022-04-28 17:45:33 +02:00
|
|
|
|
func canWriteFiles(ctx *context.APIContext, branch string) bool {
|
2023-07-22 16:14:27 +02:00
|
|
|
|
return ctx.Repo.CanWriteToBranch(ctx, ctx.Doer, branch) &&
|
2022-04-28 17:45:33 +02:00
|
|
|
|
!ctx.Repo.Repository.IsMirror &&
|
|
|
|
|
!ctx.Repo.Repository.IsArchived
|
2019-04-17 18:06:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
2020-04-24 18:20:22 +02:00
|
|
|
|
// canReadFiles returns true if repository is readable and user has proper access level.
|
|
|
|
|
func canReadFiles(r *context.Repository) bool {
|
2021-11-09 20:57:58 +01:00
|
|
|
|
return r.Permission.CanRead(unit.TypeCode)
|
2019-04-17 18:06:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
2024-02-19 15:50:03 +01:00
|
|
|
|
func base64Reader(s string) (io.ReadSeeker, error) {
|
2023-07-18 20:14:47 +02:00
|
|
|
|
b, err := base64.StdEncoding.DecodeString(s)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
return bytes.NewReader(b), nil
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-07 17:49:58 +02:00
|
|
|
|
// ChangeFiles handles API call for modifying multiple files
|
2023-05-29 11:41:35 +02:00
|
|
|
|
func ChangeFiles(ctx *context.APIContext) {
|
|
|
|
|
// swagger:operation POST /repos/{owner}/{repo}/contents repository repoChangeFiles
|
|
|
|
|
// ---
|
2023-06-07 17:49:58 +02:00
|
|
|
|
// summary: Modify multiple files in a repository
|
2023-05-29 11:41:35 +02:00
|
|
|
|
// consumes:
|
|
|
|
|
// - application/json
|
|
|
|
|
// produces:
|
|
|
|
|
// - application/json
|
|
|
|
|
// parameters:
|
|
|
|
|
// - name: owner
|
|
|
|
|
// in: path
|
|
|
|
|
// description: owner of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: repo
|
|
|
|
|
// in: path
|
|
|
|
|
// description: name of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: body
|
|
|
|
|
// in: body
|
|
|
|
|
// required: true
|
|
|
|
|
// schema:
|
|
|
|
|
// "$ref": "#/definitions/ChangeFilesOptions"
|
|
|
|
|
// responses:
|
|
|
|
|
// "201":
|
|
|
|
|
// "$ref": "#/responses/FilesResponse"
|
|
|
|
|
// "403":
|
|
|
|
|
// "$ref": "#/responses/error"
|
|
|
|
|
// "404":
|
|
|
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
|
// "422":
|
|
|
|
|
// "$ref": "#/responses/error"
|
2023-09-22 01:43:29 +02:00
|
|
|
|
// "423":
|
|
|
|
|
// "$ref": "#/responses/repoArchivedError"
|
2023-05-29 11:41:35 +02:00
|
|
|
|
|
|
|
|
|
apiOpts := web.GetForm(ctx).(*api.ChangeFilesOptions)
|
|
|
|
|
|
|
|
|
|
if apiOpts.BranchName == "" {
|
|
|
|
|
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-18 20:14:47 +02:00
|
|
|
|
var files []*files_service.ChangeRepoFile
|
2023-05-29 11:41:35 +02:00
|
|
|
|
for _, file := range apiOpts.Files {
|
2023-07-18 20:14:47 +02:00
|
|
|
|
contentReader, err := base64Reader(file.ContentBase64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "Invalid base64 content", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-05-29 11:41:35 +02:00
|
|
|
|
changeRepoFile := &files_service.ChangeRepoFile{
|
2023-07-18 20:14:47 +02:00
|
|
|
|
Operation: file.Operation,
|
|
|
|
|
TreePath: file.Path,
|
|
|
|
|
FromTreePath: file.FromPath,
|
|
|
|
|
ContentReader: contentReader,
|
|
|
|
|
SHA: file.SHA,
|
2023-05-29 11:41:35 +02:00
|
|
|
|
}
|
|
|
|
|
files = append(files, changeRepoFile)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
opts := &files_service.ChangeRepoFilesOptions{
|
|
|
|
|
Files: files,
|
|
|
|
|
Message: apiOpts.Message,
|
|
|
|
|
OldBranch: apiOpts.BranchName,
|
|
|
|
|
NewBranch: apiOpts.NewBranchName,
|
|
|
|
|
Committer: &files_service.IdentityOptions{
|
|
|
|
|
Name: apiOpts.Committer.Name,
|
|
|
|
|
Email: apiOpts.Committer.Email,
|
|
|
|
|
},
|
|
|
|
|
Author: &files_service.IdentityOptions{
|
|
|
|
|
Name: apiOpts.Author.Name,
|
|
|
|
|
Email: apiOpts.Author.Email,
|
|
|
|
|
},
|
|
|
|
|
Dates: &files_service.CommitDateOptions{
|
|
|
|
|
Author: apiOpts.Dates.Author,
|
|
|
|
|
Committer: apiOpts.Dates.Committer,
|
|
|
|
|
},
|
|
|
|
|
Signoff: apiOpts.Signoff,
|
|
|
|
|
}
|
|
|
|
|
if opts.Dates.Author.IsZero() {
|
|
|
|
|
opts.Dates.Author = time.Now()
|
|
|
|
|
}
|
|
|
|
|
if opts.Dates.Committer.IsZero() {
|
|
|
|
|
opts.Dates.Committer = time.Now()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if opts.Message == "" {
|
|
|
|
|
opts.Message = changeFilesCommitMessage(ctx, files)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
|
|
|
|
|
handleCreateOrUpdateFileError(ctx, err)
|
|
|
|
|
} else {
|
|
|
|
|
ctx.JSON(http.StatusCreated, filesResponse)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-17 18:06:35 +02:00
|
|
|
|
// CreateFile handles API call for creating a file
|
2021-01-26 16:36:53 +01:00
|
|
|
|
func CreateFile(ctx *context.APIContext) {
|
2019-04-17 18:06:35 +02:00
|
|
|
|
// swagger:operation POST /repos/{owner}/{repo}/contents/{filepath} repository repoCreateFile
|
|
|
|
|
// ---
|
|
|
|
|
// summary: Create a file in a repository
|
|
|
|
|
// consumes:
|
|
|
|
|
// - application/json
|
|
|
|
|
// produces:
|
|
|
|
|
// - application/json
|
|
|
|
|
// parameters:
|
|
|
|
|
// - name: owner
|
|
|
|
|
// in: path
|
|
|
|
|
// description: owner of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: repo
|
|
|
|
|
// in: path
|
|
|
|
|
// description: name of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: filepath
|
|
|
|
|
// in: path
|
|
|
|
|
// description: path of the file to create
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: body
|
|
|
|
|
// in: body
|
2019-05-30 19:57:55 +02:00
|
|
|
|
// required: true
|
2019-04-17 18:06:35 +02:00
|
|
|
|
// schema:
|
|
|
|
|
// "$ref": "#/definitions/CreateFileOptions"
|
|
|
|
|
// responses:
|
|
|
|
|
// "201":
|
|
|
|
|
// "$ref": "#/responses/FileResponse"
|
2020-05-31 22:59:34 +02:00
|
|
|
|
// "403":
|
|
|
|
|
// "$ref": "#/responses/error"
|
|
|
|
|
// "404":
|
|
|
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
|
// "422":
|
|
|
|
|
// "$ref": "#/responses/error"
|
2023-09-22 01:43:29 +02:00
|
|
|
|
// "423":
|
|
|
|
|
// "$ref": "#/responses/repoArchivedError"
|
2020-05-31 22:59:34 +02:00
|
|
|
|
|
2021-01-26 16:36:53 +01:00
|
|
|
|
apiOpts := web.GetForm(ctx).(*api.CreateFileOptions)
|
2019-04-17 18:06:35 +02:00
|
|
|
|
|
2020-04-20 18:47:05 +02:00
|
|
|
|
if apiOpts.BranchName == "" {
|
|
|
|
|
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-18 20:14:47 +02:00
|
|
|
|
contentReader, err := base64Reader(apiOpts.ContentBase64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "Invalid base64 content", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-29 11:41:35 +02:00
|
|
|
|
opts := &files_service.ChangeRepoFilesOptions{
|
|
|
|
|
Files: []*files_service.ChangeRepoFile{
|
|
|
|
|
{
|
2023-07-18 20:14:47 +02:00
|
|
|
|
Operation: "create",
|
|
|
|
|
TreePath: ctx.Params("*"),
|
|
|
|
|
ContentReader: contentReader,
|
2023-05-29 11:41:35 +02:00
|
|
|
|
},
|
|
|
|
|
},
|
2019-04-17 18:06:35 +02:00
|
|
|
|
Message: apiOpts.Message,
|
|
|
|
|
OldBranch: apiOpts.BranchName,
|
|
|
|
|
NewBranch: apiOpts.NewBranchName,
|
2021-11-24 08:56:24 +01:00
|
|
|
|
Committer: &files_service.IdentityOptions{
|
2019-04-17 18:06:35 +02:00
|
|
|
|
Name: apiOpts.Committer.Name,
|
|
|
|
|
Email: apiOpts.Committer.Email,
|
|
|
|
|
},
|
2021-11-24 08:56:24 +01:00
|
|
|
|
Author: &files_service.IdentityOptions{
|
2019-04-17 18:06:35 +02:00
|
|
|
|
Name: apiOpts.Author.Name,
|
|
|
|
|
Email: apiOpts.Author.Email,
|
|
|
|
|
},
|
2021-11-24 08:56:24 +01:00
|
|
|
|
Dates: &files_service.CommitDateOptions{
|
2019-12-24 03:33:52 +01:00
|
|
|
|
Author: apiOpts.Dates.Author,
|
|
|
|
|
Committer: apiOpts.Dates.Committer,
|
|
|
|
|
},
|
2021-01-29 09:57:45 +01:00
|
|
|
|
Signoff: apiOpts.Signoff,
|
2019-12-24 03:33:52 +01:00
|
|
|
|
}
|
|
|
|
|
if opts.Dates.Author.IsZero() {
|
|
|
|
|
opts.Dates.Author = time.Now()
|
|
|
|
|
}
|
|
|
|
|
if opts.Dates.Committer.IsZero() {
|
|
|
|
|
opts.Dates.Committer = time.Now()
|
2019-04-17 18:06:35 +02:00
|
|
|
|
}
|
2019-06-29 17:19:24 +02:00
|
|
|
|
|
|
|
|
|
if opts.Message == "" {
|
2023-05-29 11:41:35 +02:00
|
|
|
|
opts.Message = changeFilesCommitMessage(ctx, opts.Files)
|
2019-06-29 17:19:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
2023-05-29 11:41:35 +02:00
|
|
|
|
if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
|
2020-05-31 22:59:34 +02:00
|
|
|
|
handleCreateOrUpdateFileError(ctx, err)
|
2019-04-17 18:06:35 +02:00
|
|
|
|
} else {
|
2023-05-29 11:41:35 +02:00
|
|
|
|
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
2019-04-17 18:06:35 +02:00
|
|
|
|
ctx.JSON(http.StatusCreated, fileResponse)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UpdateFile handles API call for updating a file
|
2021-01-26 16:36:53 +01:00
|
|
|
|
func UpdateFile(ctx *context.APIContext) {
|
2019-04-17 18:06:35 +02:00
|
|
|
|
// swagger:operation PUT /repos/{owner}/{repo}/contents/{filepath} repository repoUpdateFile
|
|
|
|
|
// ---
|
|
|
|
|
// summary: Update a file in a repository
|
|
|
|
|
// consumes:
|
|
|
|
|
// - application/json
|
|
|
|
|
// produces:
|
|
|
|
|
// - application/json
|
|
|
|
|
// parameters:
|
|
|
|
|
// - name: owner
|
|
|
|
|
// in: path
|
|
|
|
|
// description: owner of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: repo
|
|
|
|
|
// in: path
|
|
|
|
|
// description: name of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: filepath
|
|
|
|
|
// in: path
|
|
|
|
|
// description: path of the file to update
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: body
|
|
|
|
|
// in: body
|
2019-05-30 19:57:55 +02:00
|
|
|
|
// required: true
|
2019-04-17 18:06:35 +02:00
|
|
|
|
// schema:
|
|
|
|
|
// "$ref": "#/definitions/UpdateFileOptions"
|
|
|
|
|
// responses:
|
|
|
|
|
// "200":
|
|
|
|
|
// "$ref": "#/responses/FileResponse"
|
2020-05-31 22:59:34 +02:00
|
|
|
|
// "403":
|
|
|
|
|
// "$ref": "#/responses/error"
|
|
|
|
|
// "404":
|
|
|
|
|
// "$ref": "#/responses/notFound"
|
|
|
|
|
// "422":
|
|
|
|
|
// "$ref": "#/responses/error"
|
2023-09-22 01:43:29 +02:00
|
|
|
|
// "423":
|
|
|
|
|
// "$ref": "#/responses/repoArchivedError"
|
2021-01-26 16:36:53 +01:00
|
|
|
|
apiOpts := web.GetForm(ctx).(*api.UpdateFileOptions)
|
2020-05-31 22:59:34 +02:00
|
|
|
|
if ctx.Repo.Repository.IsEmpty {
|
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "RepoIsEmpty", fmt.Errorf("repo is empty"))
|
2024-02-27 16:09:13 +01:00
|
|
|
|
return
|
2020-05-31 22:59:34 +02:00
|
|
|
|
}
|
2019-04-17 18:06:35 +02:00
|
|
|
|
|
2020-04-20 18:47:05 +02:00
|
|
|
|
if apiOpts.BranchName == "" {
|
|
|
|
|
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-18 20:14:47 +02:00
|
|
|
|
contentReader, err := base64Reader(apiOpts.ContentBase64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "Invalid base64 content", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-29 11:41:35 +02:00
|
|
|
|
opts := &files_service.ChangeRepoFilesOptions{
|
|
|
|
|
Files: []*files_service.ChangeRepoFile{
|
|
|
|
|
{
|
2023-07-18 20:14:47 +02:00
|
|
|
|
Operation: "update",
|
|
|
|
|
ContentReader: contentReader,
|
|
|
|
|
SHA: apiOpts.SHA,
|
|
|
|
|
FromTreePath: apiOpts.FromPath,
|
|
|
|
|
TreePath: ctx.Params("*"),
|
2023-05-29 11:41:35 +02:00
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Message: apiOpts.Message,
|
|
|
|
|
OldBranch: apiOpts.BranchName,
|
|
|
|
|
NewBranch: apiOpts.NewBranchName,
|
2021-11-24 08:56:24 +01:00
|
|
|
|
Committer: &files_service.IdentityOptions{
|
2019-04-17 18:06:35 +02:00
|
|
|
|
Name: apiOpts.Committer.Name,
|
|
|
|
|
Email: apiOpts.Committer.Email,
|
|
|
|
|
},
|
2021-11-24 08:56:24 +01:00
|
|
|
|
Author: &files_service.IdentityOptions{
|
2019-04-17 18:06:35 +02:00
|
|
|
|
Name: apiOpts.Author.Name,
|
|
|
|
|
Email: apiOpts.Author.Email,
|
|
|
|
|
},
|
2021-11-24 08:56:24 +01:00
|
|
|
|
Dates: &files_service.CommitDateOptions{
|
2019-12-24 03:33:52 +01:00
|
|
|
|
Author: apiOpts.Dates.Author,
|
|
|
|
|
Committer: apiOpts.Dates.Committer,
|
|
|
|
|
},
|
2021-01-29 09:57:45 +01:00
|
|
|
|
Signoff: apiOpts.Signoff,
|
2019-12-24 03:33:52 +01:00
|
|
|
|
}
|
|
|
|
|
if opts.Dates.Author.IsZero() {
|
|
|
|
|
opts.Dates.Author = time.Now()
|
|
|
|
|
}
|
|
|
|
|
if opts.Dates.Committer.IsZero() {
|
|
|
|
|
opts.Dates.Committer = time.Now()
|
2019-04-17 18:06:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 17:19:24 +02:00
|
|
|
|
if opts.Message == "" {
|
2023-05-29 11:41:35 +02:00
|
|
|
|
opts.Message = changeFilesCommitMessage(ctx, opts.Files)
|
2019-06-29 17:19:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
2023-05-29 11:41:35 +02:00
|
|
|
|
if filesResponse, err := createOrUpdateFiles(ctx, opts); err != nil {
|
2020-05-31 22:59:34 +02:00
|
|
|
|
handleCreateOrUpdateFileError(ctx, err)
|
2019-04-17 18:06:35 +02:00
|
|
|
|
} else {
|
2023-05-29 11:41:35 +02:00
|
|
|
|
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
2019-04-17 18:06:35 +02:00
|
|
|
|
ctx.JSON(http.StatusOK, fileResponse)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-31 22:59:34 +02:00
|
|
|
|
func handleCreateOrUpdateFileError(ctx *context.APIContext, err error) {
|
|
|
|
|
if models.IsErrUserCannotCommit(err) || models.IsErrFilePathProtected(err) {
|
|
|
|
|
ctx.Error(http.StatusForbidden, "Access", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-06-29 12:03:20 +02:00
|
|
|
|
if git_model.IsErrBranchAlreadyExists(err) || models.IsErrFilenameInvalid(err) || models.IsErrSHADoesNotMatch(err) ||
|
2020-05-31 22:59:34 +02:00
|
|
|
|
models.IsErrFilePathInvalid(err) || models.IsErrRepoFileAlreadyExists(err) {
|
|
|
|
|
ctx.Error(http.StatusUnprocessableEntity, "Invalid", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
2023-06-29 12:03:20 +02:00
|
|
|
|
if git_model.IsErrBranchNotExist(err) || git.IsErrBranchNotExist(err) {
|
2020-06-07 19:30:58 +02:00
|
|
|
|
ctx.Error(http.StatusNotFound, "BranchDoesNotExist", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
2020-05-31 22:59:34 +02:00
|
|
|
|
|
|
|
|
|
ctx.Error(http.StatusInternalServerError, "UpdateFile", err)
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-17 18:06:35 +02:00
|
|
|
|
// Called from both CreateFile or UpdateFile to handle both
|
2023-05-29 11:41:35 +02:00
|
|
|
|
func createOrUpdateFiles(ctx *context.APIContext, opts *files_service.ChangeRepoFilesOptions) (*api.FilesResponse, error) {
|
2022-04-28 17:45:33 +02:00
|
|
|
|
if !canWriteFiles(ctx, opts.OldBranch) {
|
2022-06-13 11:37:59 +02:00
|
|
|
|
return nil, repo_model.ErrUserDoesNotHaveAccessToRepo{
|
2022-03-22 08:03:22 +01:00
|
|
|
|
UserID: ctx.Doer.ID,
|
2019-04-17 18:06:35 +02:00
|
|
|
|
RepoName: ctx.Repo.Repository.LowerName,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-29 11:41:35 +02:00
|
|
|
|
return files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// format commit message if empty
|
|
|
|
|
func changeFilesCommitMessage(ctx *context.APIContext, files []*files_service.ChangeRepoFile) string {
|
|
|
|
|
var (
|
|
|
|
|
createFiles []string
|
|
|
|
|
updateFiles []string
|
|
|
|
|
deleteFiles []string
|
|
|
|
|
)
|
|
|
|
|
for _, file := range files {
|
|
|
|
|
switch file.Operation {
|
|
|
|
|
case "create":
|
|
|
|
|
createFiles = append(createFiles, file.TreePath)
|
|
|
|
|
case "update":
|
|
|
|
|
updateFiles = append(updateFiles, file.TreePath)
|
|
|
|
|
case "delete":
|
|
|
|
|
deleteFiles = append(deleteFiles, file.TreePath)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
message := ""
|
|
|
|
|
if len(createFiles) != 0 {
|
2024-02-14 22:48:45 +01:00
|
|
|
|
message += ctx.Locale.TrString("repo.editor.add", strings.Join(createFiles, ", ")+"\n")
|
2023-05-29 11:41:35 +02:00
|
|
|
|
}
|
|
|
|
|
if len(updateFiles) != 0 {
|
2024-02-14 22:48:45 +01:00
|
|
|
|
message += ctx.Locale.TrString("repo.editor.update", strings.Join(updateFiles, ", ")+"\n")
|
2023-05-29 11:41:35 +02:00
|
|
|
|
}
|
|
|
|
|
if len(deleteFiles) != 0 {
|
2024-02-14 22:48:45 +01:00
|
|
|
|
message += ctx.Locale.TrString("repo.editor.delete", strings.Join(deleteFiles, ", "))
|
2023-05-29 11:41:35 +02:00
|
|
|
|
}
|
|
|
|
|
return strings.Trim(message, "\n")
|
2019-04-17 18:06:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
2022-01-10 10:32:37 +01:00
|
|
|
|
// DeleteFile Delete a file in a repository
|
2021-01-26 16:36:53 +01:00
|
|
|
|
func DeleteFile(ctx *context.APIContext) {
|
2019-04-17 18:06:35 +02:00
|
|
|
|
// swagger:operation DELETE /repos/{owner}/{repo}/contents/{filepath} repository repoDeleteFile
|
|
|
|
|
// ---
|
|
|
|
|
// summary: Delete a file in a repository
|
|
|
|
|
// consumes:
|
|
|
|
|
// - application/json
|
|
|
|
|
// produces:
|
|
|
|
|
// - application/json
|
|
|
|
|
// parameters:
|
|
|
|
|
// - name: owner
|
|
|
|
|
// in: path
|
|
|
|
|
// description: owner of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: repo
|
|
|
|
|
// in: path
|
|
|
|
|
// description: name of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: filepath
|
|
|
|
|
// in: path
|
|
|
|
|
// description: path of the file to delete
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: body
|
|
|
|
|
// in: body
|
2019-05-30 19:57:55 +02:00
|
|
|
|
// required: true
|
2019-04-17 18:06:35 +02:00
|
|
|
|
// schema:
|
|
|
|
|
// "$ref": "#/definitions/DeleteFileOptions"
|
|
|
|
|
// responses:
|
|
|
|
|
// "200":
|
|
|
|
|
// "$ref": "#/responses/FileDeleteResponse"
|
2020-04-15 07:18:51 +02:00
|
|
|
|
// "400":
|
|
|
|
|
// "$ref": "#/responses/error"
|
|
|
|
|
// "403":
|
|
|
|
|
// "$ref": "#/responses/error"
|
|
|
|
|
// "404":
|
|
|
|
|
// "$ref": "#/responses/error"
|
2023-09-22 01:43:29 +02:00
|
|
|
|
// "423":
|
|
|
|
|
// "$ref": "#/responses/repoArchivedError"
|
2019-12-20 18:07:12 +01:00
|
|
|
|
|
2021-01-26 16:36:53 +01:00
|
|
|
|
apiOpts := web.GetForm(ctx).(*api.DeleteFileOptions)
|
2022-04-28 17:45:33 +02:00
|
|
|
|
if !canWriteFiles(ctx, apiOpts.BranchName) {
|
2022-06-13 11:37:59 +02:00
|
|
|
|
ctx.Error(http.StatusForbidden, "DeleteFile", repo_model.ErrUserDoesNotHaveAccessToRepo{
|
2022-03-22 08:03:22 +01:00
|
|
|
|
UserID: ctx.Doer.ID,
|
2019-04-17 18:06:35 +02:00
|
|
|
|
RepoName: ctx.Repo.Repository.LowerName,
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-20 18:47:05 +02:00
|
|
|
|
if apiOpts.BranchName == "" {
|
|
|
|
|
apiOpts.BranchName = ctx.Repo.Repository.DefaultBranch
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-29 11:41:35 +02:00
|
|
|
|
opts := &files_service.ChangeRepoFilesOptions{
|
|
|
|
|
Files: []*files_service.ChangeRepoFile{
|
|
|
|
|
{
|
|
|
|
|
Operation: "delete",
|
|
|
|
|
SHA: apiOpts.SHA,
|
|
|
|
|
TreePath: ctx.Params("*"),
|
|
|
|
|
},
|
|
|
|
|
},
|
2019-04-17 18:06:35 +02:00
|
|
|
|
Message: apiOpts.Message,
|
|
|
|
|
OldBranch: apiOpts.BranchName,
|
|
|
|
|
NewBranch: apiOpts.NewBranchName,
|
2021-11-24 08:56:24 +01:00
|
|
|
|
Committer: &files_service.IdentityOptions{
|
2019-04-17 18:06:35 +02:00
|
|
|
|
Name: apiOpts.Committer.Name,
|
|
|
|
|
Email: apiOpts.Committer.Email,
|
|
|
|
|
},
|
2021-11-24 08:56:24 +01:00
|
|
|
|
Author: &files_service.IdentityOptions{
|
2019-04-17 18:06:35 +02:00
|
|
|
|
Name: apiOpts.Author.Name,
|
|
|
|
|
Email: apiOpts.Author.Email,
|
|
|
|
|
},
|
2021-11-24 08:56:24 +01:00
|
|
|
|
Dates: &files_service.CommitDateOptions{
|
2019-12-24 03:33:52 +01:00
|
|
|
|
Author: apiOpts.Dates.Author,
|
|
|
|
|
Committer: apiOpts.Dates.Committer,
|
|
|
|
|
},
|
2021-01-29 09:57:45 +01:00
|
|
|
|
Signoff: apiOpts.Signoff,
|
2019-12-24 03:33:52 +01:00
|
|
|
|
}
|
|
|
|
|
if opts.Dates.Author.IsZero() {
|
|
|
|
|
opts.Dates.Author = time.Now()
|
|
|
|
|
}
|
|
|
|
|
if opts.Dates.Committer.IsZero() {
|
|
|
|
|
opts.Dates.Committer = time.Now()
|
2019-04-17 18:06:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 17:19:24 +02:00
|
|
|
|
if opts.Message == "" {
|
2023-05-29 11:41:35 +02:00
|
|
|
|
opts.Message = changeFilesCommitMessage(ctx, opts.Files)
|
2019-06-29 17:19:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
2023-05-29 11:41:35 +02:00
|
|
|
|
if filesResponse, err := files_service.ChangeRepoFiles(ctx, ctx.Repo.Repository, ctx.Doer, opts); err != nil {
|
2020-04-15 07:18:51 +02:00
|
|
|
|
if git.IsErrBranchNotExist(err) || models.IsErrRepoFileDoesNotExist(err) || git.IsErrNotExist(err) {
|
|
|
|
|
ctx.Error(http.StatusNotFound, "DeleteFile", err)
|
|
|
|
|
return
|
2023-06-29 12:03:20 +02:00
|
|
|
|
} else if git_model.IsErrBranchAlreadyExists(err) ||
|
2020-04-15 07:18:51 +02:00
|
|
|
|
models.IsErrFilenameInvalid(err) ||
|
|
|
|
|
models.IsErrSHADoesNotMatch(err) ||
|
|
|
|
|
models.IsErrCommitIDDoesNotMatch(err) ||
|
|
|
|
|
models.IsErrSHAOrCommitIDNotProvided(err) {
|
|
|
|
|
ctx.Error(http.StatusBadRequest, "DeleteFile", err)
|
|
|
|
|
return
|
|
|
|
|
} else if models.IsErrUserCannotCommit(err) {
|
|
|
|
|
ctx.Error(http.StatusForbidden, "DeleteFile", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-04-17 18:06:35 +02:00
|
|
|
|
ctx.Error(http.StatusInternalServerError, "DeleteFile", err)
|
|
|
|
|
} else {
|
2023-05-29 11:41:35 +02:00
|
|
|
|
fileResponse := files_service.GetFileResponseFromFilesResponse(filesResponse, 0)
|
2020-04-15 07:18:51 +02:00
|
|
|
|
ctx.JSON(http.StatusOK, fileResponse) // FIXME on APIv2: return http.StatusNoContent
|
2019-04-17 18:06:35 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-29 22:51:10 +02:00
|
|
|
|
// GetContents Get the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
|
|
|
|
|
func GetContents(ctx *context.APIContext) {
|
|
|
|
|
// swagger:operation GET /repos/{owner}/{repo}/contents/{filepath} repository repoGetContents
|
2019-04-17 18:06:35 +02:00
|
|
|
|
// ---
|
2019-06-29 22:51:10 +02:00
|
|
|
|
// summary: Gets the metadata and contents (if a file) of an entry in a repository, or a list of entries if a dir
|
2019-04-17 18:06:35 +02:00
|
|
|
|
// produces:
|
|
|
|
|
// - application/json
|
|
|
|
|
// parameters:
|
|
|
|
|
// - name: owner
|
|
|
|
|
// in: path
|
|
|
|
|
// description: owner of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: repo
|
|
|
|
|
// in: path
|
|
|
|
|
// description: name of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: filepath
|
|
|
|
|
// in: path
|
2019-06-29 22:51:10 +02:00
|
|
|
|
// description: path of the dir, file, symlink or submodule in the repo
|
2019-04-17 18:06:35 +02:00
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: ref
|
|
|
|
|
// in: query
|
|
|
|
|
// description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
|
|
|
|
|
// type: string
|
2019-06-29 22:51:10 +02:00
|
|
|
|
// required: false
|
2019-04-17 18:06:35 +02:00
|
|
|
|
// responses:
|
|
|
|
|
// "200":
|
2019-06-29 22:51:10 +02:00
|
|
|
|
// "$ref": "#/responses/ContentsResponse"
|
2020-04-15 07:18:51 +02:00
|
|
|
|
// "404":
|
|
|
|
|
// "$ref": "#/responses/notFound"
|
2019-04-17 18:06:35 +02:00
|
|
|
|
|
2020-04-24 18:20:22 +02:00
|
|
|
|
if !canReadFiles(ctx.Repo) {
|
2022-06-13 11:37:59 +02:00
|
|
|
|
ctx.Error(http.StatusInternalServerError, "GetContentsOrList", repo_model.ErrUserDoesNotHaveAccessToRepo{
|
2022-03-22 08:03:22 +01:00
|
|
|
|
UserID: ctx.Doer.ID,
|
2019-04-17 18:06:35 +02:00
|
|
|
|
RepoName: ctx.Repo.Repository.LowerName,
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
treePath := ctx.Params("*")
|
2021-07-29 03:42:15 +02:00
|
|
|
|
ref := ctx.FormTrim("ref")
|
2019-04-17 18:06:35 +02:00
|
|
|
|
|
2022-01-20 00:26:57 +01:00
|
|
|
|
if fileList, err := files_service.GetContentsOrList(ctx, ctx.Repo.Repository, treePath, ref); err != nil {
|
2020-04-15 07:18:51 +02:00
|
|
|
|
if git.IsErrNotExist(err) {
|
|
|
|
|
ctx.NotFound("GetContentsOrList", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-06-29 22:51:10 +02:00
|
|
|
|
ctx.Error(http.StatusInternalServerError, "GetContentsOrList", err)
|
2019-04-17 18:06:35 +02:00
|
|
|
|
} else {
|
2019-06-29 22:51:10 +02:00
|
|
|
|
ctx.JSON(http.StatusOK, fileList)
|
2019-04-17 18:06:35 +02:00
|
|
|
|
}
|
2016-08-31 01:18:40 +02:00
|
|
|
|
}
|
2019-06-29 22:51:10 +02:00
|
|
|
|
|
|
|
|
|
// GetContentsList Get the metadata of all the entries of the root dir
|
|
|
|
|
func GetContentsList(ctx *context.APIContext) {
|
|
|
|
|
// swagger:operation GET /repos/{owner}/{repo}/contents repository repoGetContentsList
|
|
|
|
|
// ---
|
|
|
|
|
// summary: Gets the metadata of all the entries of the root dir
|
|
|
|
|
// produces:
|
|
|
|
|
// - application/json
|
|
|
|
|
// parameters:
|
|
|
|
|
// - name: owner
|
|
|
|
|
// in: path
|
|
|
|
|
// description: owner of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: repo
|
|
|
|
|
// in: path
|
|
|
|
|
// description: name of the repo
|
|
|
|
|
// type: string
|
|
|
|
|
// required: true
|
|
|
|
|
// - name: ref
|
|
|
|
|
// in: query
|
|
|
|
|
// description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
|
|
|
|
|
// type: string
|
|
|
|
|
// required: false
|
|
|
|
|
// responses:
|
|
|
|
|
// "200":
|
|
|
|
|
// "$ref": "#/responses/ContentsListResponse"
|
2020-04-15 07:18:51 +02:00
|
|
|
|
// "404":
|
|
|
|
|
// "$ref": "#/responses/notFound"
|
2019-06-29 22:51:10 +02:00
|
|
|
|
|
|
|
|
|
// same as GetContents(), this function is here because swagger fails if path is empty in GetContents() interface
|
|
|
|
|
GetContents(ctx)
|
|
|
|
|
}
|