mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-11 17:17:53 +02:00
Merge remote-tracking branch 'upstream/main' into limit-repo-size
This commit is contained in:
commit
82dcd0c3cf
15
cmd/admin.go
15
cmd/admin.go
@ -180,6 +180,11 @@ var (
|
||||
Name: "raw",
|
||||
Usage: "Display only the token value",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "scopes",
|
||||
Value: "",
|
||||
Usage: "Comma separated list of scopes to apply to access token",
|
||||
},
|
||||
},
|
||||
Action: runGenerateAccessToken,
|
||||
}
|
||||
@ -698,9 +703,15 @@ func runGenerateAccessToken(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
accessTokenScope, err := auth_model.AccessTokenScope(c.String("scopes")).Normalize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := &auth_model.AccessToken{
|
||||
Name: c.String("token-name"),
|
||||
UID: user.ID,
|
||||
Name: c.String("token-name"),
|
||||
UID: user.ID,
|
||||
Scope: accessTokenScope,
|
||||
}
|
||||
|
||||
if err := auth_model.NewAccessToken(t); err != nil {
|
||||
|
||||
@ -2458,6 +2458,8 @@ ROUTER = console
|
||||
;LIMIT_SIZE_COMPOSER = -1
|
||||
;; Maximum size of a Conan upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_CONAN = -1
|
||||
;; Maximum size of a Conda upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_CONDA = -1
|
||||
;; Maximum size of a Container upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
;LIMIT_SIZE_CONTAINER = -1
|
||||
;; Maximum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
|
||||
@ -1214,6 +1214,7 @@ Task queue configuration has been moved to `queue.task`. However, the below conf
|
||||
- `LIMIT_TOTAL_OWNER_SIZE`: **-1**: Maximum size of packages a single owner can use (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_COMPOSER`: **-1**: Maximum size of a Composer upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_CONAN`: **-1**: Maximum size of a Conan upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_CONDA`: **-1**: Maximum size of a Conda upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_CONTAINER`: **-1**: Maximum size of a Container upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_GENERIC`: **-1**: Maximum size of a Generic upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
- `LIMIT_SIZE_HELM`: **-1**: Maximum size of a Helm upload (`-1` means no limits, format `1000`, `1 MB`, `1 GiB`)
|
||||
|
||||
@ -329,3 +329,22 @@ Before activating SSPI single sign-on authentication (SSO) you have to prepare y
|
||||
- You have added the URL of the web app to the `Local intranet zone`
|
||||
- The clocks of the server and client should not differ with more than 5 minutes (depends on group policy)
|
||||
- `Integrated Windows Authentication` should be enabled in Internet Explorer (under `Advanced settings`)
|
||||
|
||||
## Reverse Proxy
|
||||
|
||||
Gitea supports Reverse Proxy Header authentication, it will read headers as a trusted login user name or user email address. This hasn't been enabled by default, you can enable it with
|
||||
|
||||
```ini
|
||||
[service]
|
||||
ENABLE_REVERSE_PROXY_AUTHENTICATION = true
|
||||
```
|
||||
|
||||
The default login user name is in the `X-WEBAUTH-USER` header, you can change it via changing `REVERSE_PROXY_AUTHENTICATION_USER` in app.ini. If the user doesn't exist, you can enable automatic registration with `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION=true`.
|
||||
|
||||
The default login user email is `X-WEBAUTH-EMAIL`, you can change it via changing `REVERSE_PROXY_AUTHENTICATION_EMAIL` in app.ini, this could also be disabled with `ENABLE_REVERSE_PROXY_EMAIL`
|
||||
|
||||
If set `ENABLE_REVERSE_PROXY_FULL_NAME=true`, a user full name expected in `X-WEBAUTH-FULLNAME` will be assigned to the user when auto creating the user. You can also change the header name with `REVERSE_PROXY_AUTHENTICATION_FULL_NAME`.
|
||||
|
||||
You can also limit the reverse proxy's IP address range with `REVERSE_PROXY_TRUSTED_PROXIES` which default value is `127.0.0.0/8,::1/128`. By `REVERSE_PROXY_LIMIT`, you can limit trusted proxies level.
|
||||
|
||||
Notice: Reverse Proxy Auth doesn't support the API. You still need an access token or basic auth to make API requests.
|
||||
|
||||
@ -15,4 +15,21 @@ menu:
|
||||
|
||||
# 认证
|
||||
|
||||
## TBD
|
||||
## 反向代理认证
|
||||
|
||||
Gitea 支持通过读取反向代理传递的 HTTP 头中的登录名或者 email 地址来支持反向代理来认证。默认是不启用的,你可以用以下配置启用。
|
||||
|
||||
```ini
|
||||
[service]
|
||||
ENABLE_REVERSE_PROXY_AUTHENTICATION = true
|
||||
```
|
||||
|
||||
默认的登录用户名的 HTTP 头是 `X-WEBAUTH-USER`,你可以通过修改 `REVERSE_PROXY_AUTHENTICATION_USER` 来变更它。如果用户不存在,可以自动创建用户,当然你需要修改 `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION=true` 来启用它。
|
||||
|
||||
默认的登录用户 Email 的 HTTP 头是 `X-WEBAUTH-EMAIL`,你可以通过修改 `REVERSE_PROXY_AUTHENTICATION_EMAIL` 来变更它。如果用户不存在,可以自动创建用户,当然你需要修改 `ENABLE_REVERSE_PROXY_AUTO_REGISTRATION=true` 来启用它。你也可以通过修改 `ENABLE_REVERSE_PROXY_EMAIL` 来启用或停用这个 HTTP 头。
|
||||
|
||||
如果设置了 `ENABLE_REVERSE_PROXY_FULL_NAME=true`,则用户的全名会从 `X-WEBAUTH-FULLNAME` 读取,这样在自动创建用户时将使用这个字段作为用户全名,你也可以通过修改 `REVERSE_PROXY_AUTHENTICATION_FULL_NAME` 来变更 HTTP 头。
|
||||
|
||||
你也可以通过修改 `REVERSE_PROXY_TRUSTED_PROXIES` 来设置反向代理的IP地址范围,加强安全性,默认值是 `127.0.0.0/8,::1/128`。 通过 `REVERSE_PROXY_LIMIT`, 可以设置最多信任几级反向代理。
|
||||
|
||||
注意:反向代理认证不支持认证 API,API 仍旧需要用 access token 来进行认证。
|
||||
|
||||
85
docs/content/doc/packages/conda.en-us.md
Normal file
85
docs/content/doc/packages/conda.en-us.md
Normal file
@ -0,0 +1,85 @@
|
||||
---
|
||||
date: "2022-12-28T00:00:00+00:00"
|
||||
title: "Conda Packages Repository"
|
||||
slug: "packages/conda"
|
||||
draft: false
|
||||
toc: false
|
||||
menu:
|
||||
sidebar:
|
||||
parent: "packages"
|
||||
name: "Conda"
|
||||
weight: 25
|
||||
identifier: "conda"
|
||||
---
|
||||
|
||||
# Conda Packages Repository
|
||||
|
||||
Publish [Conda](https://docs.conda.io/en/latest/) packages for your user or organization.
|
||||
|
||||
**Table of Contents**
|
||||
|
||||
{{< toc >}}
|
||||
|
||||
## Requirements
|
||||
|
||||
To work with the Conda package registry, you need to use [conda](https://docs.conda.io/projects/conda/en/stable/user-guide/install/index.html).
|
||||
|
||||
## Configuring the package registry
|
||||
|
||||
To register the package registry and provide credentials, edit your `.condarc` file:
|
||||
|
||||
```yaml
|
||||
channel_alias: https://gitea.example.com/api/packages/{owner}/conda
|
||||
channels:
|
||||
- https://gitea.example.com/api/packages/{owner}/conda
|
||||
default_channels:
|
||||
- https://gitea.example.com/api/packages/{owner}/conda
|
||||
```
|
||||
|
||||
| Placeholder | Description |
|
||||
| ------------ | ----------- |
|
||||
| `owner` | The owner of the package. |
|
||||
|
||||
See the [official documentation](https://conda.io/projects/conda/en/latest/user-guide/configuration/use-condarc.html) for explanations of the individual settings.
|
||||
|
||||
If you need to provide credentials, you may embed them as part of the channel url (`https://user:password@gitea.example.com/...`).
|
||||
|
||||
## Publish a package
|
||||
|
||||
To publish a package, perform a HTTP PUT operation with the package content in the request body.
|
||||
|
||||
```
|
||||
PUT https://gitea.example.com/api/packages/{owner}/conda/{channel}/{filename}
|
||||
```
|
||||
|
||||
| Placeholder | Description |
|
||||
| ------------ | ----------- |
|
||||
| `owner` | The owner of the package. |
|
||||
| `channel` | The [channel](https://conda.io/projects/conda/en/latest/user-guide/concepts/channels.html) of the package. (optional) |
|
||||
| `filename` | The name of the file. |
|
||||
|
||||
Example request using HTTP Basic authentication:
|
||||
|
||||
```shell
|
||||
curl --user your_username:your_password_or_token \
|
||||
--upload-file path/to/package-1.0.conda \
|
||||
https://gitea.example.com/api/packages/testuser/conda/package-1.0.conda
|
||||
```
|
||||
|
||||
You cannot publish a package if a package of the same name and version already exists. You must delete the existing package first.
|
||||
|
||||
## Install a package
|
||||
|
||||
To install a package from the package registry, execute one of the following commands:
|
||||
|
||||
```shell
|
||||
conda install {package_name}
|
||||
conda install {package_name}={package_version}
|
||||
conda install -c {channel} {package_name}
|
||||
```
|
||||
|
||||
| Parameter | Description |
|
||||
| ----------------- | ----------- |
|
||||
| `package_name` | The package name. |
|
||||
| `package_version` | The package version. |
|
||||
| `channel` | The channel of the package. (optional) |
|
||||
@ -28,6 +28,7 @@ The following package managers are currently supported:
|
||||
| ---- | -------- | -------------- |
|
||||
| [Composer]({{< relref "doc/packages/composer.en-us.md" >}}) | PHP | `composer` |
|
||||
| [Conan]({{< relref "doc/packages/conan.en-us.md" >}}) | C++ | `conan` |
|
||||
| [Conda]({{< relref "doc/packages/conda.en-us.md" >}}) | - | `conda` |
|
||||
| [Container]({{< relref "doc/packages/container.en-us.md" >}}) | - | any OCI compliant client |
|
||||
| [Generic]({{< relref "doc/packages/generic.en-us.md" >}}) | - | any HTTP client |
|
||||
| [Helm]({{< relref "doc/packages/helm.en-us.md" >}}) | - | any HTTP client, `cm-push` |
|
||||
|
||||
2
go.mod
2
go.mod
@ -26,6 +26,7 @@ require (
|
||||
github.com/dimiro1/reply v0.0.0-20200315094148-d0136a4c9e21
|
||||
github.com/djherbis/buffer v1.2.0
|
||||
github.com/djherbis/nio/v3 v3.0.1
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/editorconfig/editorconfig-core-go/v2 v2.5.1
|
||||
github.com/emersion/go-imap v1.2.1
|
||||
@ -161,7 +162,6 @@ require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.3 // indirect
|
||||
|
||||
@ -24,6 +24,9 @@ func (err ErrKeyUnableVerify) Error() string {
|
||||
return fmt.Sprintf("Unable to verify key content [result: %s]", err.Result)
|
||||
}
|
||||
|
||||
// ErrKeyIsPrivate is returned when the provided key is a private key not a public key
|
||||
var ErrKeyIsPrivate = util.NewSilentWrapErrorf(util.ErrInvalidArgument, "the provided key is a private key")
|
||||
|
||||
// ErrKeyNotExist represents a "KeyNotExist" kind of error.
|
||||
type ErrKeyNotExist struct {
|
||||
ID int64
|
||||
|
||||
@ -96,6 +96,9 @@ func parseKeyString(content string) (string, error) {
|
||||
if block == nil {
|
||||
return "", fmt.Errorf("failed to parse PEM block containing the public key")
|
||||
}
|
||||
if strings.Contains(block.Type, "PRIVATE") {
|
||||
return "", ErrKeyIsPrivate
|
||||
}
|
||||
|
||||
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
|
||||
@ -10,6 +10,35 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
)
|
||||
|
||||
/*
|
||||
The reasons behind the DBFS (database-filesystem) package:
|
||||
When a Gitea action is running, the Gitea action server should collect and store all the logs.
|
||||
|
||||
The requirements are:
|
||||
* The running logs must be stored across the cluster if the Gitea servers are deployed as a cluster.
|
||||
* The logs will be archived to Object Storage (S3/MinIO, etc.) after a period of time.
|
||||
* The Gitea action UI should be able to render the running logs and the archived logs.
|
||||
|
||||
Some possible solutions for the running logs:
|
||||
* [Not ideal] Using local temp file: it can not be shared across the cluster.
|
||||
* [Not ideal] Using shared file in the filesystem of git repository: although at the moment, the Gitea cluster's
|
||||
git repositories must be stored in a shared filesystem, in the future, Gitea may need a dedicated Git Service Server
|
||||
to decouple the shared filesystem. Then the action logs will become a blocker.
|
||||
* [Not ideal] Record the logs in a database table line by line: it has a couple of problems:
|
||||
- It's difficult to make multiple increasing sequence (log line number) for different databases.
|
||||
- The database table will have a lot of rows and be affected by the big-table performance problem.
|
||||
- It's difficult to load logs by using the same interface as other storages.
|
||||
- It's difficult to calculate the size of the logs.
|
||||
|
||||
The DBFS solution:
|
||||
* It can be used in a cluster.
|
||||
* It can share the same interface (Read/Write/Seek) as other storages.
|
||||
* It's very friendly to database because it only needs to store much fewer rows than the log-line solution.
|
||||
* In the future, when Gitea action needs to limit the log size (other CI/CD services also do so), it's easier to calculate the log file size.
|
||||
* Even sometimes the UI needs to render the tailing lines, the tailing lines can be found be counting the "\n" from the end of the file by seek.
|
||||
The seeking and finding is not the fastest way, but it's still acceptable and won't affect the performance too much.
|
||||
*/
|
||||
|
||||
type dbfsMeta struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
FullPath string `xorm:"VARCHAR(500) UNIQUE NOT NULL"`
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
owner_name: user2
|
||||
lower_name: repo1
|
||||
name: repo1
|
||||
default_branch: master
|
||||
num_watches: 4
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -34,6 +35,7 @@
|
||||
owner_name: user2
|
||||
lower_name: repo2
|
||||
name: repo2
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 1
|
||||
num_forks: 0
|
||||
@ -64,6 +66,7 @@
|
||||
owner_name: user3
|
||||
lower_name: repo3
|
||||
name: repo3
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -94,6 +97,7 @@
|
||||
owner_name: user5
|
||||
lower_name: repo4
|
||||
name: repo4
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 1
|
||||
num_forks: 0
|
||||
@ -274,6 +278,7 @@
|
||||
owner_name: user12
|
||||
lower_name: repo10
|
||||
name: repo10
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 1
|
||||
@ -304,6 +309,7 @@
|
||||
owner_name: user13
|
||||
lower_name: repo11
|
||||
name: repo11
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -425,6 +431,7 @@
|
||||
owner_name: user2
|
||||
lower_name: repo15
|
||||
name: repo15
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -455,6 +462,7 @@
|
||||
owner_name: user2
|
||||
lower_name: repo16
|
||||
name: repo16
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -905,6 +913,7 @@
|
||||
owner_name: user2
|
||||
lower_name: repo20
|
||||
name: repo20
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -965,6 +974,7 @@
|
||||
owner_name: user2
|
||||
lower_name: utf8
|
||||
name: utf8
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -1055,6 +1065,7 @@
|
||||
owner_name: user2
|
||||
lower_name: commits_search_test
|
||||
name: commits_search_test
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -1085,6 +1096,7 @@
|
||||
owner_name: user2
|
||||
lower_name: git_hooks_test
|
||||
name: git_hooks_test
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -1115,6 +1127,7 @@
|
||||
owner_name: limited_org
|
||||
lower_name: public_repo_on_limited_org
|
||||
name: public_repo_on_limited_org
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -1145,6 +1158,7 @@
|
||||
owner_name: limited_org
|
||||
lower_name: private_repo_on_limited_org
|
||||
name: private_repo_on_limited_org
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -1175,6 +1189,7 @@
|
||||
owner_name: privated_org
|
||||
lower_name: public_repo_on_private_org
|
||||
name: public_repo_on_private_org
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -1205,6 +1220,7 @@
|
||||
owner_name: privated_org
|
||||
lower_name: private_repo_on_private_org
|
||||
name: private_repo_on_private_org
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -1235,6 +1251,7 @@
|
||||
owner_name: user2
|
||||
lower_name: glob
|
||||
name: glob
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -1295,6 +1312,7 @@
|
||||
owner_name: user27
|
||||
lower_name: template1
|
||||
name: template1
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -1355,6 +1373,7 @@
|
||||
owner_name: org26
|
||||
lower_name: repo_external_tracker
|
||||
name: repo_external_tracker
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -1385,6 +1404,7 @@
|
||||
owner_name: org26
|
||||
lower_name: repo_external_tracker_numeric
|
||||
name: repo_external_tracker_numeric
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -1415,6 +1435,7 @@
|
||||
owner_name: org26
|
||||
lower_name: repo_external_tracker_alpha
|
||||
name: repo_external_tracker_alpha
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -1445,6 +1466,7 @@
|
||||
owner_name: user27
|
||||
lower_name: repo49
|
||||
name: repo49
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -1475,6 +1497,7 @@
|
||||
owner_name: user30
|
||||
lower_name: repo50
|
||||
name: repo50
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -1505,6 +1528,7 @@
|
||||
owner_name: user30
|
||||
lower_name: repo51
|
||||
name: repo51
|
||||
default_branch: master
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
@ -1565,6 +1589,7 @@
|
||||
owner_name: user30
|
||||
lower_name: renderer
|
||||
name: renderer
|
||||
default_branch: master
|
||||
is_archived: false
|
||||
is_empty: false
|
||||
is_private: false
|
||||
@ -1592,6 +1617,7 @@
|
||||
owner_name: user2
|
||||
lower_name: lfs
|
||||
name: lfs
|
||||
default_branch: master
|
||||
is_empty: false
|
||||
is_archived: false
|
||||
is_private: true
|
||||
|
||||
@ -173,8 +173,9 @@ func (prs PullRequestList) loadAttributes(ctx context.Context) error {
|
||||
for i := range issues {
|
||||
set[issues[i].ID] = issues[i]
|
||||
}
|
||||
for i := range prs {
|
||||
prs[i].Issue = set[prs[i].IssueID]
|
||||
for _, pr := range prs {
|
||||
pr.Issue = set[pr.IssueID]
|
||||
pr.Issue.PullRequest = pr // panic here means issueIDs and prs are not in sync
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
63
models/packages/conda/search.go
Normal file
63
models/packages/conda/search.go
Normal file
@ -0,0 +1,63 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package conda
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/packages"
|
||||
conda_module "code.gitea.io/gitea/modules/packages/conda"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type FileSearchOptions struct {
|
||||
OwnerID int64
|
||||
Channel string
|
||||
Subdir string
|
||||
Filename string
|
||||
}
|
||||
|
||||
// SearchFiles gets all files matching the search options
|
||||
func SearchFiles(ctx context.Context, opts *FileSearchOptions) ([]*packages.PackageFile, error) {
|
||||
var cond builder.Cond = builder.Eq{
|
||||
"package.type": packages.TypeConda,
|
||||
"package.owner_id": opts.OwnerID,
|
||||
"package_version.is_internal": false,
|
||||
}
|
||||
|
||||
if opts.Filename != "" {
|
||||
cond = cond.And(builder.Eq{
|
||||
"package_file.lower_name": strings.ToLower(opts.Filename),
|
||||
})
|
||||
}
|
||||
|
||||
var versionPropsCond builder.Cond = builder.Eq{
|
||||
"package_property.ref_type": packages.PropertyTypePackage,
|
||||
"package_property.name": conda_module.PropertyChannel,
|
||||
"package_property.value": opts.Channel,
|
||||
}
|
||||
|
||||
cond = cond.And(builder.In("package.id", builder.Select("package_property.ref_id").Where(versionPropsCond).From("package_property")))
|
||||
|
||||
var filePropsCond builder.Cond = builder.Eq{
|
||||
"package_property.ref_type": packages.PropertyTypeFile,
|
||||
"package_property.name": conda_module.PropertySubdir,
|
||||
"package_property.value": opts.Subdir,
|
||||
}
|
||||
|
||||
cond = cond.And(builder.In("package_file.id", builder.Select("package_property.ref_id").Where(filePropsCond).From("package_property")))
|
||||
|
||||
sess := db.GetEngine(ctx).
|
||||
Select("package_file.*").
|
||||
Table("package_file").
|
||||
Join("INNER", "package_version", "package_version.id = package_file.version_id").
|
||||
Join("INNER", "package", "package.id = package_version.package_id").
|
||||
Where(cond)
|
||||
|
||||
pfs := make([]*packages.PackageFile, 0, 10)
|
||||
return pfs, sess.Find(&pfs)
|
||||
}
|
||||
@ -13,6 +13,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/packages/composer"
|
||||
"code.gitea.io/gitea/modules/packages/conan"
|
||||
"code.gitea.io/gitea/modules/packages/conda"
|
||||
"code.gitea.io/gitea/modules/packages/container"
|
||||
"code.gitea.io/gitea/modules/packages/helm"
|
||||
"code.gitea.io/gitea/modules/packages/maven"
|
||||
@ -132,6 +133,8 @@ func GetPackageDescriptor(ctx context.Context, pv *PackageVersion) (*PackageDesc
|
||||
metadata = &composer.Metadata{}
|
||||
case TypeConan:
|
||||
metadata = &conan.Metadata{}
|
||||
case TypeConda:
|
||||
metadata = &conda.VersionMetadata{}
|
||||
case TypeContainer:
|
||||
metadata = &container.Metadata{}
|
||||
case TypeGeneric:
|
||||
|
||||
@ -32,6 +32,7 @@ type Type string
|
||||
const (
|
||||
TypeComposer Type = "composer"
|
||||
TypeConan Type = "conan"
|
||||
TypeConda Type = "conda"
|
||||
TypeContainer Type = "container"
|
||||
TypeGeneric Type = "generic"
|
||||
TypeHelm Type = "helm"
|
||||
@ -47,6 +48,7 @@ const (
|
||||
var TypeList = []Type{
|
||||
TypeComposer,
|
||||
TypeConan,
|
||||
TypeConda,
|
||||
TypeContainer,
|
||||
TypeGeneric,
|
||||
TypeHelm,
|
||||
@ -66,6 +68,8 @@ func (pt Type) Name() string {
|
||||
return "Composer"
|
||||
case TypeConan:
|
||||
return "Conan"
|
||||
case TypeConda:
|
||||
return "Conda"
|
||||
case TypeContainer:
|
||||
return "Container"
|
||||
case TypeGeneric:
|
||||
@ -97,6 +101,8 @@ func (pt Type) SVGName() string {
|
||||
return "gitea-composer"
|
||||
case TypeConan:
|
||||
return "gitea-conan"
|
||||
case TypeConda:
|
||||
return "gitea-conda"
|
||||
case TypeContainer:
|
||||
return "octicon-container"
|
||||
case TypeGeneric:
|
||||
|
||||
@ -228,11 +228,6 @@ func (repo *Repository) IsBroken() bool {
|
||||
|
||||
// AfterLoad is invoked from XORM after setting the values of all fields of this object.
|
||||
func (repo *Repository) AfterLoad() {
|
||||
// FIXME: use models migration to solve all at once.
|
||||
if len(repo.DefaultBranch) == 0 {
|
||||
repo.DefaultBranch = setting.Repository.DefaultBranch
|
||||
}
|
||||
|
||||
repo.NumOpenIssues = repo.NumIssues - repo.NumClosedIssues
|
||||
repo.NumOpenPulls = repo.NumPulls - repo.NumClosedPulls
|
||||
repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
|
||||
|
||||
@ -75,7 +75,6 @@ func DetectWorkflows(commit *git.Commit, triggedEvent webhook_module.HookEventTy
|
||||
if evt.Name != triggedEvent.Event() {
|
||||
continue
|
||||
}
|
||||
|
||||
if detectMatched(commit, triggedEvent, payload, evt) {
|
||||
workflows[entry.Name()] = content
|
||||
}
|
||||
@ -105,8 +104,9 @@ func detectMatched(commit *git.Commit, triggedEvent webhook_module.HookEventType
|
||||
for cond, vals := range evt.Acts {
|
||||
switch cond {
|
||||
case "branches", "tags":
|
||||
refShortName := git.RefName(pushPayload.Ref).ShortName()
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(pushPayload.Ref) {
|
||||
if glob.MustCompile(val, '/').Match(refShortName) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
@ -160,8 +160,9 @@ func detectMatched(commit *git.Commit, triggedEvent webhook_module.HookEventType
|
||||
}
|
||||
}
|
||||
case "branches":
|
||||
refShortName := git.RefName(prPayload.PullRequest.Base.Ref).ShortName()
|
||||
for _, val := range vals {
|
||||
if glob.MustCompile(val, '/').Match(prPayload.PullRequest.Base.Ref) {
|
||||
if glob.MustCompile(val, '/').Match(refShortName) {
|
||||
matchTimes++
|
||||
break
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ func EscapeControlReader(reader io.Reader, writer io.Writer, locale translation.
|
||||
return streamer.escaped, err
|
||||
}
|
||||
|
||||
// EscapeControlStringReader escapes the unicode control sequences in a provided reader of string content and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte
|
||||
// EscapeControlStringReader escapes the unicode control sequences in a provided reader of string content and writer in a locale and returns the findings as an EscapeStatus and the escaped []byte. HTML line breaks are not inserted after every newline by this method.
|
||||
func EscapeControlStringReader(reader io.Reader, writer io.Writer, locale translation.Locale, allowed ...rune) (escaped *EscapeStatus, err error) {
|
||||
bufRd := bufio.NewReader(reader)
|
||||
outputStream := &HTMLStreamerWriter{Writer: writer}
|
||||
@ -65,10 +65,6 @@ func EscapeControlStringReader(reader io.Reader, writer io.Writer, locale transl
|
||||
}
|
||||
break
|
||||
}
|
||||
if err := streamer.SelfClosingTag("br"); err != nil {
|
||||
streamer.escaped.HasError = true
|
||||
return streamer.escaped, err
|
||||
}
|
||||
}
|
||||
return streamer.escaped, err
|
||||
}
|
||||
|
||||
@ -19,14 +19,16 @@ import (
|
||||
func AddCacheControlToHeader(h http.Header, maxAge time.Duration, additionalDirectives ...string) {
|
||||
directives := make([]string, 0, 2+len(additionalDirectives))
|
||||
|
||||
// "max-age=0 + must-revalidate" (aka "no-cache") is preferred instead of "no-store"
|
||||
// because browsers may restore some input fields after navigate-back / reload a page.
|
||||
if setting.IsProd {
|
||||
if maxAge == 0 {
|
||||
directives = append(directives, "no-store")
|
||||
directives = append(directives, "max-age=0", "private", "must-revalidate")
|
||||
} else {
|
||||
directives = append(directives, "private", "max-age="+strconv.Itoa(int(maxAge.Seconds())))
|
||||
}
|
||||
} else {
|
||||
directives = append(directives, "no-store")
|
||||
directives = append(directives, "max-age=0", "private", "must-revalidate")
|
||||
|
||||
// to remind users they are using non-prod setting.
|
||||
h.Add("X-Gitea-Debug", "RUN_MODE="+setting.RunMode)
|
||||
|
||||
243
modules/packages/conda/metadata.go
Normal file
243
modules/packages/conda/metadata.go
Normal file
@ -0,0 +1,243 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package conda
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"compress/bzip2"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidStructure = util.SilentWrap{Message: "package structure is invalid", Err: util.ErrInvalidArgument}
|
||||
ErrInvalidName = util.SilentWrap{Message: "package name is invalid", Err: util.ErrInvalidArgument}
|
||||
ErrInvalidVersion = util.SilentWrap{Message: "package version is invalid", Err: util.ErrInvalidArgument}
|
||||
)
|
||||
|
||||
const (
|
||||
PropertyName = "conda.name"
|
||||
PropertyChannel = "conda.channel"
|
||||
PropertySubdir = "conda.subdir"
|
||||
PropertyMetadata = "conda.metdata"
|
||||
)
|
||||
|
||||
// Package represents a Conda package
|
||||
type Package struct {
|
||||
Name string
|
||||
Version string
|
||||
Subdir string
|
||||
VersionMetadata *VersionMetadata
|
||||
FileMetadata *FileMetadata
|
||||
}
|
||||
|
||||
// VersionMetadata represents the metadata of a Conda package
|
||||
type VersionMetadata struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
ProjectURL string `json:"project_url,omitempty"`
|
||||
RepositoryURL string `json:"repository_url,omitempty"`
|
||||
DocumentationURL string `json:"documentation_url,omitempty"`
|
||||
License string `json:"license,omitempty"`
|
||||
LicenseFamily string `json:"license_family,omitempty"`
|
||||
}
|
||||
|
||||
// FileMetadata represents the metadata of a Conda package file
|
||||
type FileMetadata struct {
|
||||
IsCondaPackage bool `json:"is_conda"`
|
||||
Architecture string `json:"architecture,omitempty"`
|
||||
NoArch string `json:"noarch,omitempty"`
|
||||
Build string `json:"build,omitempty"`
|
||||
BuildNumber int64 `json:"build_number,omitempty"`
|
||||
Dependencies []string `json:"dependencies,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
Timestamp int64 `json:"timestamp,omitempty"`
|
||||
}
|
||||
|
||||
type index struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Architecture string `json:"arch"`
|
||||
NoArch string `json:"noarch"`
|
||||
Build string `json:"build"`
|
||||
BuildNumber int64 `json:"build_number"`
|
||||
Dependencies []string `json:"depends"`
|
||||
License string `json:"license"`
|
||||
LicenseFamily string `json:"license_family"`
|
||||
Platform string `json:"platform"`
|
||||
Subdir string `json:"subdir"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type about struct {
|
||||
Description string `json:"description"`
|
||||
Summary string `json:"summary"`
|
||||
ProjectURL string `json:"home"`
|
||||
RepositoryURL string `json:"dev_url"`
|
||||
DocumentationURL string `json:"doc_url"`
|
||||
}
|
||||
|
||||
type ReaderAndReaderAt interface {
|
||||
io.Reader
|
||||
io.ReaderAt
|
||||
}
|
||||
|
||||
// ParsePackageBZ2 parses the Conda package file compressed with bzip2
|
||||
func ParsePackageBZ2(r io.Reader) (*Package, error) {
|
||||
gzr := bzip2.NewReader(r)
|
||||
|
||||
return parsePackageTar(gzr)
|
||||
}
|
||||
|
||||
// ParsePackageConda parses the Conda package file compressed with zip and zstd
|
||||
func ParsePackageConda(r io.ReaderAt, size int64) (*Package, error) {
|
||||
zr, err := zip.NewReader(r, size)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range zr.File {
|
||||
if strings.HasPrefix(file.Name, "info-") && strings.HasSuffix(file.Name, ".tar.zst") {
|
||||
f, err := zr.Open(file.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
dec, err := zstd.NewReader(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dec.Close()
|
||||
|
||||
p, err := parsePackageTar(dec)
|
||||
if p != nil {
|
||||
p.FileMetadata.IsCondaPackage = true
|
||||
}
|
||||
return p, err
|
||||
}
|
||||
}
|
||||
|
||||
return nil, ErrInvalidStructure
|
||||
}
|
||||
|
||||
func parsePackageTar(r io.Reader) (*Package, error) {
|
||||
var i *index
|
||||
var a *about
|
||||
|
||||
tr := tar.NewReader(r)
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hdr.Typeflag != tar.TypeReg {
|
||||
continue
|
||||
}
|
||||
|
||||
if hdr.Name == "info/index.json" {
|
||||
if err := json.NewDecoder(tr).Decode(&i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !checkName(i.Name) {
|
||||
return nil, ErrInvalidName
|
||||
}
|
||||
|
||||
if !checkVersion(i.Version) {
|
||||
return nil, ErrInvalidVersion
|
||||
}
|
||||
|
||||
if a != nil {
|
||||
break // stop loop if both files were found
|
||||
}
|
||||
} else if hdr.Name == "info/about.json" {
|
||||
if err := json.NewDecoder(tr).Decode(&a); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !validation.IsValidURL(a.ProjectURL) {
|
||||
a.ProjectURL = ""
|
||||
}
|
||||
if !validation.IsValidURL(a.RepositoryURL) {
|
||||
a.RepositoryURL = ""
|
||||
}
|
||||
if !validation.IsValidURL(a.DocumentationURL) {
|
||||
a.DocumentationURL = ""
|
||||
}
|
||||
|
||||
if i != nil {
|
||||
break // stop loop if both files were found
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if i == nil {
|
||||
return nil, ErrInvalidStructure
|
||||
}
|
||||
if a == nil {
|
||||
a = &about{}
|
||||
}
|
||||
|
||||
return &Package{
|
||||
Name: i.Name,
|
||||
Version: i.Version,
|
||||
Subdir: i.Subdir,
|
||||
VersionMetadata: &VersionMetadata{
|
||||
License: i.License,
|
||||
LicenseFamily: i.LicenseFamily,
|
||||
Description: a.Description,
|
||||
Summary: a.Summary,
|
||||
ProjectURL: a.ProjectURL,
|
||||
RepositoryURL: a.RepositoryURL,
|
||||
DocumentationURL: a.DocumentationURL,
|
||||
},
|
||||
FileMetadata: &FileMetadata{
|
||||
Architecture: i.Architecture,
|
||||
NoArch: i.NoArch,
|
||||
Build: i.Build,
|
||||
BuildNumber: i.BuildNumber,
|
||||
Dependencies: i.Dependencies,
|
||||
Platform: i.Platform,
|
||||
Timestamp: i.Timestamp,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1393
|
||||
func checkName(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
if name != strings.ToLower(name) {
|
||||
return false
|
||||
}
|
||||
return !checkBadCharacters(name, "!")
|
||||
}
|
||||
|
||||
// https://github.com/conda/conda-build/blob/db9a728a9e4e6cfc895637ca3221117970fc2663/conda_build/metadata.py#L1403
|
||||
func checkVersion(version string) bool {
|
||||
if version == "" {
|
||||
return false
|
||||
}
|
||||
return !checkBadCharacters(version, "-")
|
||||
}
|
||||
|
||||
func checkBadCharacters(s, additional string) bool {
|
||||
if strings.ContainsAny(s, "=@#$%^&*:;\"'\\|<>?/ ") {
|
||||
return true
|
||||
}
|
||||
return strings.ContainsAny(s, additional)
|
||||
}
|
||||
150
modules/packages/conda/metadata_test.go
Normal file
150
modules/packages/conda/metadata_test.go
Normal file
@ -0,0 +1,150 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package conda
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/dsnet/compress/bzip2"
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
packageName = "gitea"
|
||||
packageVersion = "1.0.1"
|
||||
description = "Package Description"
|
||||
projectURL = "https://gitea.io"
|
||||
repositoryURL = "https://gitea.io/gitea/gitea"
|
||||
documentationURL = "https://docs.gitea.io"
|
||||
)
|
||||
|
||||
func TestParsePackage(t *testing.T) {
|
||||
createArchive := func(files map[string][]byte) *bytes.Buffer {
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
for filename, content := range files {
|
||||
hdr := &tar.Header{
|
||||
Name: filename,
|
||||
Mode: 0o600,
|
||||
Size: int64(len(content)),
|
||||
}
|
||||
tw.WriteHeader(hdr)
|
||||
tw.Write(content)
|
||||
}
|
||||
tw.Close()
|
||||
return &buf
|
||||
}
|
||||
|
||||
t.Run("MissingIndexFile", func(t *testing.T) {
|
||||
buf := createArchive(map[string][]byte{"dummy.txt": {}})
|
||||
|
||||
p, err := parsePackageTar(buf)
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrInvalidStructure)
|
||||
})
|
||||
|
||||
t.Run("MissingAboutFile", func(t *testing.T) {
|
||||
buf := createArchive(map[string][]byte{"info/index.json": []byte(`{"name":"name","version":"1.0"}`)})
|
||||
|
||||
p, err := parsePackageTar(buf)
|
||||
assert.NotNil(t, p)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "name", p.Name)
|
||||
assert.Equal(t, "1.0", p.Version)
|
||||
assert.Empty(t, p.VersionMetadata.ProjectURL)
|
||||
})
|
||||
|
||||
t.Run("InvalidName", func(t *testing.T) {
|
||||
for _, name := range []string{"", "name!", "nAMe"} {
|
||||
buf := createArchive(map[string][]byte{"info/index.json": []byte(`{"name":"` + name + `","version":"1.0"}`)})
|
||||
|
||||
p, err := parsePackageTar(buf)
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrInvalidName)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidVersion", func(t *testing.T) {
|
||||
for _, version := range []string{"", "1.0-2"} {
|
||||
buf := createArchive(map[string][]byte{"info/index.json": []byte(`{"name":"name","version":"` + version + `"}`)})
|
||||
|
||||
p, err := parsePackageTar(buf)
|
||||
assert.Nil(t, p)
|
||||
assert.ErrorIs(t, err, ErrInvalidVersion)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Valid", func(t *testing.T) {
|
||||
buf := createArchive(map[string][]byte{
|
||||
"info/index.json": []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `","subdir":"linux-64"}`),
|
||||
"info/about.json": []byte(`{"description":"` + description + `","dev_url":"` + repositoryURL + `","doc_url":"` + documentationURL + `","home":"` + projectURL + `"}`),
|
||||
})
|
||||
|
||||
p, err := parsePackageTar(buf)
|
||||
assert.NotNil(t, p)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, packageName, p.Name)
|
||||
assert.Equal(t, packageVersion, p.Version)
|
||||
assert.Equal(t, "linux-64", p.Subdir)
|
||||
assert.Equal(t, description, p.VersionMetadata.Description)
|
||||
assert.Equal(t, projectURL, p.VersionMetadata.ProjectURL)
|
||||
assert.Equal(t, repositoryURL, p.VersionMetadata.RepositoryURL)
|
||||
assert.Equal(t, documentationURL, p.VersionMetadata.DocumentationURL)
|
||||
})
|
||||
|
||||
t.Run(".tar.bz2", func(t *testing.T) {
|
||||
tarArchive := createArchive(map[string][]byte{
|
||||
"info/index.json": []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `"}`),
|
||||
})
|
||||
|
||||
var buf bytes.Buffer
|
||||
bw, _ := bzip2.NewWriter(&buf, nil)
|
||||
io.Copy(bw, tarArchive)
|
||||
bw.Close()
|
||||
|
||||
br := bytes.NewReader(buf.Bytes())
|
||||
|
||||
p, err := ParsePackageBZ2(br)
|
||||
assert.NotNil(t, p)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, packageName, p.Name)
|
||||
assert.Equal(t, packageVersion, p.Version)
|
||||
assert.False(t, p.FileMetadata.IsCondaPackage)
|
||||
})
|
||||
|
||||
t.Run(".conda", func(t *testing.T) {
|
||||
tarArchive := createArchive(map[string][]byte{
|
||||
"info/index.json": []byte(`{"name":"` + packageName + `","version":"` + packageVersion + `"}`),
|
||||
})
|
||||
|
||||
var infoBuf bytes.Buffer
|
||||
zsw, _ := zstd.NewWriter(&infoBuf)
|
||||
io.Copy(zsw, tarArchive)
|
||||
zsw.Close()
|
||||
|
||||
var buf bytes.Buffer
|
||||
zpw := zip.NewWriter(&buf)
|
||||
w, _ := zpw.Create("info-x.tar.zst")
|
||||
w.Write(infoBuf.Bytes())
|
||||
zpw.Close()
|
||||
|
||||
br := bytes.NewReader(buf.Bytes())
|
||||
|
||||
p, err := ParsePackageConda(br, int64(br.Len()))
|
||||
assert.NotNil(t, p)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, packageName, p.Name)
|
||||
assert.Equal(t, packageVersion, p.Version)
|
||||
assert.True(t, p.FileMetadata.IsCondaPackage)
|
||||
})
|
||||
}
|
||||
@ -27,6 +27,7 @@ var (
|
||||
LimitTotalOwnerSize int64
|
||||
LimitSizeComposer int64
|
||||
LimitSizeConan int64
|
||||
LimitSizeConda int64
|
||||
LimitSizeContainer int64
|
||||
LimitSizeGeneric int64
|
||||
LimitSizeHelm int64
|
||||
@ -66,6 +67,7 @@ func newPackages() {
|
||||
Packages.LimitTotalOwnerSize = mustBytes(sec, "LIMIT_TOTAL_OWNER_SIZE")
|
||||
Packages.LimitSizeComposer = mustBytes(sec, "LIMIT_SIZE_COMPOSER")
|
||||
Packages.LimitSizeConan = mustBytes(sec, "LIMIT_SIZE_CONAN")
|
||||
Packages.LimitSizeConda = mustBytes(sec, "LIMIT_SIZE_CONDA")
|
||||
Packages.LimitSizeContainer = mustBytes(sec, "LIMIT_SIZE_CONTAINER")
|
||||
Packages.LimitSizeGeneric = mustBytes(sec, "LIMIT_SIZE_GENERIC")
|
||||
Packages.LimitSizeHelm = mustBytes(sec, "LIMIT_SIZE_HELM")
|
||||
|
||||
@ -518,6 +518,7 @@ organization_leave_success = You have successfully left the organization %s.
|
||||
invalid_ssh_key = Cannot verify your SSH key: %s
|
||||
invalid_gpg_key = Cannot verify your GPG key: %s
|
||||
invalid_ssh_principal = Invalid principal: %s
|
||||
must_use_public_key = The key you provided is a private key. Please do not upload your private key anywhere. Use your public key instead.
|
||||
unable_verify_ssh_key = "Cannot verify the SSH key; double-check it for mistakes."
|
||||
auth_failed = Authentication failed: %v
|
||||
|
||||
@ -3162,6 +3163,11 @@ conan.details.repository = Repository
|
||||
conan.registry = Setup this registry from the command line:
|
||||
conan.install = To install the package using Conan, run the following command:
|
||||
conan.documentation = For more information on the Conan registry, see <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/conan/">the documentation</a>.
|
||||
conda.registry = Setup this registry as a Conda repository in your <code>.condarc</code> file:
|
||||
conda.install = To install the package using Conda, run the following command:
|
||||
conda.documentation = For more information on the Conda registry, see <a target="_blank" rel="noopener noreferrer" href="https://docs.gitea.io/en-us/packages/conda/">the documentation</a>.
|
||||
conda.details.repository_site = Repository Site
|
||||
conda.details.documentation_site = Documentation Site
|
||||
container.details.type = Image Type
|
||||
container.details.platform = Platform
|
||||
container.details.repository_site = Repository Site
|
||||
|
||||
1
public/img/svg/gitea-conda.svg
Normal file
1
public/img/svg/gitea-conda.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg viewBox="0 0 32 32" class="svg gitea-conda" width="16" height="16" aria-hidden="true"><path fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068" d="M16.559 8.137a7.2 7.2 0 0 0-1.234-1.708 7.586 7.586 0 0 0-.19 2.183 5.161 5.161 0 0 1 1.424-.475ZM13.617 9.466a7.992 7.992 0 0 0-1.993-1.2 8.123 8.123 0 0 0 .885 2.183c0 .063.443-.475 1.108-.981ZM17.445 7.188a9.143 9.143 0 0 1 1.3-2.246A7.585 7.585 0 0 0 17 2.854a8.35 8.35 0 0 0-1.3 2.278 8.451 8.451 0 0 1 1.74 2.056ZM11.592 11.744a10.276 10.276 0 0 0-2.692-.158 7.478 7.478 0 0 0 1.93 1.9 6.858 6.858 0 0 1 .759-1.74zM6.878 15.161a7.44 7.44 0 0 1 2.942-1.139 10.019 10.019 0 0 1-2.056-2.278 7.639 7.639 0 0 0-2.847 1.2 7.11 7.11 0 0 0 1.961 2.215zM10.516 14.876a6.16 6.16 0 0 0-2.815.886 9.936 9.936 0 0 0 2.815 1.2 7.683 7.683 0 0 1 0-2.088zM14.281 5.543A7.839 7.839 0 0 0 11.592 4.4 8.361 8.361 0 0 0 11.4 7a8.875 8.875 0 0 1 2.47 1.264 10.292 10.292 0 0 1 .411-2.721ZM24.025 3.234a20.488 20.488 0 0 1 .917 4.112 6.823 6.823 0 0 0-3.068 1.519 7.443 7.443 0 0 1 1.55 1.044 1.351 1.351 0 0 0 1.645.316 36.938 36.938 0 0 0 2.721-2.72 1.273 1.273 0 0 0-.159-1.835 20.521 20.521 0 0 0-3.606-2.436ZM4.379 12.06a8.67 8.67 0 0 1 2.847-1.26 7.763 7.763 0 0 1-.759-2.974 14.687 14.687 0 0 0-2.088 4.234ZM11.339 10.668a9.991 9.991 0 0 1-.949-2.784 7.928 7.928 0 0 0-2.911-.126 7.312 7.312 0 0 0 .791 2.879 9.664 9.664 0 0 1 3.069.031ZM6.119 15.73a8.894 8.894 0 0 1-2.025-2.373 14.208 14.208 0 0 0-.063 4.9 8.522 8.522 0 0 1 2.088-2.527Z"/><path fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".068" d="M22.538 3.487A7.581 7.581 0 0 0 20.323 5.1a11.789 11.789 0 0 1 .823 2.5 9.775 9.775 0 0 1 2.309-1.329 6.593 6.593 0 0 0-.917-2.784ZM19.374 6.3a8.608 8.608 0 0 0-.822 1.676 9.645 9.645 0 0 1 1.329.19 7.568 7.568 0 0 0-.507-1.866ZM19.659 3.9a9.577 9.577 0 0 1 2.056-1.487A15.38 15.38 0 0 0 18.046 2a9.709 9.709 0 0 1 1.613 1.9Z"/><path fill="#43b02a" d="M27.378 23.892c-1.993-1.9-2.4-3.132-4.081-1.835a7.837 7.837 0 0 1-12.591-4.144A10.179 10.179 0 0 1 6.878 16.3a9.427 9.427 0 0 0-2.562 3.321h-.032C7.163 30.5 21.178 33.035 27.663 26.233c1.076-1.139.095-1.933-.285-2.341ZM6.309 20.855a7.559 7.559 0 0 1 .917-2.025 6.872 6.872 0 0 0 2.151.538c1.013 2.689 4.556 6.264 8.922 6.264a9.632 9.632 0 0 0 6.3-2.309 12.841 12.841 0 0 1 1.772 1.771c.095.127.095.159.095.159-5.766 5.03-15.538 4.302-20.157-4.398Z"/><path fill="#43b02a" fill-rule="evenodd" stroke="#43b02a" stroke-width=".067" d="M10.67 4.11a19.934 19.934 0 0 0-.214 2.509 10.512 10.512 0 0 0-2.689-.093A18 18 0 0 1 10.67 4.11ZM12.26 3.274a9.107 9.107 0 0 1 2.445 1.053 14.083 14.083 0 0 1 1.253-2.137 12.106 12.106 0 0 0-3.698 1.084z"/></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
@ -16,6 +16,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/routers/api/packages/composer"
|
||||
"code.gitea.io/gitea/routers/api/packages/conan"
|
||||
"code.gitea.io/gitea/routers/api/packages/conda"
|
||||
"code.gitea.io/gitea/routers/api/packages/container"
|
||||
"code.gitea.io/gitea/routers/api/packages/generic"
|
||||
"code.gitea.io/gitea/routers/api/packages/helm"
|
||||
@ -167,6 +168,43 @@ func CommonRoutes(ctx gocontext.Context) *web.Route {
|
||||
})
|
||||
})
|
||||
}, reqPackageAccess(perm.AccessModeRead))
|
||||
r.Group("/conda", func() {
|
||||
var (
|
||||
downloadPattern = regexp.MustCompile(`\A(.+/)?(.+)/((?:[^/]+(?:\.tar\.bz2|\.conda))|(?:current_)?repodata\.json(?:\.bz2)?)\z`)
|
||||
uploadPattern = regexp.MustCompile(`\A(.+/)?([^/]+(?:\.tar\.bz2|\.conda))\z`)
|
||||
)
|
||||
|
||||
r.Get("/*", func(ctx *context.Context) {
|
||||
m := downloadPattern.FindStringSubmatch(ctx.Params("*"))
|
||||
if len(m) == 0 {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetParams("channel", strings.TrimSuffix(m[1], "/"))
|
||||
ctx.SetParams("architecture", m[2])
|
||||
ctx.SetParams("filename", m[3])
|
||||
|
||||
switch m[3] {
|
||||
case "repodata.json", "repodata.json.bz2", "current_repodata.json", "current_repodata.json.bz2":
|
||||
conda.EnumeratePackages(ctx)
|
||||
default:
|
||||
conda.DownloadPackageFile(ctx)
|
||||
}
|
||||
})
|
||||
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), func(ctx *context.Context) {
|
||||
m := uploadPattern.FindStringSubmatch(ctx.Params("*"))
|
||||
if len(m) == 0 {
|
||||
ctx.Status(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetParams("channel", strings.TrimSuffix(m[1], "/"))
|
||||
ctx.SetParams("filename", m[2])
|
||||
|
||||
conda.UploadPackageFile(ctx)
|
||||
})
|
||||
}, reqPackageAccess(perm.AccessModeRead))
|
||||
r.Group("/generic", func() {
|
||||
r.Group("/{packagename}/{packageversion}", func() {
|
||||
r.Delete("", reqPackageAccess(perm.AccessModeWrite), generic.DeletePackage)
|
||||
|
||||
306
routers/api/packages/conda/conda.go
Normal file
306
routers/api/packages/conda/conda.go
Normal file
@ -0,0 +1,306 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package conda
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
packages_model "code.gitea.io/gitea/models/packages"
|
||||
conda_model "code.gitea.io/gitea/models/packages/conda"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
packages_module "code.gitea.io/gitea/modules/packages"
|
||||
conda_module "code.gitea.io/gitea/modules/packages/conda"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/routers/api/packages/helper"
|
||||
packages_service "code.gitea.io/gitea/services/packages"
|
||||
|
||||
"github.com/dsnet/compress/bzip2"
|
||||
)
|
||||
|
||||
func apiError(ctx *context.Context, status int, obj interface{}) {
|
||||
helper.LogAndProcessError(ctx, status, obj, func(message string) {
|
||||
ctx.JSON(status, struct {
|
||||
Reason string `json:"reason"`
|
||||
Message string `json:"message"`
|
||||
}{
|
||||
Reason: http.StatusText(status),
|
||||
Message: message,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func EnumeratePackages(ctx *context.Context) {
|
||||
type Info struct {
|
||||
Subdir string `json:"subdir"`
|
||||
}
|
||||
|
||||
type PackageInfo struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
NoArch string `json:"noarch"`
|
||||
Subdir string `json:"subdir"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
Build string `json:"build"`
|
||||
BuildNumber int64 `json:"build_number"`
|
||||
Dependencies []string `json:"depends"`
|
||||
License string `json:"license"`
|
||||
LicenseFamily string `json:"license_family"`
|
||||
HashMD5 string `json:"md5"`
|
||||
HashSHA256 string `json:"sha256"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
|
||||
type RepoData struct {
|
||||
Info Info `json:"info"`
|
||||
Packages map[string]*PackageInfo `json:"packages"`
|
||||
PackagesConda map[string]*PackageInfo `json:"packages.conda"`
|
||||
Removed map[string]*PackageInfo `json:"removed"`
|
||||
}
|
||||
|
||||
repoData := &RepoData{
|
||||
Info: Info{
|
||||
Subdir: ctx.Params("architecture"),
|
||||
},
|
||||
Packages: make(map[string]*PackageInfo),
|
||||
PackagesConda: make(map[string]*PackageInfo),
|
||||
Removed: make(map[string]*PackageInfo),
|
||||
}
|
||||
|
||||
pfs, err := conda_model.SearchFiles(ctx, &conda_model.FileSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Channel: ctx.Params("channel"),
|
||||
Subdir: repoData.Info.Subdir,
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(pfs) == 0 {
|
||||
apiError(ctx, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
pds := make(map[int64]*packages_model.PackageDescriptor)
|
||||
|
||||
for _, pf := range pfs {
|
||||
pd, exists := pds[pf.VersionID]
|
||||
if !exists {
|
||||
pv, err := packages_model.GetVersionByID(ctx, pf.VersionID)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
pd, err = packages_model.GetPackageDescriptor(ctx, pv)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
pds[pf.VersionID] = pd
|
||||
}
|
||||
|
||||
var pfd *packages_model.PackageFileDescriptor
|
||||
for _, d := range pd.Files {
|
||||
if d.File.ID == pf.ID {
|
||||
pfd = d
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
var fileMetadata *conda_module.FileMetadata
|
||||
if err := json.Unmarshal([]byte(pfd.Properties.GetByName(conda_module.PropertyMetadata)), &fileMetadata); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
versionMetadata := pd.Metadata.(*conda_module.VersionMetadata)
|
||||
|
||||
pi := &PackageInfo{
|
||||
Name: pd.PackageProperties.GetByName(conda_module.PropertyName),
|
||||
Version: pd.Version.Version,
|
||||
NoArch: fileMetadata.NoArch,
|
||||
Subdir: repoData.Info.Subdir,
|
||||
Timestamp: fileMetadata.Timestamp,
|
||||
Build: fileMetadata.Build,
|
||||
BuildNumber: fileMetadata.BuildNumber,
|
||||
Dependencies: fileMetadata.Dependencies,
|
||||
License: versionMetadata.License,
|
||||
LicenseFamily: versionMetadata.LicenseFamily,
|
||||
HashMD5: pfd.Blob.HashMD5,
|
||||
HashSHA256: pfd.Blob.HashSHA256,
|
||||
Size: pfd.Blob.Size,
|
||||
}
|
||||
|
||||
if fileMetadata.IsCondaPackage {
|
||||
repoData.PackagesConda[pfd.File.Name] = pi
|
||||
} else {
|
||||
repoData.Packages[pfd.File.Name] = pi
|
||||
}
|
||||
}
|
||||
|
||||
resp := ctx.Resp
|
||||
|
||||
var w io.Writer = resp
|
||||
|
||||
if strings.HasSuffix(ctx.Params("filename"), ".json") {
|
||||
resp.Header().Set("Content-Type", "application/json")
|
||||
} else {
|
||||
resp.Header().Set("Content-Type", "application/x-bzip2")
|
||||
|
||||
zw, err := bzip2.NewWriter(w, nil)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer zw.Close()
|
||||
|
||||
w = zw
|
||||
}
|
||||
|
||||
resp.WriteHeader(http.StatusOK)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(repoData); err != nil {
|
||||
log.Error("JSON encode: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func UploadPackageFile(ctx *context.Context) {
|
||||
upload, close, err := ctx.UploadStream()
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if close {
|
||||
defer upload.Close()
|
||||
}
|
||||
|
||||
buf, err := packages_module.CreateHashedBufferFromReader(upload, 32*1024*1024)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer buf.Close()
|
||||
|
||||
var pck *conda_module.Package
|
||||
if strings.HasSuffix(strings.ToLower(ctx.Params("filename")), ".tar.bz2") {
|
||||
pck, err = conda_module.ParsePackageBZ2(buf)
|
||||
} else {
|
||||
pck, err = conda_module.ParsePackageConda(buf, buf.Size())
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrInvalidArgument) {
|
||||
apiError(ctx, http.StatusBadRequest, err)
|
||||
} else {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := buf.Seek(0, io.SeekStart); err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
fullName := pck.Name
|
||||
|
||||
channel := ctx.Params("channel")
|
||||
if channel != "" {
|
||||
fullName = channel + "/" + pck.Name
|
||||
}
|
||||
|
||||
extension := ".tar.bz2"
|
||||
if pck.FileMetadata.IsCondaPackage {
|
||||
extension = ".conda"
|
||||
}
|
||||
|
||||
fileMetadataRaw, err := json.Marshal(pck.FileMetadata)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, _, err = packages_service.CreatePackageOrAddFileToExisting(
|
||||
&packages_service.PackageCreationInfo{
|
||||
PackageInfo: packages_service.PackageInfo{
|
||||
Owner: ctx.Package.Owner,
|
||||
PackageType: packages_model.TypeConda,
|
||||
Name: fullName,
|
||||
Version: pck.Version,
|
||||
},
|
||||
SemverCompatible: false,
|
||||
Creator: ctx.Doer,
|
||||
Metadata: pck.VersionMetadata,
|
||||
PackageProperties: map[string]string{
|
||||
conda_module.PropertyName: pck.Name,
|
||||
conda_module.PropertyChannel: channel,
|
||||
},
|
||||
},
|
||||
&packages_service.PackageFileCreationInfo{
|
||||
PackageFileInfo: packages_service.PackageFileInfo{
|
||||
Filename: fmt.Sprintf("%s-%s-%s%s", pck.Name, pck.Version, pck.FileMetadata.Build, extension),
|
||||
CompositeKey: pck.Subdir,
|
||||
},
|
||||
Creator: ctx.Doer,
|
||||
Data: buf,
|
||||
IsLead: true,
|
||||
Properties: map[string]string{
|
||||
conda_module.PropertySubdir: pck.Subdir,
|
||||
conda_module.PropertyMetadata: string(fileMetadataRaw),
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
switch err {
|
||||
case packages_model.ErrDuplicatePackageFile:
|
||||
apiError(ctx, http.StatusConflict, err)
|
||||
case packages_service.ErrQuotaTotalCount, packages_service.ErrQuotaTypeSize, packages_service.ErrQuotaTotalSize:
|
||||
apiError(ctx, http.StatusForbidden, err)
|
||||
default:
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Status(http.StatusCreated)
|
||||
}
|
||||
|
||||
func DownloadPackageFile(ctx *context.Context) {
|
||||
pfs, err := conda_model.SearchFiles(ctx, &conda_model.FileSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Channel: ctx.Params("channel"),
|
||||
Subdir: ctx.Params("architecture"),
|
||||
Filename: ctx.Params("filename"),
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(pfs) != 1 {
|
||||
apiError(ctx, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
pf := pfs[0]
|
||||
|
||||
s, _, err := packages_service.GetPackageFileStream(ctx, pf)
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
ctx.ServeContent(s, &context.ServeHeaderOptions{
|
||||
Filename: pf.Name,
|
||||
LastModified: pf.CreatedUnix.AsLocalTime(),
|
||||
})
|
||||
}
|
||||
@ -40,7 +40,7 @@ func ListPackages(ctx *context.APIContext) {
|
||||
// in: query
|
||||
// description: package type filter
|
||||
// type: string
|
||||
// enum: [composer, conan, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant]
|
||||
// enum: [composer, conan, conda, container, generic, helm, maven, npm, nuget, pub, pypi, rubygems, vagrant]
|
||||
// - name: q
|
||||
// in: query
|
||||
// description: name filter
|
||||
|
||||
@ -22,6 +22,7 @@ const (
|
||||
func DefaultOrSystemWebhooks(ctx *context.Context) {
|
||||
var err error
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("admin.hooks")
|
||||
ctx.Data["PageIsAdminSystemHooks"] = true
|
||||
ctx.Data["PageIsAdminDefaultHooks"] = true
|
||||
|
||||
|
||||
@ -328,6 +328,14 @@ func NewRelease(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
|
||||
var err error
|
||||
// Get assignees.
|
||||
ctx.Data["Assignees"], err = repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAssignees", err)
|
||||
return
|
||||
}
|
||||
|
||||
upload.AddUploadContext(ctx, "release")
|
||||
ctx.HTML(http.StatusOK, tplReleaseNew)
|
||||
}
|
||||
@ -484,6 +492,13 @@ func EditRelease(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["attachments"] = rel.Attachments
|
||||
|
||||
// Get assignees.
|
||||
ctx.Data["Assignees"], err = repo_model.GetRepoAssignees(ctx, rel.Repo)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAssignees", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplReleaseNew)
|
||||
}
|
||||
|
||||
|
||||
@ -60,7 +60,7 @@ const (
|
||||
// SettingsCtxData is a middleware that sets all the general context data for the
|
||||
// settings template.
|
||||
func SettingsCtxData(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings")
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.options")
|
||||
ctx.Data["PageIsSettingsOptions"] = true
|
||||
ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate
|
||||
ctx.Data["MirrorsEnabled"] = setting.Mirror.Enabled
|
||||
@ -894,7 +894,7 @@ func handleSettingRemoteAddrError(ctx *context.Context, err error, form *forms.R
|
||||
|
||||
// Collaboration render a repository's collaboration page
|
||||
func Collaboration(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings")
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.collaboration")
|
||||
ctx.Data["PageIsSettingsCollaboration"] = true
|
||||
|
||||
users, err := repo_model.GetCollaborators(ctx, ctx.Repo.Repository.ID, db.ListOptions{})
|
||||
@ -1134,7 +1134,7 @@ func GitHooksEditPost(ctx *context.Context) {
|
||||
|
||||
// DeployKeys render the deploy keys list of a repository page
|
||||
func DeployKeys(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys") + " / " + ctx.Tr("secrets.secrets")
|
||||
ctx.Data["PageIsSettingsKeys"] = true
|
||||
ctx.Data["DisableSSH"] = setting.SSH.Disabled
|
||||
|
||||
@ -1173,6 +1173,10 @@ func DeployKeysPost(ctx *context.Context) {
|
||||
ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
|
||||
} else if asymkey_model.IsErrKeyUnableVerify(err) {
|
||||
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
|
||||
} else if err == asymkey_model.ErrKeyIsPrivate {
|
||||
ctx.Data["HasError"] = true
|
||||
ctx.Data["Err_Content"] = true
|
||||
ctx.Flash.Error(ctx.Tr("form.must_use_public_key"))
|
||||
} else {
|
||||
ctx.Data["HasError"] = true
|
||||
ctx.Data["Err_Content"] = true
|
||||
|
||||
@ -31,7 +31,7 @@ const (
|
||||
|
||||
// ProtectedBranchRules render the page to protect the repository
|
||||
func ProtectedBranchRules(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings")
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.branches")
|
||||
ctx.Data["PageIsSettingsBranches"] = true
|
||||
|
||||
rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID)
|
||||
@ -46,7 +46,7 @@ func ProtectedBranchRules(ctx *context.Context) {
|
||||
|
||||
// SetDefaultBranchPost set default branch
|
||||
func SetDefaultBranchPost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings")
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.branches.update_default_branch")
|
||||
ctx.Data["PageIsSettingsBranches"] = true
|
||||
|
||||
repo := ctx.Repo.Repository
|
||||
|
||||
@ -133,7 +133,7 @@ func DeleteProtectedTagPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
func setTagsContext(ctx *context.Context) error {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings")
|
||||
ctx.Data["Title"] = ctx.Tr("repo.settings.tags")
|
||||
ctx.Data["PageIsSettingsTags"] = true
|
||||
|
||||
protectedTags, err := git_model.GetProtectedTags(ctx, ctx.Repo.Repository.ID)
|
||||
|
||||
@ -30,7 +30,7 @@ const (
|
||||
|
||||
// Account renders change user's password, user's email and user suicide page
|
||||
func Account(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["Title"] = ctx.Tr("settings.account")
|
||||
ctx.Data["PageIsSettingsAccount"] = true
|
||||
ctx.Data["Email"] = ctx.Doer.Email
|
||||
ctx.Data["EnableNotifyMail"] = setting.Service.EnableNotifyMail
|
||||
|
||||
@ -17,7 +17,7 @@ import (
|
||||
|
||||
// AdoptOrDeleteRepository adopts or deletes a repository
|
||||
func AdoptOrDeleteRepository(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["Title"] = ctx.Tr("settings.adopt")
|
||||
ctx.Data["PageIsSettingsRepos"] = true
|
||||
allowAdopt := ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
|
||||
ctx.Data["allowAdopt"] = allowAdopt
|
||||
|
||||
@ -21,7 +21,7 @@ const (
|
||||
|
||||
// Applications render manage access token page
|
||||
func Applications(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["Title"] = ctx.Tr("settings.applications")
|
||||
ctx.Data["PageIsSettingsApplications"] = true
|
||||
|
||||
loadApplicationsData(ctx)
|
||||
|
||||
@ -23,7 +23,7 @@ const (
|
||||
|
||||
// Keys render user's SSH/GPG public keys page
|
||||
func Keys(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["Title"] = ctx.Tr("settings.ssh_gpg_keys")
|
||||
ctx.Data["PageIsSettingsKeys"] = true
|
||||
ctx.Data["DisableSSH"] = setting.SSH.Disabled
|
||||
ctx.Data["BuiltinSSH"] = setting.SSH.StartBuiltinServer
|
||||
@ -159,6 +159,8 @@ func KeysPost(ctx *context.Context) {
|
||||
ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
|
||||
} else if asymkey_model.IsErrKeyUnableVerify(err) {
|
||||
ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
|
||||
} else if err == asymkey_model.ErrKeyIsPrivate {
|
||||
ctx.Flash.Error(ctx.Tr("form.must_use_public_key"))
|
||||
} else {
|
||||
ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
|
||||
}
|
||||
|
||||
@ -42,7 +42,7 @@ const (
|
||||
|
||||
// Profile render user's profile page
|
||||
func Profile(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["Title"] = ctx.Tr("settings.profile")
|
||||
ctx.Data["PageIsSettingsProfile"] = true
|
||||
ctx.Data["AllowedUserVisibilityModes"] = setting.Service.AllowedUserVisibilityModesSlice.ToVisibleTypeSlice()
|
||||
|
||||
@ -219,7 +219,7 @@ func DeleteAvatar(ctx *context.Context) {
|
||||
|
||||
// Organization render all the organization of the user
|
||||
func Organization(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["Title"] = ctx.Tr("settings.organization")
|
||||
ctx.Data["PageIsSettingsOrganization"] = true
|
||||
|
||||
opts := organization.FindOrgOptions{
|
||||
@ -254,7 +254,7 @@ func Organization(ctx *context.Context) {
|
||||
|
||||
// Repos display a list of all repositories of the user
|
||||
func Repos(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["Title"] = ctx.Tr("settings.repos")
|
||||
ctx.Data["PageIsSettingsRepos"] = true
|
||||
ctx.Data["allowAdopt"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowAdoptionOfUnadoptedRepositories
|
||||
ctx.Data["allowDelete"] = ctx.IsUserSiteAdmin() || setting.Repository.AllowDeleteOfUnadoptedRepositories
|
||||
@ -360,7 +360,7 @@ func Repos(ctx *context.Context) {
|
||||
|
||||
// Appearance render user's appearance settings
|
||||
func Appearance(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["Title"] = ctx.Tr("settings.appearance")
|
||||
ctx.Data["PageIsSettingsAppearance"] = true
|
||||
|
||||
var hiddenCommentTypes *big.Int
|
||||
|
||||
@ -22,7 +22,7 @@ const (
|
||||
|
||||
// Security render change user's password page and 2FA
|
||||
func Security(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("settings")
|
||||
ctx.Data["Title"] = ctx.Tr("settings.security")
|
||||
ctx.Data["PageIsSettingsSecurity"] = true
|
||||
|
||||
if ctx.FormString("openid.return_to") != "" {
|
||||
|
||||
@ -196,22 +196,39 @@ func checkRestricted(l *ldap.Conn, ls *Source, userDN string) bool {
|
||||
}
|
||||
|
||||
// List all group memberships of a user
|
||||
func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string) []string {
|
||||
func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGroupFilter bool) []string {
|
||||
var ldapGroups []string
|
||||
groupFilter := fmt.Sprintf("(%s=%s)", source.GroupMemberUID, ldap.EscapeFilter(uid))
|
||||
var searchFilter string
|
||||
|
||||
groupFilter, ok := source.sanitizedGroupFilter(source.GroupFilter)
|
||||
if !ok {
|
||||
return ldapGroups
|
||||
}
|
||||
|
||||
groupDN, ok := source.sanitizedGroupDN(source.GroupDN)
|
||||
if !ok {
|
||||
return ldapGroups
|
||||
}
|
||||
|
||||
if applyGroupFilter {
|
||||
searchFilter = fmt.Sprintf("(&(%s)(%s=%s))", groupFilter, source.GroupMemberUID, ldap.EscapeFilter(uid))
|
||||
} else {
|
||||
searchFilter = fmt.Sprintf("(%s=%s)", source.GroupMemberUID, ldap.EscapeFilter(uid))
|
||||
}
|
||||
|
||||
result, err := l.Search(ldap.NewSearchRequest(
|
||||
source.GroupDN,
|
||||
groupDN,
|
||||
ldap.ScopeWholeSubtree,
|
||||
ldap.NeverDerefAliases,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
groupFilter,
|
||||
searchFilter,
|
||||
[]string{},
|
||||
nil,
|
||||
))
|
||||
if err != nil {
|
||||
log.Error("Failed group search using filter[%s]: %v", groupFilter, err)
|
||||
log.Error("Failed group search in LDAP with filter [%s]: %v", searchFilter, err)
|
||||
return ldapGroups
|
||||
}
|
||||
|
||||
@ -238,9 +255,7 @@ func (source *Source) mapLdapGroupsToTeams() map[string]map[string][]string {
|
||||
}
|
||||
|
||||
// getMappedMemberships : returns the organizations and teams to modify the users membership
|
||||
func (source *Source) getMappedMemberships(l *ldap.Conn, uid string) (map[string][]string, map[string][]string) {
|
||||
// get all LDAP group memberships for user
|
||||
usersLdapGroups := source.listLdapGroupMemberships(l, uid)
|
||||
func (source *Source) getMappedMemberships(usersLdapGroups []string, uid string) (map[string][]string, map[string][]string) {
|
||||
// unmarshall LDAP group team map from configs
|
||||
ldapGroupsToTeams := source.mapLdapGroupsToTeams()
|
||||
membershipsToAdd := map[string][]string{}
|
||||
@ -260,6 +275,14 @@ func (source *Source) getMappedMemberships(l *ldap.Conn, uid string) (map[string
|
||||
return membershipsToAdd, membershipsToRemove
|
||||
}
|
||||
|
||||
func (source *Source) getUserAttributeListedInGroup(entry *ldap.Entry) string {
|
||||
if strings.ToLower(source.UserUID) == "dn" {
|
||||
return entry.DN
|
||||
}
|
||||
|
||||
return entry.GetAttributeValue(source.UserUID)
|
||||
}
|
||||
|
||||
// SearchEntry : search an LDAP source if an entry (name, passwd) is valid and in the specific filter
|
||||
func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchResult {
|
||||
// See https://tools.ietf.org/search/rfc4513#section-5.1.2
|
||||
@ -375,58 +398,30 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
|
||||
firstname := sr.Entries[0].GetAttributeValue(source.AttributeName)
|
||||
surname := sr.Entries[0].GetAttributeValue(source.AttributeSurname)
|
||||
mail := sr.Entries[0].GetAttributeValue(source.AttributeMail)
|
||||
uid := sr.Entries[0].GetAttributeValue(source.UserUID)
|
||||
if source.UserUID == "dn" || source.UserUID == "DN" {
|
||||
uid = sr.Entries[0].DN
|
||||
}
|
||||
|
||||
teamsToAdd := make(map[string][]string)
|
||||
teamsToRemove := make(map[string][]string)
|
||||
|
||||
// Check group membership
|
||||
if source.GroupsEnabled && source.GroupFilter != "" {
|
||||
groupFilter, ok := source.sanitizedGroupFilter(source.GroupFilter)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
groupDN, ok := source.sanitizedGroupDN(source.GroupDN)
|
||||
if !ok {
|
||||
if source.GroupsEnabled {
|
||||
userAttributeListedInGroup := source.getUserAttributeListedInGroup(sr.Entries[0])
|
||||
usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
|
||||
|
||||
if source.GroupFilter != "" && len(usersLdapGroups) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Trace("Fetching groups '%v' with filter '%s' and base '%s'", source.GroupMemberUID, groupFilter, groupDN)
|
||||
groupSearch := ldap.NewSearchRequest(
|
||||
groupDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, groupFilter,
|
||||
[]string{source.GroupMemberUID},
|
||||
nil)
|
||||
|
||||
srg, err := l.Search(groupSearch)
|
||||
if err != nil {
|
||||
log.Error("LDAP group search failed: %v", err)
|
||||
return nil
|
||||
} else if len(srg.Entries) < 1 {
|
||||
log.Error("LDAP group search failed: 0 entries")
|
||||
return nil
|
||||
}
|
||||
|
||||
isMember := false
|
||||
Entries:
|
||||
for _, group := range srg.Entries {
|
||||
for _, member := range group.GetAttributeValues(source.GroupMemberUID) {
|
||||
if (source.UserUID == "dn" && member == sr.Entries[0].DN) || member == uid {
|
||||
isMember = true
|
||||
break Entries
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !isMember {
|
||||
log.Error("LDAP group membership test failed")
|
||||
return nil
|
||||
if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
|
||||
teamsToAdd, teamsToRemove = source.getMappedMemberships(usersLdapGroups, userAttributeListedInGroup)
|
||||
}
|
||||
}
|
||||
|
||||
if isAttributeSSHPublicKeySet {
|
||||
sshPublicKey = sr.Entries[0].GetAttributeValues(source.AttributeSSHPublicKey)
|
||||
}
|
||||
|
||||
isAdmin := checkAdmin(l, source, userDN)
|
||||
|
||||
var isRestricted bool
|
||||
if !isAdmin {
|
||||
isRestricted = checkRestricted(l, source, userDN)
|
||||
@ -436,12 +431,6 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
|
||||
Avatar = sr.Entries[0].GetRawAttributeValue(source.AttributeAvatar)
|
||||
}
|
||||
|
||||
teamsToAdd := make(map[string][]string)
|
||||
teamsToRemove := make(map[string][]string)
|
||||
if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
|
||||
teamsToAdd, teamsToRemove = source.getMappedMemberships(l, uid)
|
||||
}
|
||||
|
||||
if !directBind && source.AttributesInBind {
|
||||
// binds user (checking password) after looking-up attributes in BindDN context
|
||||
err = bindUser(l, userDN, passwd)
|
||||
@ -520,19 +509,29 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]*SearchResult, len(sr.Entries))
|
||||
result := make([]*SearchResult, 0, len(sr.Entries))
|
||||
|
||||
for i, v := range sr.Entries {
|
||||
for _, v := range sr.Entries {
|
||||
teamsToAdd := make(map[string][]string)
|
||||
teamsToRemove := make(map[string][]string)
|
||||
if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
|
||||
userAttributeListedInGroup := v.GetAttributeValue(source.UserUID)
|
||||
if source.UserUID == "dn" || source.UserUID == "DN" {
|
||||
userAttributeListedInGroup = v.DN
|
||||
|
||||
if source.GroupsEnabled {
|
||||
userAttributeListedInGroup := source.getUserAttributeListedInGroup(v)
|
||||
|
||||
if source.GroupFilter != "" {
|
||||
usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
|
||||
if len(usersLdapGroups) == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
|
||||
usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, false)
|
||||
teamsToAdd, teamsToRemove = source.getMappedMemberships(usersLdapGroups, userAttributeListedInGroup)
|
||||
}
|
||||
teamsToAdd, teamsToRemove = source.getMappedMemberships(l, userAttributeListedInGroup)
|
||||
}
|
||||
result[i] = &SearchResult{
|
||||
|
||||
user := &SearchResult{
|
||||
Username: v.GetAttributeValue(source.AttributeUsername),
|
||||
Name: v.GetAttributeValue(source.AttributeName),
|
||||
Surname: v.GetAttributeValue(source.AttributeSurname),
|
||||
@ -541,16 +540,22 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) {
|
||||
LdapTeamAdd: teamsToAdd,
|
||||
LdapTeamRemove: teamsToRemove,
|
||||
}
|
||||
if !result[i].IsAdmin {
|
||||
result[i].IsRestricted = checkRestricted(l, source, v.DN)
|
||||
|
||||
if !user.IsAdmin {
|
||||
user.IsRestricted = checkRestricted(l, source, v.DN)
|
||||
}
|
||||
|
||||
if isAttributeSSHPublicKeySet {
|
||||
result[i].SSHPublicKey = v.GetAttributeValues(source.AttributeSSHPublicKey)
|
||||
user.SSHPublicKey = v.GetAttributeValues(source.AttributeSSHPublicKey)
|
||||
}
|
||||
|
||||
if isAtributeAvatarSet {
|
||||
result[i].Avatar = v.GetRawAttributeValue(source.AttributeAvatar)
|
||||
user.Avatar = v.GetRawAttributeValue(source.AttributeAvatar)
|
||||
}
|
||||
result[i].LowerName = strings.ToLower(result[i].Username)
|
||||
|
||||
user.LowerName = strings.ToLower(user.Username)
|
||||
|
||||
result = append(result, user)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
@ -15,7 +15,7 @@ import (
|
||||
type PackageCleanupRuleForm struct {
|
||||
ID int64
|
||||
Enabled bool
|
||||
Type string `binding:"Required;In(composer,conan,container,generic,helm,maven,npm,nuget,pub,pypi,rubygems,vagrant)"`
|
||||
Type string `binding:"Required;In(composer,conan,conda,container,generic,helm,maven,npm,nuget,pub,pypi,rubygems,vagrant)"`
|
||||
KeepCount int `binding:"In(0,1,5,10,25,50,100)"`
|
||||
KeepPattern string `binding:"RegexPattern"`
|
||||
RemoveDays int `binding:"In(0,7,14,30,60,90,180)"`
|
||||
|
||||
@ -339,6 +339,8 @@ func CheckSizeQuotaExceeded(ctx context.Context, doer, owner *user_model.User, p
|
||||
typeSpecificSize = setting.Packages.LimitSizeComposer
|
||||
case packages_model.TypeConan:
|
||||
typeSpecificSize = setting.Packages.LimitSizeConan
|
||||
case packages_model.TypeConda:
|
||||
typeSpecificSize = setting.Packages.LimitSizeConda
|
||||
case packages_model.TypeContainer:
|
||||
typeSpecificSize = setting.Packages.LimitSizeContainer
|
||||
case packages_model.TypeGeneric:
|
||||
|
||||
@ -298,7 +298,6 @@ func AddTestPullRequestTask(doer *user_model.User, repoID int64, branch string,
|
||||
}
|
||||
}
|
||||
|
||||
pr.Issue.PullRequest = pr
|
||||
notification.NotifyPullRequestSynchronized(ctx, doer, pr)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin config">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin config">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
<div class="twelve wide column content">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin config">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin config">
|
||||
{{template "admin/navbar" .}}
|
||||
|
||||
{{template "user/settings/applications_oauth2_edit_form" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin edit authentication">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin edit authentication">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin authentication">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin authentication">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin new authentication">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin new authentication">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin config">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin config">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin dashboard">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin dashboard">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin user">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin user">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin settings new webhook">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin settings new webhook">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin hooks">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin hooks">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin monitor">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin monitor">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin notice">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin notice">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin user">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin user">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin user">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin user">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin monitor">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin monitor">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin user">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin user">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin user">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin user">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin monitor">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin monitor">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin edit user">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin edit user">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin user">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin user">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content admin new user">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content admin new user">
|
||||
{{template "admin/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content explore users">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content explore users">
|
||||
{{template "explore/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "code/searchform" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content explore users">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content explore users">
|
||||
{{template "explore/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "explore/search" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content explore repositories">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content explore repositories">
|
||||
{{template "explore/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "explore/repo_search" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content explore users">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content explore users">
|
||||
{{template "explore/navbar" .}}
|
||||
<div class="ui container">
|
||||
{{template "explore/search" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content home">
|
||||
<div role="main" aria-label="{{if .IsSigned}}{{.locale.Tr "dashboard"}}{{else}}{{.locale.Tr "home"}}{{end}}" class="page-content home">
|
||||
<div class="ui stackable middle very relaxed page grid">
|
||||
<div class="sixteen wide center aligned centered column">
|
||||
<div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content install">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content install">
|
||||
<div class="ui middle very relaxed page grid">
|
||||
<div class="sixteen wide center aligned centered column">
|
||||
<h3 class="ui top attached header">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization new org">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization new org">
|
||||
<div class="ui middle very relaxed page grid">
|
||||
<div class="column">
|
||||
<form class="ui form" action="{{.Link}}" method="post">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization profile">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization profile">
|
||||
<div class="ui container df">
|
||||
{{avatar .Org 140 "org-avatar"}}
|
||||
<div id="org-info">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization members">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization members">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content repository packages">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository packages">
|
||||
{{template "user/overview/header" .}}
|
||||
{{template "projects/list" .}}
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content repository packages">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository packages">
|
||||
{{template "user/overview/header" .}}
|
||||
{{template "projects/new" .}}
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content repository packages">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository packages">
|
||||
{{template "user/overview/header" .}}
|
||||
{{template "projects/view" .}}
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization settings options">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization settings options">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization settings options">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization settings options">
|
||||
{{template "org/header" .}}
|
||||
|
||||
{{template "user/settings/applications_oauth2_edit_form" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization settings delete">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization settings delete">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization settings new webhook">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization settings new webhook">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization settings webhooks">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization settings webhooks">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization settings labels">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization settings labels">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization settings options">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization settings options">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization settings packages">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization settings packages">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization settings packages">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization settings packages">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization settings packages admin">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization settings packages admin">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization settings webhooks">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization settings webhooks">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization invite">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization invite">
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
<div class="ui centered card">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization teams">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization teams">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization new team">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization new team">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization teams">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization teams">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content organization teams">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content organization teams">
|
||||
{{template "org/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
30
templates/package/content/conda.tmpl
Normal file
30
templates/package/content/conda.tmpl
Normal file
@ -0,0 +1,30 @@
|
||||
{{if eq .PackageDescriptor.Package.Type "conda"}}
|
||||
<h4 class="ui top attached header">{{.locale.Tr "packages.installation"}}</h4>
|
||||
<div class="ui attached segment">
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-code"}} {{.locale.Tr "packages.conda.registry" | Safe}}</label>
|
||||
<div class="markup"><pre class="code-block"><code>channel_alias: {{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/conda
|
||||
channels:
|
||||
  - {{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/conda
|
||||
default_channels:
|
||||
  - {{AppUrl}}api/packages/{{.PackageDescriptor.Owner.Name}}/conda</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{svg "octicon-terminal"}} {{.locale.Tr "packages.conda.install"}}</label>
|
||||
{{$channel := .PackageDescriptor.PackageProperties.GetByName "conda.channel"}}
|
||||
<div class="markup"><pre class="code-block"><code>conda install{{if $channel}} -c {{$channel}}{{end}} {{.PackageDescriptor.PackageProperties.GetByName "conda.name"}}={{.PackageDescriptor.Version.Version}}</code></pre></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{.locale.Tr "packages.conda.documentation" | Safe}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{if or .PackageDescriptor.Metadata.Description .PackageDescriptor.Metadata.Summary}}
|
||||
<h4 class="ui top attached header">{{.locale.Tr "packages.about"}}</h4>
|
||||
<div class="ui attached segment">
|
||||
{{if .PackageDescriptor.Metadata.Description}}{{.PackageDescriptor.Metadata.Description}}{{else}}{{.PackageDescriptor.Metadata.Summary}}{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
6
templates/package/metadata/conda.tmpl
Normal file
6
templates/package/metadata/conda.tmpl
Normal file
@ -0,0 +1,6 @@
|
||||
{{if eq .PackageDescriptor.Package.Type "conda"}}
|
||||
{{if .PackageDescriptor.Metadata.License}}<div class="item">{{svg "octicon-law" 16 "mr-3"}} {{.PackageDescriptor.Metadata.License}}</div>{{end}}
|
||||
{{if .PackageDescriptor.Metadata.ProjectURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.ProjectURL}}" target="_blank" rel="noopener noreferrer me">{{.locale.Tr "packages.details.project_site"}}</a></div>{{end}}
|
||||
{{if .PackageDescriptor.Metadata.RepositoryURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.RepositoryURL}}" target="_blank" rel="noopener noreferrer me">{{.locale.Tr "packages.conda.details.repository_site"}}</a></div>{{end}}
|
||||
{{if .PackageDescriptor.Metadata.DocumentationURL}}<div class="item">{{svg "octicon-link-external" 16 "mr-3"}} <a href="{{.PackageDescriptor.Metadata.DocumentationURL}}" target="_blank" rel="noopener noreferrer me">{{.locale.Tr "packages.conda.details.documentation_site"}}</a></div>{{end}}
|
||||
{{end}}
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content repository settings options">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository settings options">
|
||||
{{template "user/overview/header" .}}
|
||||
<div class="ui container">
|
||||
{{template "base/alert" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content repository view issue packages">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository view issue packages">
|
||||
{{template "user/overview/header" .}}
|
||||
<div class="ui container">
|
||||
<div>
|
||||
@ -21,6 +21,7 @@
|
||||
<div class="twelve wide column">
|
||||
{{template "package/content/composer" .}}
|
||||
{{template "package/content/conan" .}}
|
||||
{{template "package/content/conda" .}}
|
||||
{{template "package/content/container" .}}
|
||||
{{template "package/content/generic" .}}
|
||||
{{template "package/content/helm" .}}
|
||||
@ -44,6 +45,7 @@
|
||||
<div class="item">{{svg "octicon-download" 16 "mr-3"}} {{.PackageDescriptor.Version.DownloadCount}}</div>
|
||||
{{template "package/metadata/composer" .}}
|
||||
{{template "package/metadata/conan" .}}
|
||||
{{template "package/metadata/conda" .}}
|
||||
{{template "package/metadata/container" .}}
|
||||
{{template "package/metadata/generic" .}}
|
||||
{{template "package/metadata/helm" .}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{{template "base/head" .}}
|
||||
<div class="page-content install">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content install">
|
||||
<div class="ui container">
|
||||
<div class="ui grid">
|
||||
<div class="sixteen wide column content">
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="page-content repository projects">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository projects">
|
||||
<div class="ui container">
|
||||
{{if .CanWriteProjects}}
|
||||
<div class="navbar">
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="page-content repository projects edit-project new milestone">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository projects edit-project new milestone">
|
||||
<div class="ui container">
|
||||
<div class="navbar">
|
||||
{{if and .CanWriteProjects .PageIsEditProject}}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
<div class="page-content repository projects view-project">
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository projects view-project">
|
||||
<div class="ui container">
|
||||
<div class="ui two column stackable grid">
|
||||
<div class="column">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user