diff --git a/Makefile b/Makefile index e044ed09f269..7dd7464acb2f 100644 --- a/Makefile +++ b/Makefile @@ -190,7 +190,7 @@ help: go-check: $(eval GO_VERSION := $(shell printf "%03d%03d%03d" $(shell $(GO) version | grep -Eo '[0-9]+\.[0-9.]+' | tr '.' ' ');)) @if [ "$(GO_VERSION)" -lt "$(MIN_GO_VERSION)" ]; then \ - echo "Gitea requires Go 1.13 or greater to build. You can get it at https://golang.org/dl/"; \ + echo "Gitea requires Go 1.14 or greater to build. You can get it at https://golang.org/dl/"; \ exit 1; \ fi diff --git a/build/generate-emoji.go b/build/generate-emoji.go index 00d60acacc8c..c544fd0af98e 100644 --- a/build/generate-emoji.go +++ b/build/generate-emoji.go @@ -174,7 +174,7 @@ func generate() ([]byte, error) { s = append(s, k) } else { // insert into slice after first element because all emoji that support skin tones - // have that modifer placed at this spot + // have that modifier placed at this spot s = append(s, "") copy(s[2:], s[1:]) s[1] = k diff --git a/cmd/dump.go b/cmd/dump.go index 65e2c817f92b..1acc69f1c856 100644 --- a/cmd/dump.go +++ b/cmd/dump.go @@ -49,38 +49,6 @@ func addFile(w archiver.Writer, filePath string, absPath string, verbose bool) e }) } -func addRecursive(w archiver.Writer, dirPath string, absPath string, verbose bool) error { - if verbose { - log.Info("Adding dir %s\n", dirPath) - } - dir, err := os.Open(absPath) - if err != nil { - return fmt.Errorf("Could not open directory %s: %s", absPath, err) - } - defer dir.Close() - - files, err := dir.Readdir(0) - if err != nil { - return fmt.Errorf("Unable to list files in %s: %s", absPath, err) - } - - if err := addFile(w, dirPath, absPath, false); err != nil { - return err - } - - for _, fileInfo := range files { - if fileInfo.IsDir() { - err = addRecursive(w, filepath.Join(dirPath, fileInfo.Name()), filepath.Join(absPath, fileInfo.Name()), verbose) - } else { - err = addFile(w, filepath.Join(dirPath, fileInfo.Name()), filepath.Join(absPath, fileInfo.Name()), verbose) - } - if err != nil { - return err - } - } - return nil -} - func isSubdir(upper string, lower string) (bool, error) { if relPath, err := filepath.Rel(upper, lower); err != nil { return false, err @@ -157,6 +125,10 @@ It can be used for backup and capture Gitea server image to send to maintainer`, Name: "skip-log, L", Usage: "Skip the log dumping", }, + cli.BoolFlag{ + Name: "skip-custom-dir", + Usage: "Skip custom directory", + }, cli.GenericFlag{ Name: "type", Value: outputTypeEnum, @@ -211,6 +183,11 @@ func runDump(ctx *cli.Context) error { } defer file.Close() + absFileName, err := filepath.Abs(fileName) + if err != nil { + return err + } + verbose := ctx.Bool("verbose") outType := ctx.String("type") var iface interface{} @@ -233,7 +210,7 @@ func runDump(ctx *cli.Context) error { log.Info("Skip dumping local repositories") } else { log.Info("Dumping local repositories... %s", setting.RepoRootPath) - if err := addRecursive(w, "repos", setting.RepoRootPath, verbose); err != nil { + if err := addRecursiveExclude(w, "repos", setting.RepoRootPath, []string{absFileName}, verbose); err != nil { fatal("Failed to include repositories: %v", err) } @@ -292,17 +269,21 @@ func runDump(ctx *cli.Context) error { } } - customDir, err := os.Stat(setting.CustomPath) - if err == nil && customDir.IsDir() { - if is, _ := isSubdir(setting.AppDataPath, setting.CustomPath); !is { - if err := addRecursive(w, "custom", setting.CustomPath, verbose); err != nil { - fatal("Failed to include custom: %v", err) + if ctx.IsSet("skip-custom-dir") && ctx.Bool("skip-custom-dir") { + log.Info("Skiping custom directory") + } else { + customDir, err := os.Stat(setting.CustomPath) + if err == nil && customDir.IsDir() { + if is, _ := isSubdir(setting.AppDataPath, setting.CustomPath); !is { + if err := addRecursiveExclude(w, "custom", setting.CustomPath, []string{absFileName}, verbose); err != nil { + fatal("Failed to include custom: %v", err) + } + } else { + log.Info("Custom dir %s is inside data dir %s, skipped", setting.CustomPath, setting.AppDataPath) } } else { - log.Info("Custom dir %s is inside data dir %s, skipped", setting.CustomPath, setting.AppDataPath) + log.Info("Custom dir %s doesn't exist, skipped", setting.CustomPath) } - } else { - log.Info("Custom dir %s doesn't exist, skipped", setting.CustomPath) } isExist, err := util.IsExist(setting.AppDataPath) @@ -325,6 +306,7 @@ func runDump(ctx *cli.Context) error { excludes = append(excludes, setting.LFS.Path) excludes = append(excludes, setting.Attachment.Path) excludes = append(excludes, setting.LogRootPath) + excludes = append(excludes, absFileName) if err := addRecursiveExclude(w, "data", setting.AppDataPath, excludes, verbose); err != nil { fatal("Failed to include data directory: %v", err) } @@ -358,7 +340,7 @@ func runDump(ctx *cli.Context) error { log.Error("Unable to check if %s exists. Error: %v", setting.LogRootPath, err) } if isExist { - if err := addRecursive(w, "log", setting.LogRootPath, verbose); err != nil { + if err := addRecursiveExclude(w, "log", setting.LogRootPath, []string{absFileName}, verbose); err != nil { fatal("Failed to include log: %v", err) } } diff --git a/docs/content/doc/installation/from-source.en-us.md b/docs/content/doc/installation/from-source.en-us.md index bff206a86213..5525faf3d574 100644 --- a/docs/content/doc/installation/from-source.en-us.md +++ b/docs/content/doc/installation/from-source.en-us.md @@ -186,3 +186,11 @@ CC=aarch64-unknown-linux-gnu-gcc GOOS=linux GOARCH=arm64 TAGS="bindata sqlite sq ``` Replace `CC`, `GOOS`, and `GOARCH` as appropriate for your architecture target. + +You will sometimes need to build a static compiled image. To do this you will need to add: + +``` +LDFLAGS="-linkmode external -extldflags '-static' $LDFLAGS" TAGS="netgo osusergo $TAGS" make build +``` + +This can be combined with `CC`, `GOOS`, and `GOARCH` as above. diff --git a/docs/content/doc/installation/with-docker.zh-cn.md b/docs/content/doc/installation/with-docker.zh-cn.md index af9b0d45142a..d32b774c201a 100644 --- a/docs/content/doc/installation/with-docker.zh-cn.md +++ b/docs/content/doc/installation/with-docker.zh-cn.md @@ -1,6 +1,6 @@ --- date: "2016-12-01T16:00:00+02:00" -title: "从Docker安装" +title: "使用 Docker 安装" slug: "install-with-docker" weight: 10 toc: false @@ -8,39 +8,338 @@ draft: false menu: sidebar: parent: "installation" - name: "从Docker安装" + name: "使用 Docker 安装" weight: 10 identifier: "install-with-docker" --- -# 从Docker安装 +# 使用 Docker 安装 -阅读本章之前我们已经假设您对docker已经有了解并能够正常使用docker。 +Gitea 在其 Docker Hub 组织内提供自动更新的 Docker 镜像。可以始终使用最新的稳定标签或使用其他服务来更新 Docker 镜像。 + +该参考设置指导用户完成基于 `docker-compose` 的设置,但是 `docker-compose` 的安装不在本文档的范围之内。要安装 `docker-compose` 本身,请遵循官方[安装说明](https://docs.docker.com/compose/install/)。 {{< toc >}} -我们在 Docker Hub 的 Gitea 组织中提供了自动更新的 Docker 镜像,它会保持最新的稳定版。你也可以用其它 Docker 服务来更新。首先你需要pull镜像: +## 基本 + +最简单的设置只是创建一个卷和一个网络,然后将 `gitea/gitea:latest` 镜像作为服务启动。由于没有可用的数据库,因此可以使用 SQLite3 初始化数据库。创建一个类似 `gitea` 的目录,并将以下内容粘贴到名为 `docker-compose.yml` 的文件中。请注意,该卷应由配置文件中指定的 UID/GID 的用户/组拥有。如果您不授予卷正确的权限,则容器可能无法启动。另请注意,标签 `:latest` 将安装当前的开发版本。对于稳定的发行版,您可以使用 `:1` 或指定某个发行版,例如 `:1.13.0`。 + +```yaml +version: "3" + +networks: + gitea: + external: false + +services: + server: + image: gitea/gitea:{{< version >}} + container_name: gitea + environment: + - USER_UID=1000 + - USER_GID=1000 + restart: always + networks: + - gitea + volumes: + - ./gitea:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "3000:3000" + - "222:22" +``` + +## 端口 + +要将集成的 openSSH 守护进程和 Web 服务器绑定到其他端口,请调整端口部分。通常,只需更改主机端口,容器内的端口保持原样即可。 + +```diff +version: "3" + +networks: + gitea: + external: false + +services: + server: + image: gitea/gitea:{{< version >}} + container_name: gitea + environment: + - USER_UID=1000 + - USER_GID=1000 + restart: always + networks: + - gitea + volumes: + - ./gitea:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: +- - "3000:3000" +- - "222:22" ++ - "8080:3000" ++ - "2221:22" +``` + +## 数据库 + +### MySQL 数据库 + +要将 Gitea 与 MySQL 数据库结合使用,请将这些更改应用于上面创建的 `docker-compose.yml` 文件。 + +```diff +version: "3" + +networks: + gitea: + external: false + +services: + server: + image: gitea/gitea:{{< version >}} + container_name: gitea + environment: + - USER_UID=1000 + - USER_GID=1000 ++ - DB_TYPE=mysql ++ - DB_HOST=db:3306 ++ - DB_NAME=gitea ++ - DB_USER=gitea ++ - DB_PASSWD=gitea + restart: always + networks: + - gitea + volumes: + - ./gitea:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "3000:3000" + - "222:22" ++ depends_on: ++ - db ++ ++ db: ++ image: mysql:5.7 ++ restart: always ++ environment: ++ - MYSQL_ROOT_PASSWORD=gitea ++ - MYSQL_USER=gitea ++ - MYSQL_PASSWORD=gitea ++ - MYSQL_DATABASE=gitea ++ networks: ++ - gitea ++ volumes: ++ - ./mysql:/var/lib/mysql +``` + +### PostgreSQL 数据库 + +要将 Gitea 与 PostgreSQL 数据库结合使用,请将这些更改应用于上面创建的 `docker-compose.yml` 文件。 + +```diff +version: "3" + +networks: + gitea: + external: false + +services: + server: + image: gitea/gitea:{{< version >}} + container_name: gitea + environment: + - USER_UID=1000 + - USER_GID=1000 ++ - DB_TYPE=postgres ++ - DB_HOST=db:5432 ++ - DB_NAME=gitea ++ - DB_USER=gitea ++ - DB_PASSWD=gitea + restart: always + networks: + - gitea + volumes: + - ./gitea:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "3000:3000" + - "222:22" ++ depends_on: ++ - db ++ ++ db: ++ image: postgres:9.6 ++ restart: always ++ environment: ++ - POSTGRES_USER=gitea ++ - POSTGRES_PASSWORD=gitea ++ - POSTGRES_DB=gitea ++ networks: ++ - gitea ++ volumes: ++ - ./postgres:/var/lib/postgresql/data +``` + +## 命名卷 + +要使用命名卷而不是主机卷,请在 `docker-compose.yml` 配置中定义并使用命名卷。此更改将自动创建所需的卷。您无需担心命名卷的权限;Docker 将自动处理该问题。 + +```diff +version: "3" + +networks: + gitea: + external: false + ++volumes: ++ gitea: ++ driver: local ++ +services: + server: + image: gitea/gitea:{{< version >}} + container_name: gitea + restart: always + networks: + - gitea + volumes: +- - ./gitea:/data ++ - gitea:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "3000:3000" + - "222:22" +``` + +MySQL 或 PostgreSQL 容器将需要分别创建。 + +## 启动 + +要基于 `docker-compose` 启动此设置,请执行 `docker-compose up -d`,以在后台启动 Gitea。使用 `docker-compose ps` 将显示 Gitea 是否正确启动。可以使用 `docker-compose logs` 查看日志。 + +要关闭设置,请执行 `docker-compose down`。这将停止并杀死容器。这些卷将仍然存在。 + +注意:如果在 http 上使用非 3000 端口,请更改 app.ini 以匹配 `LOCAL_ROOT_URL = http://localhost:3000/`。 + +## 安装 + +通过 `docker-compose` 启动 Docker 安装后,应该可以使用喜欢的浏览器访问 Gitea,以完成安装。访问 http://server-ip:3000 并遵循安装向导。如果数据库是通过上述 `docker-compose` 设置启动的,请注意,必须将 `db` 用作数据库主机名。 + +## 环境变量 + +您可以通过环境变量配置 Gitea 的一些设置: + +(默认值以**粗体**显示) + +- `APP_NAME`:**“Gitea: Git with a cup of tea”**:应用程序名称,在页面标题中使用。 +- `RUN_MODE`:**prod**:应用程序运行模式,会影响性能和调试。"dev","prod"或"test"。 +- `DOMAIN`:**localhost**:此服务器的域名,用于 Gitea UI 中显示的 http 克隆 URL。 +- `SSH_DOMAIN`:**localhost**:该服务器的域名,用于 Gitea UI 中显示的 ssh 克隆 URL。如果启用了安装页面,则 SSH 域服务器将采用以下形式的 DOMAIN 值(保存时将覆盖此设置)。 +- `SSH_PORT`:**22**:克隆 URL 中显示的 SSH 端口。 +- `SSH_LISTEN_PORT`:**%(SSH_PORT)s**:内置 SSH 服务器的端口。 +- `DISABLE_SSH`:**false**:如果不可用,请禁用 SSH 功能。如果要禁用 SSH 功能,则在安装 Gitea 时应将 SSH 端口设置为 `0`。 +- `HTTP_PORT`:**3000**:HTTP 监听端口。 +- `ROOT_URL`:**""**:覆盖自动生成的公共 URL。如果内部 URL 和外部 URL 不匹配(例如在 Docker 中),这很有用。 +- `LFS_START_SERVER`:**false**:启用 git-lfs 支持。 +- `DB_TYPE`:**sqlite3**:正在使用的数据库类型[mysql,postgres,mssql,sqlite3]。 +- `DB_HOST`:**localhost:3306**:数据库主机地址和端口。 +- `DB_NAME`:**gitea**:数据库名称。 +- `DB_USER`:**root**:数据库用户名。 +- `DB_PASSWD`:**"\"** :数据库用户密码。如果您在密码中使用特殊字符,请使用“您的密码”进行引用。 +- `INSTALL_LOCK`:**false**:禁止访问安装页面。 +- `SECRET_KEY`:**""** :全局密钥。这应该更改。如果它具有一个值并且 `INSTALL_LOCK` 为空,则 `INSTALL_LOCK` 将自动设置为 `true`。 +- `DISABLE_REGISTRATION`:**false**:禁用注册,之后只有管理员才能为用户创建帐户。 +- `REQUIRE_SIGNIN_VIEW`:**false**:启用此选项可强制用户登录以查看任何页面。 +- `USER_UID`:**1000**:在容器内运行 Gitea 的用户的 UID(Unix 用户 ID)。如果使用主机卷,则将其与 `/data` 卷的所有者的 UID 匹配(对于命名卷,则不需要这样做)。 +- `USER_GID`:**1000**:在容器内运行 Gitea 的用户的 GID(Unix 组 ID)。如果使用主机卷,则将其与 `/data` 卷的所有者的 GID 匹配(对于命名卷,则不需要这样做)。 + +## 自定义 + +[此处](https://docs.gitea.io/zh-cn/customizing-gitea/)描述的定制文件应放在 `/data/gitea` 目录中。如果使用主机卷,则访问这些文件非常容易;对于命名卷,可以通过另一个容器或通过直接访问 `/var/lib/docker/volumes/gitea_gitea/_data` 来完成。安装后,配置文件将保存在 `/data/gitea/conf/app.ini` 中。 + +## 升级 + +:exclamation::exclamation: **确保已将数据卷到 Docker 容器外部的某个位置** :exclamation::exclamation: + +要将安装升级到最新版本: +```bash +# Edit `docker-compose.yml` to update the version, if you have one specified +# Pull new images +docker-compose pull +# Start a new container, automatically removes old one +docker-compose up -d ``` -docker pull gitea/gitea:latest + +## SSH 容器直通 + +由于 SSH 在容器内运行,因此,如果需要 SSH 支持,则需要将 SSH 从主机传递到容器。一种选择是在非标准端口上运行容器 SSH(或将主机端口移至非标准端口)。另一个可能更直接的选择是将 SSH 连接从主机转发到容器。下面将说明此设置。 + +本指南假定您已经在名为 `git` 的主机上创建了一个用户,该用户与容器值 `USER_UID`/`USER_GID` 共享相同的 `UID`/`GID`。这些值可以在 `docker-compose.yml` 中设置为环境变量: + +```bash +environment: + - USER_UID=1000 + - USER_GID=1000 ``` -如果要将git和其它数据持久化,你需要创建一个目录来作为数据存储的地方: +接下来将主机的 `/home/git/.ssh` 装入容器。否则,SSH 身份验证将无法在容器内运行。 +```bash +volumes: + - /home/git/.ssh/:/data/git/.ssh ``` -sudo mkdir -p /var/lib/gitea + +现在,需要在主机上创建 SSH 密钥对。该密钥对将用于向主机验证主机上的 `git` 用户。 + +```bash +sudo -u git ssh-keygen -t rsa -b 4096 -C "Gitea Host Key" ``` -然后就可以运行 docker 容器了,这很简单。 当然你需要定义端口数数据目录: +在下一步中,需要在主机上创建一个名为 `/app/gitea/gitea` 的文件(具有可执行权限)。该文件将发出从主机到容器的 SSH 转发。将以下内容添加到 `/app/gitea/gitea`: +```bash +ssh -p 2222 -o StrictHostKeyChecking=no git@127.0.0.1 "SSH_ORIGINAL_COMMAND=\"$SSH_ORIGINAL_COMMAND\" $0 $@" ``` -docker run -d --name=gitea -p 10022:22 -p 10080:3000 -v /var/lib/gitea:/data gitea/gitea:latest + +为了使转发正常工作,需要将容器(22)的 SSH 端口映射到 `docker-compose.yml` 中的主机端口 2222。由于此端口不需要暴露给外界,因此可以将其映射到主机的 `localhost`: + +```bash +ports: + # [...] + - "127.0.0.1:2222:22" ``` -然后 容器已经运行成功,在浏览器中访问 http://hostname:10080 就可以看到界面了。你可以尝试在上面创建项目,clone操作 `git clone ssh://git@hostname:10022/username/repo.git`. +另外,主机上的 `/home/git/.ssh/authorized_keys` 需要修改。它需要以与 Gitea 容器内的 `authorized_keys` 相同的方式进行操作。因此,将您在上面创建的密钥(“Gitea 主机密钥”)的公共密钥添加到 `~/git/.ssh/authorized_keys`。这可以通过 `echo "$(cat /home/git/.ssh/id_rsa.pub)" >> /home/git/.ssh/authorized_keys` 完成。重要提示:来自 `git` 用户的公钥需要“按原样”添加,而通过 Gitea 网络界面添加的所有其他公钥将以 `command="/app [...]` 作为前缀。 + +该文件应该看起来像: + +```bash +# SSH pubkey from git user +ssh-rsa + +# other keys from users +command="/app/gitea/gitea --config=/data/gitea/conf/app.ini serv key-1",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty +``` + +这是详细的说明,当发出 SSH 请求时会发生什么: + +1. 使用 `git` 用户向主机发出 SSH 请求,例如 `git clone git@domain:user/repo.git`。 +2. 在 `/home/git/.ssh/authorized_keys` 中,该命令执行 `/app/gitea/gitea` 脚本。 +3. `/app/gitea/gitea` 将 SSH 请求转发到端口 2222,该端口已映射到容器的 SSH 端口(22)。 +4. 由于 `/home/git/.ssh/authorized_keys` 中存在 `git` 用户的公钥,因此身份验证主机 → 容器成功,并且 SSH 请求转发到在 docker 容器中运行的 Gitea。 + +如果在 Gitea Web 界面中添加了新的 SSH 密钥,它将以与现有密钥相同的方式附加到 `.ssh/authorized_keys` 中。 -注意:目前端口改为非3000时,需要修改配置文件 `LOCAL_ROOT_URL = http://localhost:3000/`。 +**注意** -## 需要帮助? +SSH 容器直通仅在以下情况下有效 -如果从本页中没有找到你需要的内容,请访问 [帮助页面]({{< relref "seek-help.zh-cn.md" >}}) +- 在容器中使用 `opensshd` +- 如果未将 `AuthorizedKeysCommand` 与 `SSH_CREATE_AUTHORIZED_KEYS_FILE = false` 结合使用以禁用授权文件密钥生成 +- `LOCAL_ROOT_URL` 不变 diff --git a/docs/content/doc/usage/command-line.en-us.md b/docs/content/doc/usage/command-line.en-us.md index 192eaa2c5aa4..97882f9203b7 100644 --- a/docs/content/doc/usage/command-line.en-us.md +++ b/docs/content/doc/usage/command-line.en-us.md @@ -253,6 +253,7 @@ in the current directory. - `--file name`, `-f name`: Name of the dump file with will be created. Optional. (default: gitea-dump-[timestamp].zip). - `--tempdir path`, `-t path`: Path to the temporary directory used. Optional. (default: /tmp). - `--skip-repository`, `-R`: Skip the repository dumping. Optional. + - `--skip-custom-dir`: Skip dumping of the custom dir. Optional. - `--database`, `-d`: Specify the database SQL syntax. Optional. - `--verbose`, `-V`: If provided, shows additional details. Optional. - Examples: diff --git a/integrations/api_releases_test.go b/integrations/api_releases_test.go index 870d7d0e64d6..26bf752ccae9 100644 --- a/integrations/api_releases_test.go +++ b/integrations/api_releases_test.go @@ -7,7 +7,6 @@ package integrations import ( "fmt" "net/http" - "strings" "testing" "code.gitea.io/gitea/models" @@ -152,10 +151,10 @@ func TestAPIGetReleaseByTag(t *testing.T) { var err *api.APIError DecodeJSON(t, resp, &err) - assert.True(t, strings.HasPrefix(err.Message, "release tag does not exist")) + assert.EqualValues(t, "Not Found", err.Message) } -func TestAPIDeleteTagByName(t *testing.T) { +func TestAPIDeleteReleaseByTagName(t *testing.T) { defer prepareTestEnv(t)() repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) @@ -163,17 +162,17 @@ func TestAPIDeleteTagByName(t *testing.T) { session := loginUser(t, owner.LowerName) token := getTokenForLoggedInUser(t, session) - urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/delete-tag?token=%s", - owner.Name, repo.Name, token) + createNewReleaseUsingAPI(t, session, token, owner, repo, "release-tag", "", "Release Tag", "test") - req := NewRequestf(t, http.MethodDelete, urlStr) + // delete release + req := NewRequestf(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/release-tag?token=%s", owner.Name, repo.Name, token)) _ = session.MakeRequest(t, req, http.StatusNoContent) - // Make sure that actual releases can't be deleted outright - createNewReleaseUsingAPI(t, session, token, owner, repo, "release-tag", "", "Release Tag", "test") - urlStr = fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/release-tag?token=%s", - owner.Name, repo.Name, token) + // make sure release is deleted + req = NewRequestf(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/release-tag?token=%s", owner.Name, repo.Name, token)) + _ = session.MakeRequest(t, req, http.StatusNotFound) - req = NewRequestf(t, http.MethodDelete, urlStr) - _ = session.MakeRequest(t, req, http.StatusConflict) + // delete release tag too + req = NewRequestf(t, http.MethodDelete, fmt.Sprintf("/api/v1/repos/%s/%s/tags/release-tag?token=%s", owner.Name, repo.Name, token)) + _ = session.MakeRequest(t, req, http.StatusNoContent) } diff --git a/integrations/api_repo_git_commits_test.go b/integrations/api_repo_git_commits_test.go index 5b0f82e85412..d6bd5fc62e6b 100644 --- a/integrations/api_repo_git_commits_test.go +++ b/integrations/api_repo_git_commits_test.go @@ -14,6 +14,14 @@ import ( "github.com/stretchr/testify/assert" ) +func compareCommitFiles(t *testing.T, expect []string, files []*api.CommitAffectedFiles) { + var actual []string + for i := range files { + actual = append(actual, files[i].Filename) + } + assert.ElementsMatch(t, expect, actual) +} + func TestAPIReposGitCommits(t *testing.T) { defer prepareTestEnv(t)() user := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User) @@ -56,10 +64,13 @@ func TestAPIReposGitCommitList(t *testing.T) { var apiData []api.Commit DecodeJSON(t, resp, &apiData) - assert.Equal(t, 3, len(apiData)) - assert.Equal(t, "69554a64c1e6030f051e5c3f94bfbd773cd6a324", apiData[0].CommitMeta.SHA) - assert.Equal(t, "27566bd5738fc8b4e3fef3c5e72cce608537bd95", apiData[1].CommitMeta.SHA) - assert.Equal(t, "5099b81332712fe655e34e8dd63574f503f61811", apiData[2].CommitMeta.SHA) + assert.Len(t, apiData, 3) + assert.EqualValues(t, "69554a64c1e6030f051e5c3f94bfbd773cd6a324", apiData[0].CommitMeta.SHA) + compareCommitFiles(t, []string{"readme.md"}, apiData[0].Files) + assert.EqualValues(t, "27566bd5738fc8b4e3fef3c5e72cce608537bd95", apiData[1].CommitMeta.SHA) + compareCommitFiles(t, []string{"readme.md"}, apiData[1].Files) + assert.EqualValues(t, "5099b81332712fe655e34e8dd63574f503f61811", apiData[2].CommitMeta.SHA) + compareCommitFiles(t, []string{"readme.md"}, apiData[2].Files) } func TestAPIReposGitCommitListPage2Empty(t *testing.T) { @@ -76,7 +87,7 @@ func TestAPIReposGitCommitListPage2Empty(t *testing.T) { var apiData []api.Commit DecodeJSON(t, resp, &apiData) - assert.Equal(t, 0, len(apiData)) + assert.Len(t, apiData, 0) } func TestAPIReposGitCommitListDifferentBranch(t *testing.T) { @@ -93,6 +104,7 @@ func TestAPIReposGitCommitListDifferentBranch(t *testing.T) { var apiData []api.Commit DecodeJSON(t, resp, &apiData) - assert.Equal(t, 1, len(apiData)) + assert.Len(t, apiData, 1) assert.Equal(t, "f27c2b2b03dcab38beaf89b0ab4ff61f6de63441", apiData[0].CommitMeta.SHA) + compareCommitFiles(t, []string{"readme.md"}, apiData[0].Files) } diff --git a/integrations/api_repo_git_tags_test.go b/integrations/api_repo_git_tags_test.go index ad710a45204d..bf6fc7c85813 100644 --- a/integrations/api_repo_git_tags_test.go +++ b/integrations/api_repo_git_tags_test.go @@ -5,6 +5,7 @@ package integrations import ( + "fmt" "net/http" "testing" @@ -59,3 +60,26 @@ func TestAPIGitTags(t *testing.T) { badReq := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/git/tags/%s?token=%s", user.Name, repo.Name, commit.ID.String(), token) session.MakeRequest(t, badReq, http.StatusBadRequest) } + +func TestAPIDeleteTagByName(t *testing.T) { + defer prepareTestEnv(t)() + + repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository) + owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repo.OwnerID}).(*models.User) + session := loginUser(t, owner.LowerName) + token := getTokenForLoggedInUser(t, session) + + urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/tags/delete-tag?token=%s", + owner.Name, repo.Name, token) + + req := NewRequestf(t, http.MethodDelete, urlStr) + _ = session.MakeRequest(t, req, http.StatusNoContent) + + // Make sure that actual releases can't be deleted outright + createNewReleaseUsingAPI(t, session, token, owner, repo, "release-tag", "", "Release Tag", "test") + urlStr = fmt.Sprintf("/api/v1/repos/%s/%s/tags/release-tag?token=%s", + owner.Name, repo.Name, token) + + req = NewRequestf(t, http.MethodDelete, urlStr) + _ = session.MakeRequest(t, req, http.StatusConflict) +} diff --git a/integrations/branches_test.go b/integrations/branches_test.go index 2b9fc8dda5d7..b2230e7031bf 100644 --- a/integrations/branches_test.go +++ b/integrations/branches_test.go @@ -57,7 +57,9 @@ func branchAction(t *testing.T, button string) (*HTMLDoc, string) { htmlDoc := NewHTMLParser(t, resp.Body) link, exists := htmlDoc.doc.Find(button).Attr("data-url") - assert.True(t, exists, "The template has changed") + if !assert.True(t, exists, "The template has changed") { + t.Skip() + } req = NewRequestWithValues(t, "POST", link, map[string]string{ "_csrf": getCsrf(t, htmlDoc.doc), @@ -69,7 +71,7 @@ func branchAction(t *testing.T, button string) (*HTMLDoc, string) { req = NewRequest(t, "GET", "/user2/repo1/branches") resp = session.MakeRequest(t, req, http.StatusOK) - return NewHTMLParser(t, resp.Body), url.Query()["name"][0] + return NewHTMLParser(t, resp.Body), url.Query().Get("name") } func getCsrf(t *testing.T, doc *goquery.Document) string { diff --git a/models/commit_status_test.go b/models/commit_status_test.go index 90d72cd74dfb..57b97f66063e 100644 --- a/models/commit_status_test.go +++ b/models/commit_status_test.go @@ -18,7 +18,7 @@ func TestGetCommitStatuses(t *testing.T) { sha1 := "1234123412341234123412341234123412341234" - statuses, maxResults, err := GetCommitStatuses(repo1, sha1, &CommitStatusOptions{}) + statuses, maxResults, err := GetCommitStatuses(repo1, sha1, &CommitStatusOptions{ListOptions: ListOptions{Page: 1, PageSize: 50}}) assert.NoError(t, err) assert.Equal(t, int(maxResults), 5) assert.Len(t, statuses, 5) diff --git a/models/gpg_key.go b/models/gpg_key.go index b944fdcbffe4..3e8ddd9621c1 100644 --- a/models/gpg_key.go +++ b/models/gpg_key.go @@ -65,7 +65,11 @@ func (key *GPGKey) AfterLoad(session *xorm.Session) { // ListGPGKeys returns a list of public keys belongs to given user. func ListGPGKeys(uid int64, listOptions ListOptions) ([]*GPGKey, error) { - sess := x.Where("owner_id=? AND primary_key_id=''", uid) + return listGPGKeys(x, uid, listOptions) +} + +func listGPGKeys(e Engine, uid int64, listOptions ListOptions) ([]*GPGKey, error) { + sess := e.Table(&GPGKey{}).Where("owner_id=? AND primary_key_id=''", uid) if listOptions.Page != 0 { sess = listOptions.setSessionPagination(sess) } diff --git a/models/issue.go b/models/issue.go index a7392633af51..b903e82ad7d4 100644 --- a/models/issue.go +++ b/models/issue.go @@ -745,11 +745,11 @@ func AddDeletePRBranchComment(doer *User, repo *Repository, issueID int64, branc return err } var opts = &CreateCommentOptions{ - Type: CommentTypeDeleteBranch, - Doer: doer, - Repo: repo, - Issue: issue, - CommitSHA: branchName, + Type: CommentTypeDeleteBranch, + Doer: doer, + Repo: repo, + Issue: issue, + OldRef: branchName, } if _, err = createComment(sess, opts); err != nil { return err diff --git a/models/list_options.go b/models/list_options.go index 0912355352d6..9cccd05465af 100644 --- a/models/list_options.go +++ b/models/list_options.go @@ -16,13 +16,13 @@ type ListOptions struct { Page int // start from 1 } -func (opts ListOptions) getPaginatedSession() *xorm.Session { +func (opts *ListOptions) getPaginatedSession() *xorm.Session { opts.setDefaultValues() return x.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) } -func (opts ListOptions) setSessionPagination(sess *xorm.Session) *xorm.Session { +func (opts *ListOptions) setSessionPagination(sess *xorm.Session) *xorm.Session { opts.setDefaultValues() if opts.PageSize <= 0 { @@ -31,21 +31,21 @@ func (opts ListOptions) setSessionPagination(sess *xorm.Session) *xorm.Session { return sess.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) } -func (opts ListOptions) setEnginePagination(e Engine) Engine { +func (opts *ListOptions) setEnginePagination(e Engine) Engine { opts.setDefaultValues() return e.Limit(opts.PageSize, (opts.Page-1)*opts.PageSize) } // GetStartEnd returns the start and end of the ListOptions -func (opts ListOptions) GetStartEnd() (start, end int) { +func (opts *ListOptions) GetStartEnd() (start, end int) { opts.setDefaultValues() start = (opts.Page - 1) * opts.PageSize end = start + opts.Page return } -func (opts ListOptions) setDefaultValues() { +func (opts *ListOptions) setDefaultValues() { if opts.PageSize <= 0 { opts.PageSize = setting.API.DefaultPagingNum } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 615d24941083..16e2f177ad09 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -285,6 +285,8 @@ var migrations = []Migration{ // v168 -> v169 NewMigration("Recreate user table to fix default values", recreateUserTableToFixDefaultValues), // v169 -> v170 + NewMigration("Update DeleteBranch comments to set the old_ref to the commit_sha", commentTypeDeleteBranchUseOldRef), + // v170 -> v171 NewMigration("Add Dismissed to Review table", addDismissedReviewColumn), } diff --git a/models/migrations/v169.go b/models/migrations/v169.go index 853a23d290db..e976281c5b10 100644 --- a/models/migrations/v169.go +++ b/models/migrations/v169.go @@ -5,18 +5,10 @@ package migrations import ( - "fmt" - "xorm.io/xorm" ) -func addDismissedReviewColumn(x *xorm.Engine) error { - type Review struct { - Dismissed bool `xorm:"NOT NULL DEFAULT false"` - } - - if err := x.Sync2(new(Review)); err != nil { - return fmt.Errorf("Sync2: %v", err) - } - return nil +func commentTypeDeleteBranchUseOldRef(x *xorm.Engine) error { + _, err := x.Exec("UPDATE comment SET old_ref = commit_sha, commit_sha = '' WHERE type = 11") + return err } diff --git a/models/migrations/v170.go b/models/migrations/v170.go new file mode 100644 index 000000000000..853a23d290db --- /dev/null +++ b/models/migrations/v170.go @@ -0,0 +1,22 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package migrations + +import ( + "fmt" + + "xorm.io/xorm" +) + +func addDismissedReviewColumn(x *xorm.Engine) error { + type Review struct { + Dismissed bool `xorm:"NOT NULL DEFAULT false"` + } + + if err := x.Sync2(new(Review)); err != nil { + return fmt.Errorf("Sync2: %v", err) + } + return nil +} diff --git a/models/user.go b/models/user.go index 8147c9f62610..495fed1ff4d0 100644 --- a/models/user.go +++ b/models/user.go @@ -1208,6 +1208,16 @@ func deleteUser(e Engine, u *User) error { // ***** END: PublicKey ***** // ***** START: GPGPublicKey ***** + keys, err := listGPGKeys(e, u.ID, ListOptions{}) + if err != nil { + return fmt.Errorf("ListGPGKeys: %v", err) + } + // Delete GPGKeyImport(s). + for _, key := range keys { + if _, err = e.Delete(&GPGKeyImport{KeyID: key.KeyID}); err != nil { + return fmt.Errorf("deleteGPGKeyImports: %v", err) + } + } if _, err = e.Delete(&GPGKey{OwnerID: u.ID}); err != nil { return fmt.Errorf("deleteGPGKeys: %v", err) } diff --git a/modules/context/repo.go b/modules/context/repo.go index 13037f4625d3..bf149b8158ac 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -554,7 +554,7 @@ func RepoAssignment() func(http.Handler) http.Handler { } ctx.Data["Tags"] = tags - brs, err := ctx.Repo.GitRepo.GetBranches() + brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 0) if err != nil { ctx.ServerError("GetBranches", err) return @@ -747,7 +747,7 @@ func RepoRefByType(refType RepoRefType) func(http.Handler) http.Handler { refName = ctx.Repo.Repository.DefaultBranch ctx.Repo.BranchName = refName if !ctx.Repo.GitRepo.IsBranchExist(refName) { - brs, err := ctx.Repo.GitRepo.GetBranches() + brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 0) if err != nil { ctx.ServerError("GetBranches", err) return diff --git a/modules/convert/git_commit.go b/modules/convert/git_commit.go index 87dfb51e7068..4e30ec2c0b33 100644 --- a/modules/convert/git_commit.go +++ b/modules/convert/git_commit.go @@ -131,6 +131,20 @@ func ToCommit(repo *models.Repository, commit *git.Commit, userCache map[string] } } + // Retrieve files affected by the commit + fileStatus, err := git.GetCommitFileStatus(repo.RepoPath(), commit.ID.String()) + if err != nil { + return nil, err + } + affectedFileList := make([]*api.CommitAffectedFiles, 0, len(fileStatus.Added)+len(fileStatus.Removed)+len(fileStatus.Modified)) + for _, files := range [][]string{fileStatus.Added, fileStatus.Removed, fileStatus.Modified} { + for _, filename := range files { + affectedFileList = append(affectedFileList, &api.CommitAffectedFiles{ + Filename: filename, + }) + } + } + return &api.Commit{ CommitMeta: &api.CommitMeta{ URL: repo.APIURL() + "/git/commits/" + commit.ID.String(), @@ -162,5 +176,6 @@ func ToCommit(repo *models.Repository, commit *git.Commit, userCache map[string] Author: apiAuthor, Committer: apiCommitter, Parents: apiParents, + Files: affectedFileList, }, nil } diff --git a/modules/git/blob_nogogit.go b/modules/git/blob_nogogit.go index 401b172860ee..e917a316195d 100644 --- a/modules/git/blob_nogogit.go +++ b/modules/git/blob_nogogit.go @@ -27,11 +27,10 @@ type Blob struct { // Calling the Close function on the result will discard all unread output. func (b *Blob) DataAsync() (io.ReadCloser, error) { stdoutReader, stdoutWriter := io.Pipe() - var err error go func() { stderr := &strings.Builder{} - err = NewCommand("cat-file", "--batch").RunInDirFullPipeline(b.repoPath, stdoutWriter, stderr, strings.NewReader(b.ID.String()+"\n")) + err := NewCommand("cat-file", "--batch").RunInDirFullPipeline(b.repoPath, stdoutWriter, stderr, strings.NewReader(b.ID.String()+"\n")) if err != nil { err = ConcatenateError(err, stderr.String()) _ = stdoutWriter.CloseWithError(err) @@ -50,8 +49,8 @@ func (b *Blob) DataAsync() (io.ReadCloser, error) { return &LimitedReaderCloser{ R: bufReader, C: stdoutReader, - N: int64(size), - }, err + N: size, + }, nil } // Size returns the uncompressed size of the blob diff --git a/modules/git/repo_branch.go b/modules/git/repo_branch.go index 25438530f553..58781eb1c71a 100644 --- a/modules/git/repo_branch.go +++ b/modules/git/repo_branch.go @@ -78,16 +78,17 @@ func (repo *Repository) GetBranch(branch string) (*Branch, error) { } // GetBranchesByPath returns a branch by it's path -func GetBranchesByPath(path string) ([]*Branch, error) { +// if limit = 0 it will not limit +func GetBranchesByPath(path string, skip, limit int) ([]*Branch, int, error) { gitRepo, err := OpenRepository(path) if err != nil { - return nil, err + return nil, 0, err } defer gitRepo.Close() - brs, err := gitRepo.GetBranches() + brs, countAll, err := gitRepo.GetBranches(skip, limit) if err != nil { - return nil, err + return nil, 0, err } branches := make([]*Branch, len(brs)) @@ -99,7 +100,7 @@ func GetBranchesByPath(path string) ([]*Branch, error) { } } - return branches, nil + return branches, countAll, nil } // DeleteBranchOptions Option(s) for delete branch diff --git a/modules/git/repo_branch_gogit.go b/modules/git/repo_branch_gogit.go index 65cb77a8b55f..b00253f6ffd6 100644 --- a/modules/git/repo_branch_gogit.go +++ b/modules/git/repo_branch_gogit.go @@ -25,21 +25,32 @@ func (repo *Repository) IsBranchExist(name string) bool { return reference.Type() != plumbing.InvalidReference } -// GetBranches returns all branches of the repository. -func (repo *Repository) GetBranches() ([]string, error) { +// GetBranches returns branches from the repository, skipping skip initial branches and +// returning at most limit branches, or all branches if limit is 0. +func (repo *Repository) GetBranches(skip, limit int) ([]string, int, error) { var branchNames []string branches, err := repo.gogitRepo.Branches() if err != nil { - return nil, err + return nil, 0, err } + i := 0 + count := 0 _ = branches.ForEach(func(branch *plumbing.Reference) error { + count++ + if i < skip { + i++ + return nil + } else if limit != 0 && count > skip+limit { + return nil + } + branchNames = append(branchNames, strings.TrimPrefix(branch.Name().String(), BranchPrefix)) return nil }) // TODO: Sort? - return branchNames, nil + return branchNames, count, nil } diff --git a/modules/git/repo_branch_nogogit.go b/modules/git/repo_branch_nogogit.go index 5ec46d725e4b..0628a572859c 100644 --- a/modules/git/repo_branch_nogogit.go +++ b/modules/git/repo_branch_nogogit.go @@ -21,14 +21,14 @@ func (repo *Repository) IsBranchExist(name string) bool { return IsReferenceExist(repo.Path, BranchPrefix+name) } -// GetBranches returns all branches of the repository. -func (repo *Repository) GetBranches() ([]string, error) { - return callShowRef(repo.Path, BranchPrefix, "--heads") +// GetBranches returns branches from the repository, skipping skip initial branches and +// returning at most limit branches, or all branches if limit is 0. +func (repo *Repository) GetBranches(skip, limit int) ([]string, int, error) { + return callShowRef(repo.Path, BranchPrefix, "--heads", skip, limit) } -func callShowRef(repoPath, prefix, arg string) ([]string, error) { - var branchNames []string - +// callShowRef return refs, if limit = 0 it will not limit +func callShowRef(repoPath, prefix, arg string, skip, limit int) (branchNames []string, countAll int, err error) { stdoutReader, stdoutWriter := io.Pipe() defer func() { _ = stdoutReader.Close() @@ -49,8 +49,21 @@ func callShowRef(repoPath, prefix, arg string) ([]string, error) { } }() + i := 0 bufReader := bufio.NewReader(stdoutReader) - for { + for i < skip { + _, isPrefix, err := bufReader.ReadLine() + if err == io.EOF { + return branchNames, i, nil + } + if err != nil { + return nil, 0, err + } + if !isPrefix { + i++ + } + } + for limit == 0 || i < skip+limit { // The output of show-ref is simply a list: // SP LF _, err := bufReader.ReadSlice(' ') @@ -59,24 +72,39 @@ func callShowRef(repoPath, prefix, arg string) ([]string, error) { _, err = bufReader.ReadSlice(' ') } if err == io.EOF { - return branchNames, nil + return branchNames, i, nil } if err != nil { - return nil, err + return nil, 0, err } branchName, err := bufReader.ReadString('\n') if err == io.EOF { // This shouldn't happen... but we'll tolerate it for the sake of peace - return branchNames, nil + return branchNames, i, nil } if err != nil { - return nil, err + return nil, i, err } branchName = strings.TrimPrefix(branchName, prefix) if len(branchName) > 0 { branchName = branchName[:len(branchName)-1] } branchNames = append(branchNames, branchName) + i++ + } + // count all refs + for limit != 0 { + _, isPrefix, err := bufReader.ReadLine() + if err == io.EOF { + return branchNames, i, nil + } + if err != nil { + return nil, 0, err + } + if !isPrefix { + i++ + } } + return branchNames, i, nil } diff --git a/modules/git/repo_branch_test.go b/modules/git/repo_branch_test.go index 33d31aef686d..05d5237e6a65 100644 --- a/modules/git/repo_branch_test.go +++ b/modules/git/repo_branch_test.go @@ -17,11 +17,26 @@ func TestRepository_GetBranches(t *testing.T) { assert.NoError(t, err) defer bareRepo1.Close() - branches, err := bareRepo1.GetBranches() + branches, countAll, err := bareRepo1.GetBranches(0, 2) + + assert.NoError(t, err) + assert.Len(t, branches, 2) + assert.EqualValues(t, 3, countAll) + assert.ElementsMatch(t, []string{"branch1", "branch2"}, branches) + + branches, countAll, err = bareRepo1.GetBranches(0, 0) assert.NoError(t, err) assert.Len(t, branches, 3) + assert.EqualValues(t, 3, countAll) assert.ElementsMatch(t, []string{"branch1", "branch2", "master"}, branches) + + branches, countAll, err = bareRepo1.GetBranches(5, 1) + + assert.NoError(t, err) + assert.Len(t, branches, 0) + assert.EqualValues(t, 3, countAll) + assert.ElementsMatch(t, []string{}, branches) } func BenchmarkRepository_GetBranches(b *testing.B) { @@ -33,7 +48,7 @@ func BenchmarkRepository_GetBranches(b *testing.B) { defer bareRepo1.Close() for i := 0; i < b.N; i++ { - _, err := bareRepo1.GetBranches() + _, _, err := bareRepo1.GetBranches(0, 0) if err != nil { b.Fatal(err) } diff --git a/modules/git/repo_tag_nogogit.go b/modules/git/repo_tag_nogogit.go index 83cbc58e342e..b3fa5d6dc407 100644 --- a/modules/git/repo_tag_nogogit.go +++ b/modules/git/repo_tag_nogogit.go @@ -13,6 +13,7 @@ func (repo *Repository) IsTagExist(name string) bool { } // GetTags returns all tags of the repository. -func (repo *Repository) GetTags() ([]string, error) { - return callShowRef(repo.Path, TagPrefix, "--tags") +func (repo *Repository) GetTags() (tags []string, err error) { + tags, _, err = callShowRef(repo.Path, TagPrefix, "--tags", 0, 0) + return } diff --git a/modules/migrations/github.go b/modules/migrations/github.go index 4d832387ba30..8d49f9308e8a 100644 --- a/modules/migrations/github.go +++ b/modules/migrations/github.go @@ -301,10 +301,15 @@ func (g *GithubDownloaderV3) convertGithubRelease(rel *github.RepositoryRelease) Created: asset.CreatedAt.Time, Updated: asset.UpdatedAt.Time, DownloadFunc: func() (io.ReadCloser, error) { + g.sleep() asset, redir, err := g.client.Repositories.DownloadReleaseAsset(g.ctx, g.repoOwner, g.repoName, *asset.ID, http.DefaultClient) if err != nil { return nil, err } + err = g.RefreshRate() + if err != nil { + log.Error("g.client.RateLimits: %s", err) + } if asset == nil { return ioutil.NopCloser(bytes.NewBufferString(redir)), nil } diff --git a/modules/process/manager.go b/modules/process/manager.go index 27ed1d4964d1..9d57f4eb7b90 100644 --- a/modules/process/manager.go +++ b/modules/process/manager.go @@ -25,6 +25,7 @@ var ( // ErrExecTimeout represent a timeout error ErrExecTimeout = errors.New("Process execution timeout") manager *Manager + managerInit sync.Once // DefaultContext is the default context to run processing commands in DefaultContext = context.Background() @@ -48,11 +49,11 @@ type Manager struct { // GetManager returns a Manager and initializes one as singleton if there's none yet func GetManager() *Manager { - if manager == nil { + managerInit.Do(func() { manager = &Manager{ processes: make(map[int64]*Process), } - } + }) return manager } diff --git a/modules/process/manager_test.go b/modules/process/manager_test.go index 42f4b0c04b5a..a515fc32cda7 100644 --- a/modules/process/manager_test.go +++ b/modules/process/manager_test.go @@ -12,6 +12,15 @@ import ( "github.com/stretchr/testify/assert" ) +func TestGetManager(t *testing.T) { + go func() { + // test race protection + _ = GetManager() + }() + pm := GetManager() + assert.NotNil(t, pm) +} + func TestManager_Add(t *testing.T) { pm := Manager{processes: make(map[int64]*Process)} diff --git a/modules/repository/branch.go b/modules/repository/branch.go index d369a200b053..275bae91e3f9 100644 --- a/modules/repository/branch.go +++ b/modules/repository/branch.go @@ -13,6 +13,9 @@ import ( // GetBranch returns a branch by its name func GetBranch(repo *models.Repository, branch string) (*git.Branch, error) { + if len(branch) == 0 { + return nil, fmt.Errorf("GetBranch: empty string for branch") + } gitRepo, err := git.OpenRepository(repo.RepoPath()) if err != nil { return nil, err @@ -22,9 +25,10 @@ func GetBranch(repo *models.Repository, branch string) (*git.Branch, error) { return gitRepo.GetBranch(branch) } -// GetBranches returns all the branches of a repository -func GetBranches(repo *models.Repository) ([]*git.Branch, error) { - return git.GetBranchesByPath(repo.RepoPath()) +// GetBranches returns branches from the repository, skipping skip initial branches and +// returning at most limit branches, or all branches if limit is 0. +func GetBranches(repo *models.Repository, skip, limit int) ([]*git.Branch, int, error) { + return git.GetBranchesByPath(repo.RepoPath(), skip, limit) } // checkBranchName validates branch name with existing repository branches @@ -35,7 +39,7 @@ func checkBranchName(repo *models.Repository, name string) error { } defer gitRepo.Close() - branches, err := GetBranches(repo) + branches, _, err := GetBranches(repo, 0, 0) if err != nil { return err } diff --git a/modules/repository/init.go b/modules/repository/init.go index a100456e778e..50cde4c0b9d1 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -239,7 +239,7 @@ func adoptRepository(ctx models.DBContext, repoPath string, u *models.User, repo repo.DefaultBranch = strings.TrimPrefix(repo.DefaultBranch, git.BranchPrefix) } - branches, _ := gitRepo.GetBranches() + branches, _, _ := gitRepo.GetBranches(0, 0) found := false hasDefault := false hasMaster := false diff --git a/modules/structs/repo_commit.go b/modules/structs/repo_commit.go index b9607b185bd6..f5c5f1b94018 100644 --- a/modules/structs/repo_commit.go +++ b/modules/structs/repo_commit.go @@ -42,11 +42,12 @@ type RepoCommit struct { // Commit contains information generated from a Git commit. type Commit struct { *CommitMeta - HTMLURL string `json:"html_url"` - RepoCommit *RepoCommit `json:"commit"` - Author *User `json:"author"` - Committer *User `json:"committer"` - Parents []*CommitMeta `json:"parents"` + HTMLURL string `json:"html_url"` + RepoCommit *RepoCommit `json:"commit"` + Author *User `json:"author"` + Committer *User `json:"committer"` + Parents []*CommitMeta `json:"parents"` + Files []*CommitAffectedFiles `json:"files"` } // CommitDateOptions store dates for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE @@ -56,3 +57,8 @@ type CommitDateOptions struct { // swagger:strfmt date-time Committer time.Time `json:"committer"` } + +// CommitAffectedFiles store information about files affected by the commit +type CommitAffectedFiles struct { + Filename string `json:"filename"` +} diff --git a/modules/translation/translation.go b/modules/translation/translation.go index 28ff6a5db151..b7276e53c014 100644 --- a/modules/translation/translation.go +++ b/modules/translation/translation.go @@ -5,6 +5,8 @@ package translation import ( + "errors" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/options" "code.gitea.io/gitea/modules/setting" @@ -57,8 +59,13 @@ func InitLocales() { matcher = language.NewMatcher(tags) for i := range setting.Names { key := "locale_" + setting.Langs[i] + ".ini" - if err := i18n.SetMessageWithDesc(setting.Langs[i], setting.Names[i], localFiles[key]); err != nil { - log.Fatal("Failed to set messages to %s: %v", setting.Langs[i], err) + if err = i18n.SetMessageWithDesc(setting.Langs[i], setting.Names[i], localFiles[key]); err != nil { + if errors.Is(err, i18n.ErrLangAlreadyExist) { + // just log if lang is already loaded since we can not reload it + log.Warn("Can not load language '%s' since already loaded", setting.Langs[i]) + } else { + log.Error("Failed to set messages to %s: %v", setting.Langs[i], err) + } } } i18n.SetDefaultLang("en-US") diff --git a/modules/util/paginate.go b/modules/util/paginate.go new file mode 100644 index 000000000000..2baa71664ed2 --- /dev/null +++ b/modules/util/paginate.go @@ -0,0 +1,34 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package util + +import "reflect" + +// PaginateSlice cut a slice as per pagination options +// if page = 0 it do not paginate +func PaginateSlice(list interface{}, page, pageSize int) interface{} { + if page <= 0 || pageSize <= 0 { + return list + } + if reflect.TypeOf(list).Kind() != reflect.Slice { + return list + } + + listValue := reflect.ValueOf(list) + + page-- + + if page*pageSize >= listValue.Len() { + return listValue.Slice(listValue.Len(), listValue.Len()).Interface() + } + + listValue = listValue.Slice(page*pageSize, listValue.Len()) + + if listValue.Len() > pageSize { + return listValue.Slice(0, pageSize).Interface() + } + + return listValue.Interface() +} diff --git a/modules/util/paginate_test.go b/modules/util/paginate_test.go new file mode 100644 index 000000000000..d962e04c1603 --- /dev/null +++ b/modules/util/paginate_test.go @@ -0,0 +1,47 @@ +// Copyright 2021 The Gitea Authors. All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. + +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPaginateSlice(t *testing.T) { + stringSlice := []string{"a", "b", "c", "d", "e"} + result, ok := PaginateSlice(stringSlice, 1, 2).([]string) + assert.True(t, ok) + assert.EqualValues(t, []string{"a", "b"}, result) + + result, ok = PaginateSlice(stringSlice, 100, 2).([]string) + assert.True(t, ok) + assert.EqualValues(t, []string{}, result) + + result, ok = PaginateSlice(stringSlice, 3, 2).([]string) + assert.True(t, ok) + assert.EqualValues(t, []string{"e"}, result) + + result, ok = PaginateSlice(stringSlice, 1, 0).([]string) + assert.True(t, ok) + assert.EqualValues(t, []string{"a", "b", "c", "d", "e"}, result) + + result, ok = PaginateSlice(stringSlice, 1, -1).([]string) + assert.True(t, ok) + assert.EqualValues(t, []string{"a", "b", "c", "d", "e"}, result) + + type Test struct { + Val int + } + + var testVar = []*Test{{Val: 2}, {Val: 3}, {Val: 4}} + testVar, ok = PaginateSlice(testVar, 1, 50).([]*Test) + assert.True(t, ok) + assert.EqualValues(t, []*Test{{Val: 2}, {Val: 3}, {Val: 4}}, testVar) + + testVar, ok = PaginateSlice(testVar, 2, 2).([]*Test) + assert.True(t, ok) + assert.EqualValues(t, []*Test{{Val: 4}}, testVar) +} diff --git a/options/license/FreeBSD-DOC b/options/license/FreeBSD-DOC new file mode 100644 index 000000000000..372c4e860963 --- /dev/null +++ b/options/license/FreeBSD-DOC @@ -0,0 +1,50 @@ +The FreeBSD Documentation License + +Copyright 1994-2021 The FreeBSD Project. All rights reserved. + +Redistribution and use in source (SGML DocBook) and 'compiled' forms (SGML, +HTML, PDF, PostScript, RTF and so forth) with or without modification, are +permitted provided that the following conditions are met: + +1. Redistributions of source code (SGML DocBook) must retain the above copyright +notice, this list of conditions and the following disclaimer as the first +lines of this file unmodified. + +2. Redistributions in compiled form (transformed to other DTDs, converted +to PDF, PostScript, RTF and other formats) must reproduce the above copyright +notice, this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +THIS DOCUMENTATION IS PROVIDED BY THE FREEBSD DOCUMENTATION PROJECT "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE FREEBSD DOCUMENTATION PROJECT BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +Manual Pages + +Some FreeBSD manual pages contain text from the IEEE Std 1003.1, 2004 Edition, +Standard for Information Technology — Portable Operating System Interface +(POSIX®) specification. These manual pages are subject to the following terms: + +The Institute of Electrical and Electronics Engineers and The Open Group, +have given us permission to reprint portions of their documentation. + +In the following statement, the phrase "this text" refers to portions of the +system documentation. + +Portions of this text are reprinted and reproduced in electronic form in the +FreeBSD manual pages, from IEEE Std 1003.1, 2004 Edition, Standard for Information +Technology — Portable Operating System Interface (POSIX), The Open Group Base +Specifications Issue 6, Copyright© 2001-2004 by the Institute of Electrical +and Electronics Engineers, Inc and The Open Group. In the event of any discrepancy +between these versions and the original IEEE and The Open Group Standard, +the original IEEE and The Open Group Standard is the referee document. The +original Standard can be obtained online at https://www.opengroup.org/membership/forums/platform/unix. + + This notice shall appear on any product containing this material. diff --git a/options/locale/locale_es-ES.ini b/options/locale/locale_es-ES.ini index a54832730f1f..95fe653e74c6 100644 --- a/options/locale/locale_es-ES.ini +++ b/options/locale/locale_es-ES.ini @@ -86,6 +86,8 @@ write=Escribir preview=Vista previa loading=Cargando… +step1=Paso 1: +step2=Paso 2: error404=La página a la que está intentando acceder o no existe o no está autorizado para verla. @@ -214,6 +216,7 @@ my_mirrors=Mis réplicas view_home=Ver %s search_repos=Buscar un repositorio… filter=Otros filtros +filter_by_team_repositories=Filtrar por repositorios de equipo show_archived=Archivado show_both_archived_unarchived=Mostrar respositorios archivados y desarchivados @@ -618,6 +621,7 @@ or_enter_secret=O introduzca el secreto: %s then_enter_passcode=E introduzca el código de acceso mostrado en la aplicación: passcode_invalid=El código de acceso es incorrecto. Vuelva a intentarlo. twofa_enrolled=Su cuenta ha sido inscrita en la autenticación de doble factor. ¡Guarde su código de respaldo (%s) en un lugar seguro, ya que sólo se muestra una vez! +twofa_failed_get_secret=No se pudo obtener el secreto. u2f_desc=Las claves de seguridad son dispositivos hardware que contienen claves criptográficas. Pueden ser usados para la autenticación de doble factor. Las claves de seguridad deben soportar el estándar FIDOU2F. u2f_require_twofa=Su cuenta debe tener activada la autenticación de doble factor para utilizar claves de seguridad. @@ -814,6 +818,8 @@ tag=Etiqueta released_this=publicó esto file_raw=Original file_history=Histórico +file_view_source=Ver código fuente +file_view_rendered=Ver procesado file_view_raw=Ver original file_permalink=Enlace permanente file_too_large=El archivo es demasiado grande para ser mostrado. @@ -910,6 +916,8 @@ ext_issues=Incidencias externas ext_issues.desc=Enlace a un gestor de incidencias externo. projects=Proyectos +projects.description=Descripción (opcional) +projects.description_placeholder=Descripción projects.create=Crear Proyecto projects.title=Título projects.new=Nuevo proyecto @@ -1121,10 +1129,13 @@ issues.lock.title=Bloquear conversación sobre esta incidencia. issues.unlock.title=Desbloquear conversación sobre esta incidencia. issues.comment_on_locked=No puede comentar una incidencia bloqueada. issues.tracker=Gestor de tiempo +issues.start_tracking_short=Iniciar temporizador issues.start_tracking=Inicio de seguimiento de tiempo issues.start_tracking_history=`ha empezado a trabajar %s` issues.tracker_auto_close=El temporizador se detendrá automáticamente cuando se cierre este problema +issues.stop_tracking=Detener temporizador issues.stop_tracking_history=`dejó de trabajar %s` +issues.cancel_tracking=Descartar issues.cancel_tracking_history=`canceló el seguimiento de tiempo %s` issues.add_time=Añadir tiempo gastado manualmente issues.add_time_short=Añadir tiempo gastado @@ -1204,6 +1215,7 @@ issues.review.resolve_conversation=Resolver conversación issues.review.un_resolve_conversation=Marcar conversación sin resolver issues.review.resolved_by=ha marcado esta conversación como resuelta issues.assignee.error=No todos los asignados fueron añadidos debido a un error inesperado. +issues.reference_issue.body=Cuerpo pulls.desc=Activar Pull Requests y revisiones de código. pulls.new=Nuevo Pull Request @@ -2030,6 +2042,7 @@ dashboard.resync_all_sshprincipals.desc=(No es necesario para el servidor SSH in dashboard.resync_all_hooks=Resincronizar los hooks de pre-recepción, actualización y post-recepción de todos los repositorios. dashboard.reinit_missing_repos=Reiniciar todos los repositorios Git faltantes de los que existen registros dashboard.sync_external_users=Sincronizar datos de usuario externo +dashboard.cleanup_hook_task_table=Limpiar tabla hook_task dashboard.server_uptime=Tiempo de actividad del servidor dashboard.current_goroutine=Gorutinas actuales dashboard.current_memory_usage=Uso de memoria actual @@ -2180,6 +2193,7 @@ auths.enable_tls=Habilitar cifrado TLS auths.skip_tls_verify=Omitir la verificación TLS auths.pam_service_name=Nombre del Servicio PAM auths.oauth2_provider=Proveedor OAuth2 +auths.oauth2_icon_url=URL de icono auths.oauth2_clientID=ID de cliente (clave) auths.oauth2_clientSecret=Secreto del cliente auths.openIdConnectAutoDiscoveryURL=URL de descubrimiento automático de OpenID Connect diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index f2f66cb39d30..85c4e4d5bfa0 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -754,6 +754,7 @@ func Routes() *web.Route { }, reqToken(), reqAdmin()) m.Group("/tags", func() { m.Get("", repo.ListTags) + m.Delete("/{tag}", repo.DeleteTag) }, reqRepoReader(models.UnitTypeCode), context.ReferencesGitRepo(true)) m.Group("/keys", func() { m.Combo("").Get(repo.ListDeployKeys). @@ -862,8 +863,8 @@ func Routes() *web.Route { }) m.Group("/tags", func() { m.Combo("/{tag}"). - Get(repo.GetReleaseTag). - Delete(reqToken(), reqRepoWriter(models.UnitTypeReleases), repo.DeleteReleaseTag) + Get(repo.GetReleaseByTag). + Delete(reqToken(), reqRepoWriter(models.UnitTypeReleases), repo.DeleteReleaseByTag) }) }, reqRepoReader(models.UnitTypeReleases)) m.Post("/mirror-sync", reqToken(), reqRepoWriter(models.UnitTypeCode), repo.MirrorSync) diff --git a/routers/api/v1/org/org.go b/routers/api/v1/org/org.go index 61e09e1126a0..e0f36aa1e657 100644 --- a/routers/api/v1/org/org.go +++ b/routers/api/v1/org/org.go @@ -13,6 +13,7 @@ import ( "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/routers/api/v1/user" "code.gitea.io/gitea/routers/api/v1/utils" @@ -28,9 +29,9 @@ func listUserOrgs(ctx *context.APIContext, u *models.User) { ctx.Error(http.StatusInternalServerError, "GetOrgsByUserID", err) return } - maxResults := len(orgs) - orgs = utils.PaginateUserSlice(orgs, listOptions.Page, listOptions.PageSize) + maxResults := len(orgs) + orgs, _ = util.PaginateSlice(orgs, listOptions.Page, listOptions.PageSize).([]*models.User) apiOrgs := make([]*api.Organization, len(orgs)) for i := range orgs { diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index 790464c8bc55..451fdcf516f0 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -17,6 +17,7 @@ import ( repo_module "code.gitea.io/gitea/modules/repository" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" + "code.gitea.io/gitea/routers/api/v1/utils" pull_service "code.gitea.io/gitea/services/pull" repo_service "code.gitea.io/gitea/services/repository" ) @@ -284,11 +285,21 @@ func ListBranches(ctx *context.APIContext) { // description: name of the repo // type: string // required: true + // - name: page + // in: query + // description: page number of results to return (1-based) + // type: integer + // - name: limit + // in: query + // description: page size of results + // type: integer // responses: // "200": // "$ref": "#/responses/BranchList" - branches, err := repo_module.GetBranches(ctx.Repo.Repository) + listOptions := utils.GetListOptions(ctx) + skip, _ := listOptions.GetStartEnd() + branches, totalNumOfBranches, err := repo_module.GetBranches(ctx.Repo.Repository, skip, listOptions.PageSize) if err != nil { ctx.Error(http.StatusInternalServerError, "GetBranches", err) return @@ -313,6 +324,9 @@ func ListBranches(ctx *context.APIContext) { } } + ctx.SetLinkHeader(int(totalNumOfBranches), listOptions.PageSize) + ctx.Header().Set("X-Total-Count", fmt.Sprintf("%d", totalNumOfBranches)) + ctx.Header().Set("Access-Control-Expose-Headers", "X-Total-Count, Link") ctx.JSON(http.StatusOK, &apiBranches) } diff --git a/routers/api/v1/repo/file.go b/routers/api/v1/repo/file.go index 2bd57f1460fe..37e02874b4ff 100644 --- a/routers/api/v1/repo/file.go +++ b/routers/api/v1/repo/file.go @@ -43,6 +43,11 @@ func GetRawFile(ctx *context.APIContext) { // 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: success @@ -54,7 +59,22 @@ func GetRawFile(ctx *context.APIContext) { return } - blob, err := ctx.Repo.Commit.GetBlobByPath(ctx.Repo.TreePath) + commit := ctx.Repo.Commit + + if ref := ctx.QueryTrim("ref"); len(ref) > 0 { + var err error + commit, err = ctx.Repo.GitRepo.GetCommit(ref) + if err != nil { + if git.IsErrNotExist(err) { + ctx.NotFound() + } else { + ctx.Error(http.StatusInternalServerError, "GetBlobByPath", err) + } + return + } + } + + blob, err := commit.GetBlobByPath(ctx.Repo.TreePath) if err != nil { if git.IsErrNotExist(err) { ctx.NotFound() diff --git a/routers/api/v1/repo/release_tags.go b/routers/api/v1/repo/release_tags.go index ef07ce5e1ad5..4b853d44bb95 100644 --- a/routers/api/v1/repo/release_tags.go +++ b/routers/api/v1/repo/release_tags.go @@ -5,7 +5,6 @@ package repo import ( - "errors" "net/http" "code.gitea.io/gitea/models" @@ -14,9 +13,9 @@ import ( releaseservice "code.gitea.io/gitea/services/release" ) -// GetReleaseTag get a single release of a repository by its tagname -func GetReleaseTag(ctx *context.APIContext) { - // swagger:operation GET /repos/{owner}/{repo}/releases/tags/{tag} repository repoGetReleaseTag +// GetReleaseByTag get a single release of a repository by tag name +func GetReleaseByTag(ctx *context.APIContext) { + // swagger:operation GET /repos/{owner}/{repo}/releases/tags/{tag} repository repoGetReleaseByTag // --- // summary: Get a release by tag name // produces: @@ -34,7 +33,7 @@ func GetReleaseTag(ctx *context.APIContext) { // required: true // - name: tag // in: path - // description: tagname of the release to get + // description: tag name of the release to get // type: string // required: true // responses: @@ -48,25 +47,30 @@ func GetReleaseTag(ctx *context.APIContext) { release, err := models.GetRelease(ctx.Repo.Repository.ID, tag) if err != nil { if models.IsErrReleaseNotExist(err) { - ctx.Error(http.StatusNotFound, "GetRelease", err) + ctx.NotFound() return } ctx.Error(http.StatusInternalServerError, "GetRelease", err) return } - if err := release.LoadAttributes(); err != nil { + if release.IsTag { + ctx.NotFound() + return + } + + if err = release.LoadAttributes(); err != nil { ctx.Error(http.StatusInternalServerError, "LoadAttributes", err) return } ctx.JSON(http.StatusOK, convert.ToRelease(release)) } -// DeleteReleaseTag delete a tag from a repository -func DeleteReleaseTag(ctx *context.APIContext) { - // swagger:operation DELETE /repos/{owner}/{repo}/releases/tags/{tag} repository repoDeleteReleaseTag +// DeleteReleaseByTag delete a release from a repository by tag name +func DeleteReleaseByTag(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/releases/tags/{tag} repository repoDeleteReleaseByTag // --- - // summary: Delete a release tag + // summary: Delete a release by tag name // parameters: // - name: owner // in: path @@ -80,7 +84,7 @@ func DeleteReleaseTag(ctx *context.APIContext) { // required: true // - name: tag // in: path - // description: name of the tag to delete + // description: tag name of the release to delete // type: string // required: true // responses: @@ -88,27 +92,25 @@ func DeleteReleaseTag(ctx *context.APIContext) { // "$ref": "#/responses/empty" // "404": // "$ref": "#/responses/notFound" - // "409": - // "$ref": "#/responses/conflict" tag := ctx.Params(":tag") release, err := models.GetRelease(ctx.Repo.Repository.ID, tag) if err != nil { if models.IsErrReleaseNotExist(err) { - ctx.Error(http.StatusNotFound, "GetRelease", err) + ctx.NotFound() return } ctx.Error(http.StatusInternalServerError, "GetRelease", err) return } - if !release.IsTag { - ctx.Error(http.StatusConflict, "IsTag", errors.New("a tag attached to a release cannot be deleted directly")) + if release.IsTag { + ctx.NotFound() return } - if err := releaseservice.DeleteReleaseByID(release.ID, ctx.User, true); err != nil { + if err = releaseservice.DeleteReleaseByID(release.ID, ctx.User, false); err != nil { ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err) } diff --git a/routers/api/v1/repo/tag.go b/routers/api/v1/repo/tag.go index 76c612bea420..ec9b541bd41d 100644 --- a/routers/api/v1/repo/tag.go +++ b/routers/api/v1/repo/tag.go @@ -5,12 +5,15 @@ package repo import ( + "errors" "net/http" + "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/convert" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/routers/api/v1/utils" + releaseservice "code.gitea.io/gitea/services/release" ) // ListTags list all the tags of a repository @@ -104,3 +107,56 @@ func GetTag(ctx *context.APIContext) { ctx.JSON(http.StatusOK, convert.ToAnnotatedTag(ctx.Repo.Repository, tag, commit)) } } + +// DeleteTag delete a specific tag of in a repository by name +func DeleteTag(ctx *context.APIContext) { + // swagger:operation DELETE /repos/{owner}/{repo}/tags/{tag} repository repoDeleteTag + // --- + // summary: Delete a repository's tag by name + // 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: tag + // in: path + // description: name of tag to delete + // type: string + // required: true + // responses: + // "204": + // "$ref": "#/responses/empty" + // "404": + // "$ref": "#/responses/notFound" + // "409": + // "$ref": "#/responses/conflict" + + tag, err := models.GetRelease(ctx.Repo.Repository.ID, ctx.Params("tag")) + if err != nil { + if models.IsErrReleaseNotExist(err) { + ctx.NotFound() + return + } + ctx.Error(http.StatusInternalServerError, "GetRelease", err) + return + } + + if !tag.IsTag { + ctx.Error(http.StatusConflict, "IsTag", errors.New("a tag attached to a release cannot be deleted directly")) + return + } + + if err = releaseservice.DeleteReleaseByID(tag.ID, ctx.User, true); err != nil { + ctx.Error(http.StatusInternalServerError, "DeleteReleaseByID", err) + } + + ctx.Status(http.StatusNoContent) +} diff --git a/routers/api/v1/utils/utils.go b/routers/api/v1/utils/utils.go index 5732ea7f7d50..ad1a136db463 100644 --- a/routers/api/v1/utils/utils.go +++ b/routers/api/v1/utils/utils.go @@ -66,22 +66,3 @@ func GetListOptions(ctx *context.APIContext) models.ListOptions { PageSize: convert.ToCorrectPageSize(ctx.QueryInt("limit")), } } - -// PaginateUserSlice cut a slice of Users as per pagination options -// TODO: make it generic -func PaginateUserSlice(items []*models.User, page, pageSize int) []*models.User { - if page != 0 { - page-- - } - - if page*pageSize >= len(items) { - return items[len(items):] - } - - items = items[page*pageSize:] - - if len(items) > pageSize { - return items[:pageSize] - } - return items -} diff --git a/routers/repo/attachment.go b/routers/repo/attachment.go index 5b699abc8d11..5df9cdbf12a6 100644 --- a/routers/repo/attachment.go +++ b/routers/repo/attachment.go @@ -152,7 +152,7 @@ func GetAttachment(ctx *context.Context) { return } - if err = ServeData(ctx, attach.Name, fr); err != nil { + if err = ServeData(ctx, attach.Name, attach.Size, fr); err != nil { ctx.ServerError("ServeData", err) return } diff --git a/routers/repo/branch.go b/routers/repo/branch.go index 7d844abe5a02..cf6abc08df52 100644 --- a/routers/repo/branch.go +++ b/routers/repo/branch.go @@ -58,12 +58,14 @@ func Branches(ctx *context.Context) { page = 1 } - pageSize := ctx.QueryInt("limit") - if pageSize <= 0 || pageSize > git.BranchesRangeSize { - pageSize = git.BranchesRangeSize + limit := ctx.QueryInt("limit") + if limit <= 0 || limit > git.BranchesRangeSize { + limit = git.BranchesRangeSize } - branches, branchesCount := loadBranches(ctx, page, pageSize) + skip := (page - 1) * limit + log.Debug("Branches: skip: %d limit: %d", skip, limit) + branches, branchesCount := loadBranches(ctx, skip, limit) if ctx.Written() { return } @@ -80,6 +82,7 @@ func DeleteBranchPost(ctx *context.Context) { defer redirect(ctx) branchName := ctx.Query("name") if branchName == ctx.Repo.Repository.DefaultBranch { + log.Debug("DeleteBranch: Can't delete default branch '%s'", branchName) ctx.Flash.Error(ctx.Tr("repo.branch.default_deletion_failed", branchName)) return } @@ -92,16 +95,19 @@ func DeleteBranchPost(ctx *context.Context) { } if isProtected { + log.Debug("DeleteBranch: Can't delete protected branch '%s'", branchName) ctx.Flash.Error(ctx.Tr("repo.branch.protected_deletion_failed", branchName)) return } - if !ctx.Repo.GitRepo.IsBranchExist(branchName) || branchName == ctx.Repo.Repository.DefaultBranch { + if !ctx.Repo.GitRepo.IsBranchExist(branchName) { + log.Debug("DeleteBranch: Can't delete non existing branch '%s'", branchName) ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName)) return } if err := deleteBranch(ctx, branchName); err != nil { + log.Error("DeleteBranch: %v", err) ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", branchName)) return } @@ -129,10 +135,11 @@ func RestoreBranchPost(ctx *context.Context) { Env: models.PushingEnvironment(ctx.User, ctx.Repo.Repository), }); err != nil { if strings.Contains(err.Error(), "already exists") { + log.Debug("RestoreBranch: Can't restore branch '%s', since one with same name already exist", deletedBranch.Name) ctx.Flash.Error(ctx.Tr("repo.branch.already_exists", deletedBranch.Name)) return } - log.Error("CreateBranch: %v", err) + log.Error("RestoreBranch: CreateBranch: %v", err) ctx.Flash.Error(ctx.Tr("repo.branch.restore_failed", deletedBranch.Name)) return } @@ -148,7 +155,7 @@ func RestoreBranchPost(ctx *context.Context) { RepoUserName: ctx.Repo.Owner.Name, RepoName: ctx.Repo.Repository.Name, }); err != nil { - log.Error("Update: %v", err) + log.Error("RestoreBranch: Update: %v", err) } ctx.Flash.Success(ctx.Tr("repo.branch.restore_success", deletedBranch.Name)) @@ -196,16 +203,18 @@ func deleteBranch(ctx *context.Context, branchName string) error { } // loadBranches loads branches from the repository limited by page & pageSize. -// NOTE: May write to context on error. page & pageSize must be > 0 -func loadBranches(ctx *context.Context, page, pageSize int) ([]*Branch, int) { +// NOTE: May write to context on error. +func loadBranches(ctx *context.Context, skip, limit int) ([]*Branch, int) { defaultBranch, err := repo_module.GetBranch(ctx.Repo.Repository, ctx.Repo.Repository.DefaultBranch) if err != nil { + log.Error("loadBranches: get default branch: %v", err) ctx.ServerError("GetDefaultBranch", err) return nil, 0 } - rawBranches, err := repo_module.GetBranches(ctx.Repo.Repository) + rawBranches, totalNumOfBranches, err := repo_module.GetBranches(ctx.Repo.Repository, skip, limit) if err != nil { + log.Error("GetBranches: %v", err) ctx.ServerError("GetBranches", err) return nil, 0 } @@ -222,32 +231,23 @@ func loadBranches(ctx *context.Context, page, pageSize int) ([]*Branch, int) { repoIDToGitRepo := map[int64]*git.Repository{} repoIDToGitRepo[ctx.Repo.Repository.ID] = ctx.Repo.GitRepo - var totalNumOfBranches = len(rawBranches) - var startIndex = (page - 1) * pageSize - if startIndex > totalNumOfBranches { - startIndex = totalNumOfBranches - 1 - } - var endIndex = startIndex + pageSize - if endIndex > totalNumOfBranches { - endIndex = totalNumOfBranches - 1 - } - var branches []*Branch - for i := startIndex; i < endIndex; i++ { + for i := range rawBranches { + if rawBranches[i].Name == defaultBranch.Name { + // Skip default branch + continue + } + var branch = loadOneBranch(ctx, rawBranches[i], protectedBranches, repoIDToRepo, repoIDToGitRepo) if branch == nil { return nil, 0 } - if branch.Name == ctx.Repo.Repository.DefaultBranch { - // Skip default branch - continue - } - branches = append(branches, branch) } // Always add the default branch + log.Debug("loadOneBranch: load default: '%s'", defaultBranch.Name) branches = append(branches, loadOneBranch(ctx, defaultBranch, protectedBranches, repoIDToRepo, repoIDToGitRepo)) if ctx.Repo.CanWrite(models.UnitTypeCode) { @@ -259,12 +259,13 @@ func loadBranches(ctx *context.Context, page, pageSize int) ([]*Branch, int) { branches = append(branches, deletedBranches...) } - return branches, len(rawBranches) - 1 + return branches, totalNumOfBranches - 1 } func loadOneBranch(ctx *context.Context, rawBranch *git.Branch, protectedBranches []*models.ProtectedBranch, repoIDToRepo map[int64]*models.Repository, repoIDToGitRepo map[int64]*git.Repository) *Branch { + log.Trace("loadOneBranch: '%s'", rawBranch.Name) commit, err := rawBranch.GetCommit() if err != nil { diff --git a/routers/repo/compare.go b/routers/repo/compare.go index aa4b3191b2e6..218f71246953 100644 --- a/routers/repo/compare.go +++ b/routers/repo/compare.go @@ -520,7 +520,7 @@ func getBranchesForRepo(user *models.User, repo *models.Repository) (bool, []str } defer gitRepo.Close() - branches, err := gitRepo.GetBranches() + branches, _, err := gitRepo.GetBranches(0, 0) if err != nil { return false, nil, err } @@ -541,7 +541,7 @@ func CompareDiff(ctx *context.Context) { } if ctx.Data["PageIsComparePull"] == true { - headBranches, err := headGitRepo.GetBranches() + headBranches, _, err := headGitRepo.GetBranches(0, 0) if err != nil { ctx.ServerError("GetBranches", err) return diff --git a/routers/repo/download.go b/routers/repo/download.go index f04dac6aa514..50f893690b1a 100644 --- a/routers/repo/download.go +++ b/routers/repo/download.go @@ -20,7 +20,7 @@ import ( ) // ServeData download file from io.Reader -func ServeData(ctx *context.Context, name string, reader io.Reader) error { +func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) error { buf := make([]byte, 1024) n, err := reader.Read(buf) if err != nil && err != io.EOF { @@ -31,6 +31,11 @@ func ServeData(ctx *context.Context, name string, reader io.Reader) error { } ctx.Resp.Header().Set("Cache-Control", "public,max-age=86400") + if size >= 0 { + ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size)) + } else { + log.Error("ServeData called to serve data: %s with size < 0: %d", name, size) + } name = path.Base(name) // Google Chrome dislike commas in filenames, so let's change it to a space @@ -76,7 +81,7 @@ func ServeBlob(ctx *context.Context, blob *git.Blob) error { } }() - return ServeData(ctx, ctx.Repo.TreePath, dataRc) + return ServeData(ctx, ctx.Repo.TreePath, blob.Size(), dataRc) } // ServeBlobOrLFS download a git.Blob redirecting to LFS if necessary @@ -105,7 +110,7 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob) error { log.Error("ServeBlobOrLFS: Close: %v", err) } }() - return ServeData(ctx, ctx.Repo.TreePath, lfsDataRc) + return ServeData(ctx, ctx.Repo.TreePath, meta.Size, lfsDataRc) } return ServeBlob(ctx, blob) diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 2bc55b3d8903..5510924ee0a5 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -678,7 +678,7 @@ func RetrieveRepoMetas(ctx *context.Context, repo *models.Repository, isPull boo return nil } - brs, err := ctx.Repo.GitRepo.GetBranches() + brs, _, err := ctx.Repo.GitRepo.GetBranches(0, 0) if err != nil { ctx.ServerError("GetBranches", err) return nil diff --git a/routers/repo/view.go b/routers/repo/view.go index 25d40c34d8f2..e50e4613b7a9 100644 --- a/routers/repo/view.go +++ b/routers/repo/view.go @@ -385,7 +385,6 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st fileSize := blob.Size() ctx.Data["FileIsSymlink"] = entry.IsLink() - ctx.Data["FileSize"] = fileSize ctx.Data["FileName"] = blob.Name() ctx.Data["RawFileLink"] = rawLink + "/" + ctx.Repo.TreePath @@ -395,21 +394,8 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st isTextFile := base.IsTextFile(buf) isLFSFile := false - ctx.Data["IsTextFile"] = isTextFile - isDisplayingSource := ctx.Query("display") == "source" isDisplayingRendered := !isDisplayingSource - isRepresentableAsText := base.IsRepresentableAsText(buf) - ctx.Data["IsRepresentableAsText"] = isRepresentableAsText - if !isRepresentableAsText { - // If we can't show plain text, always try to render. - isDisplayingSource = false - isDisplayingRendered = true - } - ctx.Data["IsDisplayingSource"] = isDisplayingSource - ctx.Data["IsDisplayingRendered"] = isDisplayingRendered - - ctx.Data["IsTextSource"] = isTextFile || isDisplayingSource //Check for LFS meta file if isTextFile && setting.LFS.StartServer { @@ -422,7 +408,6 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st } } if meta != nil { - ctx.Data["IsLFSFile"] = true isLFSFile = true // OK read the lfs object @@ -445,14 +430,25 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st buf = buf[:n] isTextFile = base.IsTextFile(buf) - ctx.Data["IsTextFile"] = isTextFile - fileSize = meta.Size - ctx.Data["FileSize"] = meta.Size - filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(blob.Name())) - ctx.Data["RawFileLink"] = fmt.Sprintf("%s%s.git/info/lfs/objects/%s/%s", setting.AppURL, ctx.Repo.Repository.FullName(), meta.Oid, filenameBase64) + ctx.Data["RawFileLink"] = fmt.Sprintf("%s/media/%s/%s", ctx.Repo.RepoLink, ctx.Repo.BranchNameSubURL(), ctx.Repo.TreePath) } } + + isRepresentableAsText := base.IsRepresentableAsText(buf) + if !isRepresentableAsText { + // If we can't show plain text, always try to render. + isDisplayingSource = false + isDisplayingRendered = true + } + ctx.Data["IsLFSFile"] = isLFSFile + ctx.Data["FileSize"] = fileSize + ctx.Data["IsTextFile"] = isTextFile + ctx.Data["IsRepresentableAsText"] = isRepresentableAsText + ctx.Data["IsDisplayingSource"] = isDisplayingSource + ctx.Data["IsDisplayingRendered"] = isDisplayingRendered + ctx.Data["IsTextSource"] = isTextFile || isDisplayingSource + // Check LFS Lock lfsLock, err := ctx.Repo.Repository.GetTreePathLock(ctx.Repo.TreePath) ctx.Data["LFSLock"] = lfsLock @@ -542,7 +538,6 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st ctx.Data["MarkupType"] = markupType ctx.Data["FileContent"] = string(markup.Render(blob.Name(), buf, path.Dir(treeLink), ctx.Repo.Repository.ComposeDocumentMetas())) } - } if ctx.Repo.CanEnableEditor() { @@ -713,7 +708,10 @@ func RenderUserCards(ctx *context.Context, total int, getter func(opts models.Li pager := context.NewPagination(total, models.ItemsPerPage, page, 5) ctx.Data["Page"] = pager - items, err := getter(models.ListOptions{Page: pager.Paginater.Current()}) + items, err := getter(models.ListOptions{ + Page: pager.Paginater.Current(), + PageSize: models.ItemsPerPage, + }) if err != nil { ctx.ServerError("getter", err) return @@ -744,6 +742,7 @@ func Stars(ctx *context.Context) { func Forks(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repos.forks") + // TODO: need pagination forks, err := ctx.Repo.Repository.GetForks(models.ListOptions{}) if err != nil { ctx.ServerError("GetForks", err) diff --git a/routers/user/auth_openid.go b/routers/user/auth_openid.go index 3f5c9f7248ce..e79085e94d40 100644 --- a/routers/user/auth_openid.go +++ b/routers/user/auth_openid.go @@ -415,7 +415,7 @@ func RegisterOpenIDPost(ctx *context.Context) { Name: form.UserName, Email: form.Email, Passwd: password, - IsActive: !setting.Service.RegisterEmailConfirm, + IsActive: !(setting.Service.RegisterEmailConfirm || setting.Service.RegisterManualConfirm), } //nolint: dupl if err := models.CreateUser(u); err != nil { diff --git a/services/mirror/mirror.go b/services/mirror/mirror.go index 328e23ad2fc7..e4981b8c00e6 100644 --- a/services/mirror/mirror.go +++ b/services/mirror/mirror.go @@ -301,7 +301,7 @@ func runSync(m *models.Mirror) ([]*mirrorSyncResult, bool) { } log.Trace("SyncMirrors [repo: %-v]: invalidating mirror branch caches...", m.Repo) - branches, err := repo_module.GetBranches(m.Repo) + branches, _, err := repo_module.GetBranches(m.Repo, 0, 0) if err != nil { log.Error("GetBranches: %v", err) return nil, false diff --git a/services/pull/pull.go b/services/pull/pull.go index 92f1ff65fb2f..4f742f5a1a6a 100644 --- a/services/pull/pull.go +++ b/services/pull/pull.go @@ -482,7 +482,7 @@ func CloseBranchPulls(doer *models.User, repoID int64, branch string) error { // CloseRepoBranchesPulls close all pull requests which head branches are in the given repository func CloseRepoBranchesPulls(doer *models.User, repo *models.Repository) error { - branches, err := git.GetBranchesByPath(repo.RepoPath()) + branches, _, err := git.GetBranchesByPath(repo.RepoPath(), 0, 0) if err != nil { return err } diff --git a/templates/repo/issue/view_content/comments.tmpl b/templates/repo/issue/view_content/comments.tmpl index 741441dd2a16..b971c6b1ae69 100644 --- a/templates/repo/issue/view_content/comments.tmpl +++ b/templates/repo/issue/view_content/comments.tmpl @@ -252,7 +252,7 @@ {{.Poster.GetDisplayName}} - {{$.i18n.Tr "repo.issues.delete_branch_at" (.CommitSHA|Escape) $createdStr | Safe}} + {{$.i18n.Tr "repo.issues.delete_branch_at" (.OldRef|Escape) $createdStr | Safe}} {{else if eq .Type 12}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index c21a5de06724..94493749afa4 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -2500,6 +2500,18 @@ "name": "repo", "in": "path", "required": true + }, + { + "type": "integer", + "description": "page number of results to return (1-based)", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "page size of results", + "name": "limit", + "in": "query" } ], "responses": { @@ -7951,6 +7963,12 @@ "name": "filepath", "in": "path", "required": true + }, + { + "type": "string", + "description": "The name of the commit/branch/tag. Default the repository’s default branch (usually master)", + "name": "ref", + "in": "query" } ], "responses": { @@ -8070,7 +8088,7 @@ "repository" ], "summary": "Get a release by tag name", - "operationId": "repoGetReleaseTag", + "operationId": "repoGetReleaseByTag", "parameters": [ { "type": "string", @@ -8088,7 +8106,7 @@ }, { "type": "string", - "description": "tagname of the release to get", + "description": "tag name of the release to get", "name": "tag", "in": "path", "required": true @@ -8107,8 +8125,8 @@ "tags": [ "repository" ], - "summary": "Delete a release tag", - "operationId": "repoDeleteReleaseTag", + "summary": "Delete a release by tag name", + "operationId": "repoDeleteReleaseByTag", "parameters": [ { "type": "string", @@ -8126,7 +8144,7 @@ }, { "type": "string", - "description": "name of the tag to delete", + "description": "tag name of the release to delete", "name": "tag", "in": "path", "required": true @@ -8138,9 +8156,6 @@ }, "404": { "$ref": "#/responses/notFound" - }, - "409": { - "$ref": "#/responses/conflict" } } } @@ -8921,6 +8936,52 @@ } } }, + "/repos/{owner}/{repo}/tags/{tag}": { + "delete": { + "produces": [ + "application/json" + ], + "tags": [ + "repository" + ], + "summary": "Delete a repository's tag by name", + "operationId": "repoDeleteTag", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of tag to delete", + "name": "tag", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "$ref": "#/responses/empty" + }, + "404": { + "$ref": "#/responses/notFound" + }, + "409": { + "$ref": "#/responses/conflict" + } + } + } + }, "/repos/{owner}/{repo}/teams": { "get": { "produces": [ @@ -11927,6 +11988,13 @@ "format": "date-time", "x-go-name": "Created" }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/CommitAffectedFiles" + }, + "x-go-name": "Files" + }, "html_url": { "type": "string", "x-go-name": "HTMLURL" @@ -11949,6 +12017,17 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "CommitAffectedFiles": { + "description": "CommitAffectedFiles store information about files affected by the commit", + "type": "object", + "properties": { + "filename": { + "type": "string", + "x-go-name": "Filename" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "CommitDateOptions": { "description": "CommitDateOptions store dates for GIT_AUTHOR_DATE and GIT_COMMITTER_DATE", "type": "object",