mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-28 13:18:31 +02:00
Merge remote-tracking branch 'upstream/main' into limit-repo-size
This commit is contained in:
+1
-1
@@ -28,7 +28,7 @@ linters:
|
||||
fast: false
|
||||
|
||||
run:
|
||||
go: 1.20
|
||||
go: "1.20"
|
||||
timeout: 10m
|
||||
skip-dirs:
|
||||
- node_modules
|
||||
|
||||
@@ -372,6 +372,15 @@ var (
|
||||
Value: "",
|
||||
Usage: "Group Claim value for restricted users",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "group-team-map",
|
||||
Value: "",
|
||||
Usage: "JSON mapping between groups and org teams",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "group-team-map-removal",
|
||||
Usage: "Activate automatic team membership removal depending on groups",
|
||||
},
|
||||
}
|
||||
|
||||
microcmdAuthUpdateOauth = cli.Command{
|
||||
@@ -853,6 +862,8 @@ func parseOAuth2Config(c *cli.Context) *oauth2.Source {
|
||||
GroupClaimName: c.String("group-claim-name"),
|
||||
AdminGroup: c.String("admin-group"),
|
||||
RestrictedGroup: c.String("restricted-group"),
|
||||
GroupTeamMap: c.String("group-team-map"),
|
||||
GroupTeamMapRemoval: c.Bool("group-team-map-removal"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -935,6 +946,12 @@ func runUpdateOauth(c *cli.Context) error {
|
||||
if c.IsSet("restricted-group") {
|
||||
oAuth2Config.RestrictedGroup = c.String("restricted-group")
|
||||
}
|
||||
if c.IsSet("group-team-map") {
|
||||
oAuth2Config.GroupTeamMap = c.String("group-team-map")
|
||||
}
|
||||
if c.IsSet("group-team-map-removal") {
|
||||
oAuth2Config.GroupTeamMapRemoval = c.Bool("group-team-map-removal")
|
||||
}
|
||||
|
||||
// update custom URL mapping
|
||||
customURLMapping := &oauth2.CustomURLMapping{}
|
||||
|
||||
@@ -16,7 +16,7 @@ menu:
|
||||
# Installation from binary
|
||||
|
||||
All downloads come with SQLite, MySQL and PostgreSQL support, and are built with
|
||||
embedded assets. This can be different for older releases.
|
||||
embedded assets. This can be different from Gogs.
|
||||
|
||||
**Table of Contents**
|
||||
|
||||
@@ -24,7 +24,7 @@ embedded assets. This can be different for older releases.
|
||||
|
||||
## Download
|
||||
|
||||
You can find the file matching your platform from the [downloads page](https://dl.gitea.io/gitea/) after navigating to the version you want to download.
|
||||
You can find the file matching your platform from the [downloads page](https://dl.gitea.com/gitea/) after navigating to the version you want to download.
|
||||
|
||||
### Choosing the right file
|
||||
|
||||
@@ -36,12 +36,14 @@ You can find the file matching your platform from the [downloads page](https://d
|
||||
|
||||
**For macOS**, you should choose `darwin-arm64` if your hardware uses Apple Silicon, or `darwin-amd64` for Intel.
|
||||
|
||||
**For FreeBSD**, you should choose `freebsd12-amd64` for 64-bit Intel/AMD platforms.
|
||||
|
||||
### Downloading with wget
|
||||
|
||||
Copy the commands below and replace the URL within the one you wish to download.
|
||||
|
||||
```sh
|
||||
wget -O gitea https://dl.gitea.io/gitea/{{< version >}}/gitea-{{< version >}}-linux-amd64
|
||||
wget -O gitea https://dl.gitea.com/gitea/{{< version >}}/gitea-{{< version >}}-linux-amd64
|
||||
chmod +x gitea
|
||||
```
|
||||
|
||||
@@ -173,7 +175,7 @@ an update of your Gitea version.
|
||||
Older Linux distributions (such as Debian 7 and CentOS 6) may not be able to load the
|
||||
Gitea binary, usually producing an error such as `./gitea: /lib/x86_64-linux-gnu/libc.so.6:
|
||||
version 'GLIBC\_2.14' not found (required by ./gitea)`. This is due to the integrated
|
||||
SQLite support in the binaries provided by dl.gitea.io. In this situation, it is usually
|
||||
SQLite support in the binaries provided by dl.gitea.com. In this situation, it is usually
|
||||
possible to [install from source]({{< relref "from-source.en-us.md" >}}), without including
|
||||
SQLite support.
|
||||
|
||||
|
||||
@@ -15,21 +15,150 @@ menu:
|
||||
|
||||
# 从二进制安装
|
||||
|
||||
所有下载均包括 SQLite, MySQL 和 PostgreSQL 的支持,同时所有资源均已嵌入到可执行程序中,这一点和老版本有所不同。 基于二进制的安装非常简单,只要从 [下载页面](https://dl.gitea.io/gitea) 选择对应平台,拷贝下载URL,执行以下命令即可(以Linux为例):
|
||||
所有打包的二进制程序均包含 SQLite,MySQL 和 PostgreSQL 的数据库连接支持,同时网站的静态资源均已嵌入到可执行程序中,这一点和曾经的 Gogs 有所不同。
|
||||
|
||||
```
|
||||
wget -O gitea https://dl.gitea.io/gitea/{{< version >}}/gitea-{{< version >}}-linux-amd64
|
||||
**目录**
|
||||
|
||||
{{< toc >}}
|
||||
|
||||
## 下载
|
||||
|
||||
你可以从 [下载页面](https://dl.gitea.com/gitea/) 选择对应平台的二进制文件。
|
||||
|
||||
### 选择架构
|
||||
|
||||
- **对于 Linux**,`linux-amd64` 适用于 64-bit 的 Intel/AMD 平台。更多架构包含 `arm64` (Raspberry PI 4),`386` (32-bit),`arm-5` 以及 `arm-6`。
|
||||
|
||||
- **对于 Windows**,`windows-4.0-amd64` 适用于 64-bit 的 Intel/AMD 平台,`386` 适用于 32-bit 的 Intel/AMD 平台。(提示:`gogit-windows` 版本内建了 gogit 可能缓解在旧的 Windows 平台上 Go 程序调用 git 子程序时面临的 [性能问题](https://github.com/go-gitea/gitea/pull/15482))
|
||||
|
||||
- **对于 macOS**,`darwin-arm64` 适用于 Apple Silicon 架构,`darwin-amd64` 适用于 Intel 架构.
|
||||
|
||||
- **对于 FreeBSD**,`freebsd12-amd64` 适用于 64-bit 的 Intel/AMD 平台。
|
||||
|
||||
### 使用 wget 下载
|
||||
|
||||
使用以下命令下载适用于 64-bit Linux 平台的二进制文件。
|
||||
|
||||
```sh
|
||||
wget -O gitea https://dl.gitea.com/gitea/{{< version >}}/gitea-{{< version >}}-linux-amd64
|
||||
chmod +x gitea
|
||||
```
|
||||
|
||||
## 测试
|
||||
## 验证 GPG 签名
|
||||
|
||||
在执行了以上步骤之后,你将会获得 `gitea` 的二进制文件,在你复制到部署的机器之前可以先测试一下。在命令行执行完后,你可以 `Ctrl + C` 关掉程序。
|
||||
Gitea 对打包的二进制文件使用 [GPG密钥](https://keys.openpgp.org/search?q=teabot%40gitea.io) 签名以防止篡改。
|
||||
请根据对应文件名 `.asc` 中包含的校验码检验文件的一致性。
|
||||
|
||||
```
|
||||
./gitea web
|
||||
```sh
|
||||
gpg --keyserver keys.openpgp.org --recv 7C9E68152594688862D62AF62D9AE806EC1592E2
|
||||
gpg --verify gitea-{{< version >}}-linux-amd64.asc gitea-{{< version >}}-linux-amd64
|
||||
```
|
||||
|
||||
## 需要帮助?
|
||||
校验正确时的信息为 `Good signature from "Teabot <teabot@gitea.io>"`。
|
||||
校验错误时的信息为 `This key is not certified with a trusted signature!`。
|
||||
|
||||
## 服务器设置
|
||||
|
||||
**提示:** `GITEA_WORK_DIR` 表示 Gitea 工作的路径。以下路径可以通过 [环境变量]({{< relref "doc/advanced/environment-variables.zh-cn.md" >}}) 初始化。
|
||||
|
||||
### 准备环境
|
||||
|
||||
检查是否安装 Git。要求 Git 版本 >= 2.0。
|
||||
|
||||
```sh
|
||||
git --version
|
||||
```
|
||||
|
||||
创建用户(推荐使用名称 `git`)
|
||||
|
||||
```sh
|
||||
adduser \
|
||||
--system \
|
||||
--shell /bin/bash \
|
||||
--gecos 'Git Version Control' \
|
||||
--group \
|
||||
--disabled-password \
|
||||
--home /home/git \
|
||||
git
|
||||
```
|
||||
|
||||
### 创建工作路径
|
||||
|
||||
```sh
|
||||
mkdir -p /var/lib/gitea/{custom,data,log}
|
||||
chown -R git:git /var/lib/gitea/
|
||||
chmod -R 750 /var/lib/gitea/
|
||||
mkdir /etc/gitea
|
||||
chown root:git /etc/gitea
|
||||
chmod 770 /etc/gitea
|
||||
```
|
||||
|
||||
> **注意:** 为了让 Web 安装程序可以写入配置文件,我们临时为 `/etc/gitea` 路径授予了组外用户 `git` 写入权限。建议在安装结束后将配置文件的权限设置为只读。
|
||||
>
|
||||
> ```sh
|
||||
> chmod 750 /etc/gitea
|
||||
> chmod 640 /etc/gitea/app.ini
|
||||
> ```
|
||||
|
||||
如果您不希望通过 Web 安装程序创建配置文件,可以将配置文件设置为仅供 Gitea 用户只读(owner/group `root:git`, mode `0640`)并手工创建配置文件:
|
||||
|
||||
- 设置 `INSTALL_LOCK=true` 关闭安装界面
|
||||
- 手动配置数据库连接参数
|
||||
- 使用 `gitea generate secret` 创建 `SECRET_KEY` 和 `INTERNAL_TOKEN`
|
||||
- 提供所有必要的密钥
|
||||
|
||||
详情参考 [命令行文档](/zh-cn/command-line/) 中有关 `gitea generate secret` 的内容。
|
||||
|
||||
### 配置 Gitea 工作路径
|
||||
|
||||
**提示:** 如果使用 Systemd 管理 Gitea 的 Linux 服务,你可以采用 `WorkingDirectory` 参数来配置工作路径。 否则,使用环境变量 `GITEA_WORK_DIR` 来明确指出程序工作和数据存放路径。
|
||||
|
||||
```sh
|
||||
export GITEA_WORK_DIR=/var/lib/gitea/
|
||||
```
|
||||
|
||||
### 复制二进制文件到全局位置
|
||||
|
||||
```sh
|
||||
cp gitea /usr/local/bin/gitea
|
||||
```
|
||||
|
||||
## 运行 Gitea
|
||||
|
||||
完成以上步骤后,可以通过两种方式运行 Gitea:
|
||||
|
||||
### 1. 创建服务自动启动 Gitea(推荐)
|
||||
|
||||
学习创建 [Linux 服务]({{< relref "run-as-service-in-ubuntu.zh-cn.md" >}})
|
||||
|
||||
### 2. 通过命令行终端运行
|
||||
|
||||
```sh
|
||||
GITEA_WORK_DIR=/var/lib/gitea/ /usr/local/bin/gitea web -c /etc/gitea/app.ini
|
||||
```
|
||||
|
||||
## 升级到最新版本
|
||||
|
||||
您可以通过停止程序,替换 `/usr/local/bin/gitea` 并重启来更新到新版本。直接替换可执行程序时不要更改或使用新的文件名称,以避免数据出错。
|
||||
|
||||
建议您在更新之前进行[备份]({{< relref "doc/usage/backup-and-restore.zh-cn.md" >}})。
|
||||
|
||||
### 1. 使用 systemd 重新启动 Gitea(推荐)
|
||||
|
||||
我们建议使用 systemd 作为服务管理器,使用 `systemctl restart gitea` 安全地重启程序。
|
||||
|
||||
### 2. 非 systemd 重启方法
|
||||
|
||||
使用 SIGHUP 信号关闭程序:查询到 Gitea 程序的 PID,使用 `kill -1 $GITEA_PID`,或者 `killall -1 gitea`。
|
||||
|
||||
更优雅的停止指令可能包括 `kill $GITEA_PID` 或者 `killall gitea`。
|
||||
|
||||
**提示:** 我们不建议使用 SIGKILL 信号(`-9`),这会强制停止 Gitea 程序,但不会正确关闭队列、索引器等任务。
|
||||
|
||||
请参阅下面的疑难解答说明,以在Gitea版本更新后修复损坏的仓库。
|
||||
|
||||
## 排查故障
|
||||
|
||||
> 更多经验总结,请参考英文版 [Troubleshooting](/en-us/install-from-binary/#troubleshooting)
|
||||
|
||||
如果从本页中没有找到你需要的内容,请访问 [帮助页面]({{< relref "seek-help.zh-cn.md" >}})
|
||||
|
||||
@@ -19,7 +19,7 @@ Gitea provides automatically updated Docker images within its Docker Hub organiz
|
||||
possible to always use the latest stable tag or to use another service that handles updating
|
||||
Docker images.
|
||||
|
||||
The rootless image use Gitea internal SSH to provide Git protocol and doesn't support OpenSSH.
|
||||
The rootless image uses Gitea internal SSH to provide Git protocol and doesn't support OpenSSH.
|
||||
|
||||
This reference setup guides users through the setup based on `docker-compose`, but the installation
|
||||
of `docker-compose` is out of scope of this documentation. To install `docker-compose` itself, follow
|
||||
|
||||
@@ -137,6 +137,8 @@ Admin operations:
|
||||
- `--group-claim-name`: Claim name providing group names for this source. (Optional)
|
||||
- `--admin-group`: Group Claim value for administrator users. (Optional)
|
||||
- `--restricted-group`: Group Claim value for restricted users. (Optional)
|
||||
- `--group-team-map`: JSON mapping between groups and org teams. (Optional)
|
||||
- `--group-team-map-removal`: Activate automatic team membership removal depending on groups. (Optional)
|
||||
- Examples:
|
||||
- `gitea admin auth add-oauth --name external-github --provider github --key OBTAIN_FROM_SOURCE --secret OBTAIN_FROM_SOURCE`
|
||||
- `update-oauth`:
|
||||
|
||||
@@ -223,18 +223,24 @@ func (a *Action) GetRepoAbsoluteLink() string {
|
||||
return setting.AppURL + url.PathEscape(a.GetRepoUserName()) + "/" + url.PathEscape(a.GetRepoName())
|
||||
}
|
||||
|
||||
// GetCommentLink returns link to action comment.
|
||||
func (a *Action) GetCommentLink() string {
|
||||
return a.getCommentLink(db.DefaultContext)
|
||||
// GetCommentHTMLURL returns link to action comment.
|
||||
func (a *Action) GetCommentHTMLURL() string {
|
||||
return a.getCommentHTMLURL(db.DefaultContext)
|
||||
}
|
||||
|
||||
func (a *Action) getCommentLink(ctx context.Context) string {
|
||||
func (a *Action) loadComment(ctx context.Context) (err error) {
|
||||
if a.CommentID == 0 || a.Comment != nil {
|
||||
return nil
|
||||
}
|
||||
a.Comment, err = issues_model.GetCommentByID(ctx, a.CommentID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *Action) getCommentHTMLURL(ctx context.Context) string {
|
||||
if a == nil {
|
||||
return "#"
|
||||
}
|
||||
if a.Comment == nil && a.CommentID != 0 {
|
||||
a.Comment, _ = issues_model.GetCommentByID(ctx, a.CommentID)
|
||||
}
|
||||
_ = a.loadComment(ctx)
|
||||
if a.Comment != nil {
|
||||
return a.Comment.HTMLURL()
|
||||
}
|
||||
@@ -260,6 +266,41 @@ func (a *Action) getCommentLink(ctx context.Context) string {
|
||||
return issue.HTMLURL()
|
||||
}
|
||||
|
||||
// GetCommentLink returns link to action comment.
|
||||
func (a *Action) GetCommentLink() string {
|
||||
return a.getCommentLink(db.DefaultContext)
|
||||
}
|
||||
|
||||
func (a *Action) getCommentLink(ctx context.Context) string {
|
||||
if a == nil {
|
||||
return "#"
|
||||
}
|
||||
_ = a.loadComment(ctx)
|
||||
if a.Comment != nil {
|
||||
return a.Comment.Link()
|
||||
}
|
||||
if len(a.GetIssueInfos()) == 0 {
|
||||
return "#"
|
||||
}
|
||||
// Return link to issue
|
||||
issueIDString := a.GetIssueInfos()[0]
|
||||
issueID, err := strconv.ParseInt(issueIDString, 10, 64)
|
||||
if err != nil {
|
||||
return "#"
|
||||
}
|
||||
|
||||
issue, err := issues_model.GetIssueByID(ctx, issueID)
|
||||
if err != nil {
|
||||
return "#"
|
||||
}
|
||||
|
||||
if err = issue.LoadRepo(ctx); err != nil {
|
||||
return "#"
|
||||
}
|
||||
|
||||
return issue.Link()
|
||||
}
|
||||
|
||||
// GetBranch returns the action's repository branch.
|
||||
func (a *Action) GetBranch() string {
|
||||
return strings.TrimPrefix(a.RefName, git.BranchPrefix)
|
||||
|
||||
@@ -36,7 +36,7 @@ func TestAction_GetRepoLink(t *testing.T) {
|
||||
expected := path.Join(setting.AppSubURL, owner.Name, repo.Name)
|
||||
assert.Equal(t, expected, action.GetRepoLink())
|
||||
assert.Equal(t, repo.HTMLURL(), action.GetRepoAbsoluteLink())
|
||||
assert.Equal(t, comment.HTMLURL(), action.GetCommentLink())
|
||||
assert.Equal(t, comment.HTMLURL(), action.GetCommentHTMLURL())
|
||||
}
|
||||
|
||||
func TestGetFeeds(t *testing.T) {
|
||||
|
||||
@@ -459,6 +459,22 @@ func (n *Notification) HTMLURL() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Link formats a relative URL-string to the notification
|
||||
func (n *Notification) Link() string {
|
||||
switch n.Source {
|
||||
case NotificationSourceIssue, NotificationSourcePullRequest:
|
||||
if n.Comment != nil {
|
||||
return n.Comment.Link()
|
||||
}
|
||||
return n.Issue.Link()
|
||||
case NotificationSourceCommit:
|
||||
return n.Repository.Link() + "/commit/" + url.PathEscape(n.CommitID)
|
||||
case NotificationSourceRepository:
|
||||
return n.Repository.Link()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// APIURL formats a URL-string to the notification
|
||||
func (n *Notification) APIURL() string {
|
||||
return setting.AppURL + "api/v1/notifications/threads/" + strconv.FormatInt(n.ID, 10)
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
const (
|
||||
// DefaultAvatarClass is the default class of a rendered avatar
|
||||
DefaultAvatarClass = "ui avatar vm"
|
||||
DefaultAvatarClass = "ui avatar gt-vm"
|
||||
// DefaultAvatarPixelSize is the default size in pixels of a rendered avatar
|
||||
DefaultAvatarPixelSize = 28
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
@@ -183,6 +184,31 @@ func DeleteByBean(ctx context.Context, bean interface{}) (int64, error) {
|
||||
return GetEngine(ctx).Delete(bean)
|
||||
}
|
||||
|
||||
// DeleteByID deletes the given bean with the given ID
|
||||
func DeleteByID(ctx context.Context, id int64, bean interface{}) (int64, error) {
|
||||
return GetEngine(ctx).ID(id).NoAutoTime().Delete(bean)
|
||||
}
|
||||
|
||||
// FindIDs finds the IDs for the given table name satisfying the given condition
|
||||
// By passing a different value than "id" for "idCol", you can query for foreign IDs, i.e. the repo IDs which satisfy the condition
|
||||
func FindIDs(ctx context.Context, tableName, idCol string, cond builder.Cond) ([]int64, error) {
|
||||
ids := make([]int64, 0, 10)
|
||||
if err := GetEngine(ctx).Table(tableName).
|
||||
Cols(idCol).
|
||||
Where(cond).
|
||||
Find(&ids); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
// DecrByIDs decreases the given column for entities of the "bean" type with one of the given ids by one
|
||||
// Timestamps of the entities won't be updated
|
||||
func DecrByIDs(ctx context.Context, ids []int64, decrCol string, bean interface{}) error {
|
||||
_, err := GetEngine(ctx).Decr(decrCol).In("id", ids).NoAutoCondition().NoAutoTime().Update(bean)
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteBeans deletes all given beans, beans should contain delete conditions.
|
||||
func DeleteBeans(ctx context.Context, beans ...interface{}) (err error) {
|
||||
e := GetEngine(ctx)
|
||||
|
||||
@@ -37,7 +37,9 @@ func (d *postgresSchemaDriver) Open(name string) (driver.Conn, error) {
|
||||
}
|
||||
schemaValue, _ := driver.String.ConvertValue(setting.Database.Schema)
|
||||
|
||||
if execer, ok := conn.(driver.Execer); ok {
|
||||
// golangci lint is incorrect here - there is no benefit to using driver.ExecerContext here
|
||||
// and in any case pq does not implement it
|
||||
if execer, ok := conn.(driver.Execer); ok { //nolint
|
||||
_, err := execer.Exec(`SELECT set_config(
|
||||
'search_path',
|
||||
$1 || ',' || current_setting('search_path'),
|
||||
@@ -61,7 +63,8 @@ func (d *postgresSchemaDriver) Open(name string) (driver.Conn, error) {
|
||||
|
||||
// driver.String.ConvertValue will never return err for string
|
||||
|
||||
_, err = stmt.Exec([]driver.Value{schemaValue})
|
||||
// golangci lint is incorrect here - there is no benefit to using stmt.ExecWithContext here
|
||||
_, err = stmt.Exec([]driver.Value{schemaValue}) //nolint
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return nil, err
|
||||
|
||||
@@ -391,21 +391,40 @@ func (c *Comment) HTMLURL() string {
|
||||
log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
|
||||
return ""
|
||||
}
|
||||
return c.Issue.HTMLURL() + c.hashLink()
|
||||
}
|
||||
|
||||
// Link formats a relative URL-string to the issue-comment
|
||||
func (c *Comment) Link() string {
|
||||
err := c.LoadIssue(db.DefaultContext)
|
||||
if err != nil { // Silently dropping errors :unamused:
|
||||
log.Error("LoadIssue(%d): %v", c.IssueID, err)
|
||||
return ""
|
||||
}
|
||||
err = c.Issue.LoadRepo(db.DefaultContext)
|
||||
if err != nil { // Silently dropping errors :unamused:
|
||||
log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
|
||||
return ""
|
||||
}
|
||||
return c.Issue.Link() + c.hashLink()
|
||||
}
|
||||
|
||||
func (c *Comment) hashLink() string {
|
||||
if c.Type == CommentTypeCode {
|
||||
if c.ReviewID == 0 {
|
||||
return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
|
||||
return "/files#" + c.HashTag()
|
||||
}
|
||||
if c.Review == nil {
|
||||
if err := c.LoadReview(); err != nil {
|
||||
log.Warn("LoadReview(%d): %v", c.ReviewID, err)
|
||||
return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
|
||||
return "/files#" + c.HashTag()
|
||||
}
|
||||
}
|
||||
if c.Review.Type <= ReviewTypePending {
|
||||
return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
|
||||
return "/files#" + c.HashTag()
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s#%s", c.Issue.HTMLURL(), c.HashTag())
|
||||
return "#" + c.HashTag()
|
||||
}
|
||||
|
||||
// APIURL formats a API-string to the issue-comment
|
||||
@@ -708,8 +727,8 @@ func (c *Comment) UnsignedLine() uint64 {
|
||||
return uint64(c.Line)
|
||||
}
|
||||
|
||||
// CodeCommentURL returns the url to a comment in code
|
||||
func (c *Comment) CodeCommentURL() string {
|
||||
// CodeCommentLink returns the url to a comment in code
|
||||
func (c *Comment) CodeCommentLink() string {
|
||||
err := c.LoadIssue(db.DefaultContext)
|
||||
if err != nil { // Silently dropping errors :unamused:
|
||||
log.Error("LoadIssue(%d): %v", c.IssueID, err)
|
||||
@@ -720,7 +739,7 @@ func (c *Comment) CodeCommentURL() string {
|
||||
log.Error("loadRepo(%d): %v", c.Issue.RepoID, err)
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("%s/files#%s", c.Issue.HTMLURL(), c.HashTag())
|
||||
return fmt.Sprintf("%s/files#%s", c.Issue.Link(), c.HashTag())
|
||||
}
|
||||
|
||||
// LoadPushCommits Load push commits
|
||||
|
||||
@@ -419,7 +419,7 @@ func (issue *Issue) HTMLURL() string {
|
||||
return fmt.Sprintf("%s/%s/%d", issue.Repo.HTMLURL(), path, issue.Index)
|
||||
}
|
||||
|
||||
// Link returns the Link URL to this issue.
|
||||
// Link returns the issue's relative URL.
|
||||
func (issue *Issue) Link() string {
|
||||
var path string
|
||||
if issue.IsPull {
|
||||
|
||||
@@ -277,26 +277,26 @@ func CommentTypeIsRef(t CommentType) bool {
|
||||
return t == CommentTypeCommentRef || t == CommentTypePullRef || t == CommentTypeIssueRef
|
||||
}
|
||||
|
||||
// RefCommentHTMLURL returns the HTML URL for the comment that created this reference
|
||||
func (c *Comment) RefCommentHTMLURL() string {
|
||||
// RefCommentLink returns the relative URL for the comment that created this reference
|
||||
func (c *Comment) RefCommentLink() string {
|
||||
// Edge case for when the reference is inside the title or the description of the referring issue
|
||||
if c.RefCommentID == 0 {
|
||||
return c.RefIssueHTMLURL()
|
||||
return c.RefIssueLink()
|
||||
}
|
||||
if err := c.LoadRefComment(); err != nil { // Silently dropping errors :unamused:
|
||||
log.Error("LoadRefComment(%d): %v", c.RefCommentID, err)
|
||||
return ""
|
||||
}
|
||||
return c.RefComment.HTMLURL()
|
||||
return c.RefComment.Link()
|
||||
}
|
||||
|
||||
// RefIssueHTMLURL returns the HTML URL of the issue where this reference was created
|
||||
func (c *Comment) RefIssueHTMLURL() string {
|
||||
// RefIssueLink returns the relative URL of the issue where this reference was created
|
||||
func (c *Comment) RefIssueLink() string {
|
||||
if err := c.LoadRefIssue(); err != nil { // Silently dropping errors :unamused:
|
||||
log.Error("LoadRefIssue(%d): %v", c.RefCommentID, err)
|
||||
return ""
|
||||
}
|
||||
return c.RefIssue.HTMLURL()
|
||||
return c.RefIssue.Link()
|
||||
}
|
||||
|
||||
// RefIssueTitle returns the title of the issue where this reference was created
|
||||
|
||||
@@ -759,8 +759,8 @@ func GetPullRequestsByHeadBranch(ctx context.Context, headBranch string, headRep
|
||||
return prs, nil
|
||||
}
|
||||
|
||||
// GetBaseBranchHTMLURL returns the HTML URL of the base branch
|
||||
func (pr *PullRequest) GetBaseBranchHTMLURL() string {
|
||||
// GetBaseBranchLink returns the relative URL of the base branch
|
||||
func (pr *PullRequest) GetBaseBranchLink() string {
|
||||
if err := pr.LoadBaseRepo(db.DefaultContext); err != nil {
|
||||
log.Error("LoadBaseRepo: %v", err)
|
||||
return ""
|
||||
@@ -768,11 +768,11 @@ func (pr *PullRequest) GetBaseBranchHTMLURL() string {
|
||||
if pr.BaseRepo == nil {
|
||||
return ""
|
||||
}
|
||||
return pr.BaseRepo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(pr.BaseBranch)
|
||||
return pr.BaseRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pr.BaseBranch)
|
||||
}
|
||||
|
||||
// GetHeadBranchHTMLURL returns the HTML URL of the head branch
|
||||
func (pr *PullRequest) GetHeadBranchHTMLURL() string {
|
||||
// GetHeadBranchLink returns the relative URL of the head branch
|
||||
func (pr *PullRequest) GetHeadBranchLink() string {
|
||||
if pr.Flow == PullRequestFlowAGit {
|
||||
return ""
|
||||
}
|
||||
@@ -784,7 +784,7 @@ func (pr *PullRequest) GetHeadBranchHTMLURL() string {
|
||||
if pr.HeadRepo == nil {
|
||||
return ""
|
||||
}
|
||||
return pr.HeadRepo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(pr.HeadBranch)
|
||||
return pr.HeadRepo.Link() + "/src/branch/" + util.PathEscapeSegments(pr.HeadBranch)
|
||||
}
|
||||
|
||||
// UpdateAllowEdits update if PR can be edited from maintainers
|
||||
|
||||
@@ -455,6 +455,8 @@ var migrations = []Migration{
|
||||
NewMigration("Add scope for access_token", v1_19.AddScopeForAccessTokens),
|
||||
// v240 -> v241
|
||||
NewMigration("Add actions tables", v1_19.AddActionsTables),
|
||||
// v241 -> v242
|
||||
NewMigration("Add card_type column to project table", v1_19.AddCardTypeToProjectTable),
|
||||
// to modify later
|
||||
NewMigration("add size limit on repository", v1_19.AddSizeLimitOnRepo),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_19 //nolint
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// AddCardTypeToProjectTable: add CardType column, setting existing rows to CardTypeTextOnly
|
||||
func AddCardTypeToProjectTable(x *xorm.Engine) error {
|
||||
type Project struct {
|
||||
CardType int `xorm:"NOT NULL DEFAULT 0"`
|
||||
}
|
||||
|
||||
return x.Sync(new(Project))
|
||||
}
|
||||
@@ -110,22 +110,14 @@ func (org *Organization) CanCreateOrgRepo(uid int64) (bool, error) {
|
||||
return CanCreateOrgRepo(db.DefaultContext, org.ID, uid)
|
||||
}
|
||||
|
||||
func (org *Organization) getTeam(ctx context.Context, name string) (*Team, error) {
|
||||
// GetTeam returns named team of organization.
|
||||
func (org *Organization) GetTeam(ctx context.Context, name string) (*Team, error) {
|
||||
return GetTeam(ctx, org.ID, name)
|
||||
}
|
||||
|
||||
// GetTeam returns named team of organization.
|
||||
func (org *Organization) GetTeam(name string) (*Team, error) {
|
||||
return org.getTeam(db.DefaultContext, name)
|
||||
}
|
||||
|
||||
func (org *Organization) getOwnerTeam(ctx context.Context) (*Team, error) {
|
||||
return org.getTeam(ctx, OwnerTeamName)
|
||||
}
|
||||
|
||||
// GetOwnerTeam returns owner team of organization.
|
||||
func (org *Organization) GetOwnerTeam() (*Team, error) {
|
||||
return org.getOwnerTeam(db.DefaultContext)
|
||||
func (org *Organization) GetOwnerTeam(ctx context.Context) (*Team, error) {
|
||||
return org.GetTeam(ctx, OwnerTeamName)
|
||||
}
|
||||
|
||||
// FindOrgTeams returns all teams of a given organization
|
||||
@@ -342,7 +334,7 @@ func CreateOrganization(org *Organization, owner *user_model.User) (err error) {
|
||||
}
|
||||
|
||||
// GetOrgByName returns organization by given name.
|
||||
func GetOrgByName(name string) (*Organization, error) {
|
||||
func GetOrgByName(ctx context.Context, name string) (*Organization, error) {
|
||||
if len(name) == 0 {
|
||||
return nil, ErrOrgNotExist{0, name}
|
||||
}
|
||||
@@ -350,7 +342,7 @@ func GetOrgByName(name string) (*Organization, error) {
|
||||
LowerName: strings.ToLower(name),
|
||||
Type: user_model.UserTypeOrganization,
|
||||
}
|
||||
has, err := db.GetEngine(db.DefaultContext).Get(u)
|
||||
has, err := db.GetEngine(ctx).Get(u)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
|
||||
@@ -61,28 +61,28 @@ func TestUser_IsOrgMember(t *testing.T) {
|
||||
func TestUser_GetTeam(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
team, err := org.GetTeam("team1")
|
||||
team, err := org.GetTeam(db.DefaultContext, "team1")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, org.ID, team.OrgID)
|
||||
assert.Equal(t, "team1", team.LowerName)
|
||||
|
||||
_, err = org.GetTeam("does not exist")
|
||||
_, err = org.GetTeam(db.DefaultContext, "does not exist")
|
||||
assert.True(t, organization.IsErrTeamNotExist(err))
|
||||
|
||||
nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2})
|
||||
_, err = nonOrg.GetTeam("team")
|
||||
_, err = nonOrg.GetTeam(db.DefaultContext, "team")
|
||||
assert.True(t, organization.IsErrTeamNotExist(err))
|
||||
}
|
||||
|
||||
func TestUser_GetOwnerTeam(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
org := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 3})
|
||||
team, err := org.GetOwnerTeam()
|
||||
team, err := org.GetOwnerTeam(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, org.ID, team.OrgID)
|
||||
|
||||
nonOrg := unittest.AssertExistsAndLoadBean(t, &organization.Organization{ID: 2})
|
||||
_, err = nonOrg.GetOwnerTeam()
|
||||
_, err = nonOrg.GetOwnerTeam(db.DefaultContext)
|
||||
assert.True(t, organization.IsErrTeamNotExist(err))
|
||||
}
|
||||
|
||||
@@ -115,15 +115,15 @@ func TestUser_GetMembers(t *testing.T) {
|
||||
func TestGetOrgByName(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
org, err := organization.GetOrgByName("user3")
|
||||
org, err := organization.GetOrgByName(db.DefaultContext, "user3")
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 3, org.ID)
|
||||
assert.Equal(t, "user3", org.Name)
|
||||
|
||||
_, err = organization.GetOrgByName("user2") // user2 is an individual
|
||||
_, err = organization.GetOrgByName(db.DefaultContext, "user2") // user2 is an individual
|
||||
assert.True(t, organization.IsErrOrgNotExist(err))
|
||||
|
||||
_, err = organization.GetOrgByName("") // corner case
|
||||
_, err = organization.GetOrgByName(db.DefaultContext, "") // corner case
|
||||
assert.True(t, organization.IsErrOrgNotExist(err))
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ type PackageFileDescriptor struct {
|
||||
|
||||
// PackageWebLink returns the package web link
|
||||
func (pd *PackageDescriptor) PackageWebLink() string {
|
||||
return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HTMLURL(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName))
|
||||
return fmt.Sprintf("%s/-/packages/%s/%s", pd.Owner.HomeLink(), string(pd.Package.Type), url.PathEscape(pd.Package.LowerName))
|
||||
}
|
||||
|
||||
// FullWebLink returns the package version web link
|
||||
|
||||
@@ -19,6 +19,9 @@ type (
|
||||
// BoardType is used to represent a project board type
|
||||
BoardType uint8
|
||||
|
||||
// CardType is used to represent a project board card type
|
||||
CardType uint8
|
||||
|
||||
// BoardList is a list of all project boards in a repository
|
||||
BoardList []*Board
|
||||
)
|
||||
@@ -34,6 +37,14 @@ const (
|
||||
BoardTypeBugTriage
|
||||
)
|
||||
|
||||
const (
|
||||
// CardTypeTextOnly is a project board card type that is text only
|
||||
CardTypeTextOnly CardType = iota
|
||||
|
||||
// CardTypeImagesAndText is a project board card type that has images and text
|
||||
CardTypeImagesAndText
|
||||
)
|
||||
|
||||
// BoardColorPattern is a regexp witch can validate BoardColor
|
||||
var BoardColorPattern = regexp.MustCompile("^#[0-9a-fA-F]{6}$")
|
||||
|
||||
@@ -85,6 +96,16 @@ func IsBoardTypeValid(p BoardType) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsCardTypeValid checks if the project board card type is valid
|
||||
func IsCardTypeValid(p CardType) bool {
|
||||
switch p {
|
||||
case CardTypeTextOnly, CardTypeImagesAndText:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func createBoardsForProjectsType(ctx context.Context, project *Project) error {
|
||||
var items []string
|
||||
|
||||
|
||||
@@ -19,12 +19,18 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
// ProjectsConfig is used to identify the type of board that is being created
|
||||
ProjectsConfig struct {
|
||||
// BoardConfig is used to identify the type of board that is being created
|
||||
BoardConfig struct {
|
||||
BoardType BoardType
|
||||
Translation string
|
||||
}
|
||||
|
||||
// CardConfig is used to identify the type of board card that is being used
|
||||
CardConfig struct {
|
||||
CardType CardType
|
||||
Translation string
|
||||
}
|
||||
|
||||
// Type is used to identify the type of project in question and ownership
|
||||
Type uint8
|
||||
)
|
||||
@@ -91,6 +97,7 @@ type Project struct {
|
||||
CreatorID int64 `xorm:"NOT NULL"`
|
||||
IsClosed bool `xorm:"INDEX"`
|
||||
BoardType BoardType
|
||||
CardType CardType
|
||||
Type Type
|
||||
|
||||
RenderedContent string `xorm:"-"`
|
||||
@@ -116,6 +123,7 @@ func (p *Project) LoadRepo(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Link returns the project's relative URL.
|
||||
func (p *Project) Link() string {
|
||||
if p.OwnerID > 0 {
|
||||
err := p.LoadOwner(db.DefaultContext)
|
||||
@@ -144,15 +152,23 @@ func init() {
|
||||
db.RegisterModel(new(Project))
|
||||
}
|
||||
|
||||
// GetProjectsConfig retrieves the types of configurations projects could have
|
||||
func GetProjectsConfig() []ProjectsConfig {
|
||||
return []ProjectsConfig{
|
||||
// GetBoardConfig retrieves the types of configurations project boards could have
|
||||
func GetBoardConfig() []BoardConfig {
|
||||
return []BoardConfig{
|
||||
{BoardTypeNone, "repo.projects.type.none"},
|
||||
{BoardTypeBasicKanban, "repo.projects.type.basic_kanban"},
|
||||
{BoardTypeBugTriage, "repo.projects.type.bug_triage"},
|
||||
}
|
||||
}
|
||||
|
||||
// GetCardConfig retrieves the types of configurations project board cards could have
|
||||
func GetCardConfig() []CardConfig {
|
||||
return []CardConfig{
|
||||
{CardTypeTextOnly, "repo.projects.card_type.text_only"},
|
||||
{CardTypeImagesAndText, "repo.projects.card_type.images_and_text"},
|
||||
}
|
||||
}
|
||||
|
||||
// IsTypeValid checks if a project type is valid
|
||||
func IsTypeValid(p Type) bool {
|
||||
switch p {
|
||||
@@ -236,6 +252,10 @@ func NewProject(p *Project) error {
|
||||
p.BoardType = BoardTypeNone
|
||||
}
|
||||
|
||||
if !IsCardTypeValid(p.CardType) {
|
||||
p.CardType = CardTypeTextOnly
|
||||
}
|
||||
|
||||
if !IsTypeValid(p.Type) {
|
||||
return util.NewInvalidArgumentErrorf("project type is not valid")
|
||||
}
|
||||
@@ -279,9 +299,14 @@ func GetProjectByID(ctx context.Context, id int64) (*Project, error) {
|
||||
|
||||
// UpdateProject updates project properties
|
||||
func UpdateProject(ctx context.Context, p *Project) error {
|
||||
if !IsCardTypeValid(p.CardType) {
|
||||
p.CardType = CardTypeTextOnly
|
||||
}
|
||||
|
||||
_, err := db.GetEngine(ctx).ID(p.ID).Cols(
|
||||
"title",
|
||||
"description",
|
||||
"card_type",
|
||||
).Update(p)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ func TestProject(t *testing.T) {
|
||||
project := &Project{
|
||||
Type: TypeRepository,
|
||||
BoardType: BoardTypeBasicKanban,
|
||||
CardType: CardTypeTextOnly,
|
||||
Title: "New Project",
|
||||
RepoID: 1,
|
||||
CreatedUnix: timeutil.TimeStampNow(),
|
||||
|
||||
@@ -132,6 +132,21 @@ func GetAttachmentsByIssueID(ctx context.Context, issueID int64) ([]*Attachment,
|
||||
return attachments, db.GetEngine(ctx).Where("issue_id = ? AND comment_id = 0", issueID).Find(&attachments)
|
||||
}
|
||||
|
||||
// GetAttachmentsByIssueIDImagesLatest returns the latest image attachments of an issue.
|
||||
func GetAttachmentsByIssueIDImagesLatest(ctx context.Context, issueID int64) ([]*Attachment, error) {
|
||||
attachments := make([]*Attachment, 0, 5)
|
||||
return attachments, db.GetEngine(ctx).Where(`issue_id = ? AND (name like '%.apng'
|
||||
OR name like '%.avif'
|
||||
OR name like '%.bmp'
|
||||
OR name like '%.gif'
|
||||
OR name like '%.jpg'
|
||||
OR name like '%.jpeg'
|
||||
OR name like '%.jxl'
|
||||
OR name like '%.png'
|
||||
OR name like '%.svg'
|
||||
OR name like '%.webp')`, issueID).Desc("comment_id").Limit(5).Find(&attachments)
|
||||
}
|
||||
|
||||
// GetAttachmentsByCommentID returns all attachments if comment by given ID.
|
||||
func GetAttachmentsByCommentID(ctx context.Context, commentID int64) ([]*Attachment, error) {
|
||||
attachments := make([]*Attachment, 0, 10)
|
||||
|
||||
@@ -130,6 +130,11 @@ func (r *Release) HTMLURL() string {
|
||||
return r.Repo.HTMLURL() + "/releases/tag/" + util.PathEscapeSegments(r.TagName)
|
||||
}
|
||||
|
||||
// Link the relative url for a release on the web UI. release must have attributes loaded
|
||||
func (r *Release) Link() string {
|
||||
return r.Repo.Link() + "/releases/tag/" + util.PathEscapeSegments(r.TagName)
|
||||
}
|
||||
|
||||
// IsReleaseExist returns true if release with given tag name already exists.
|
||||
func IsReleaseExist(ctx context.Context, repoID int64, tagName string) (bool, error) {
|
||||
if len(tagName) == 0 {
|
||||
|
||||
+2
-2
@@ -275,7 +275,7 @@ func (repo *Repository) CommitLink(commitID string) (result string) {
|
||||
if commitID == "" || commitID == "0000000000000000000000000000000000000000" {
|
||||
result = ""
|
||||
} else {
|
||||
result = repo.HTMLURL() + "/commit/" + url.PathEscape(commitID)
|
||||
result = repo.Link() + "/commit/" + url.PathEscape(commitID)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -481,7 +481,7 @@ func (repo *Repository) RepoPath() string {
|
||||
return RepoPath(repo.OwnerName, repo.Name)
|
||||
}
|
||||
|
||||
// Link returns the repository link
|
||||
// Link returns the repository relative url
|
||||
func (repo *Repository) Link() string {
|
||||
return setting.AppSubURL + "/" + url.PathEscape(repo.OwnerName) + "/" + url.PathEscape(repo.Name)
|
||||
}
|
||||
|
||||
@@ -125,6 +125,7 @@ type PullRequestsConfig struct {
|
||||
AllowRebaseUpdate bool
|
||||
DefaultDeleteBranchAfterMerge bool
|
||||
DefaultMergeStyle MergeStyle
|
||||
DefaultAllowMaintainerEdit bool
|
||||
}
|
||||
|
||||
// FromDB fills up a PullRequestsConfig from serialized format.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
func UnmarshalGroupTeamMapping(raw string) (map[string]map[string][]string, error) {
|
||||
groupTeamMapping := make(map[string]map[string][]string)
|
||||
if raw == "" {
|
||||
return groupTeamMapping, nil
|
||||
}
|
||||
err := json.Unmarshal([]byte(raw), &groupTeamMapping)
|
||||
if err != nil {
|
||||
log.Error("Failed to unmarshal group team mapping: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
return groupTeamMapping, nil
|
||||
}
|
||||
@@ -6,7 +6,6 @@ package charset
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@@ -20,12 +19,16 @@ import (
|
||||
var defaultWordRegexp = regexp.MustCompile(`(-?\d*\.\d\w*)|([^\` + "`" + `\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s\x00-\x1f]+)`)
|
||||
|
||||
func NewEscapeStreamer(locale translation.Locale, next HTMLStreamer, allowed ...rune) HTMLStreamer {
|
||||
allowedM := make(map[rune]bool, len(allowed))
|
||||
for _, v := range allowed {
|
||||
allowedM[v] = true
|
||||
}
|
||||
return &escapeStreamer{
|
||||
escaped: &EscapeStatus{},
|
||||
PassthroughHTMLStreamer: *NewPassthroughStreamer(next),
|
||||
locale: locale,
|
||||
ambiguousTables: AmbiguousTablesForLocale(locale),
|
||||
allowed: allowed,
|
||||
allowed: allowedM,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +37,7 @@ type escapeStreamer struct {
|
||||
escaped *EscapeStatus
|
||||
locale translation.Locale
|
||||
ambiguousTables []*AmbiguousTable
|
||||
allowed []rune
|
||||
allowed map[rune]bool
|
||||
}
|
||||
|
||||
func (e *escapeStreamer) EscapeStatus() *EscapeStatus {
|
||||
@@ -256,7 +259,7 @@ func (e *escapeStreamer) runeTypes(runes ...rune) (types []runeType, confusables
|
||||
runeCounts.numBrokenRunes++
|
||||
case r == ' ' || r == '\t' || r == '\n':
|
||||
runeCounts.numBasicRunes++
|
||||
case e.isAllowed(r):
|
||||
case e.allowed[r]:
|
||||
if r > 0x7e || r < 0x20 {
|
||||
types[i] = nonBasicASCIIRuneType
|
||||
runeCounts.numNonConfusingNonBasicRunes++
|
||||
@@ -282,16 +285,3 @@ func (e *escapeStreamer) runeTypes(runes ...rune) (types []runeType, confusables
|
||||
}
|
||||
return types, confusables, runeCounts
|
||||
}
|
||||
|
||||
func (e *escapeStreamer) isAllowed(r rune) bool {
|
||||
if len(e.allowed) == 0 {
|
||||
return false
|
||||
}
|
||||
if len(e.allowed) == 1 {
|
||||
return e.allowed[0] == r
|
||||
}
|
||||
|
||||
return sort.Search(len(e.allowed), func(i int) bool {
|
||||
return e.allowed[i] >= r
|
||||
}) >= 0
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
auth_service "code.gitea.io/gitea/services/auth"
|
||||
)
|
||||
|
||||
// APIContext is a specific context for API service
|
||||
@@ -215,35 +214,6 @@ func (ctx *APIContext) CheckForOTP() {
|
||||
}
|
||||
}
|
||||
|
||||
// APIAuth converts auth_service.Auth as a middleware
|
||||
func APIAuth(authMethod auth_service.Method) func(*APIContext) {
|
||||
return func(ctx *APIContext) {
|
||||
// Get user from session if logged in.
|
||||
var err error
|
||||
ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusUnauthorized, "APIAuth", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.Doer != nil {
|
||||
if ctx.Locale.Language() != ctx.Doer.Language {
|
||||
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
|
||||
}
|
||||
ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == auth_service.BasicMethodName
|
||||
ctx.IsSigned = true
|
||||
ctx.Data["IsSigned"] = ctx.IsSigned
|
||||
ctx.Data["SignedUser"] = ctx.Doer
|
||||
ctx.Data["SignedUserID"] = ctx.Doer.ID
|
||||
ctx.Data["SignedUserName"] = ctx.Doer.Name
|
||||
ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
|
||||
} else {
|
||||
ctx.Data["SignedUserID"] = int64(0)
|
||||
ctx.Data["SignedUserName"] = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// APIContexter returns apicontext as middleware
|
||||
func APIContexter() func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
|
||||
@@ -36,7 +36,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
"code.gitea.io/gitea/services/auth"
|
||||
|
||||
"gitea.com/go-chi/cache"
|
||||
"gitea.com/go-chi/session"
|
||||
@@ -659,37 +658,6 @@ func getCsrfOpts() CsrfOptions {
|
||||
}
|
||||
}
|
||||
|
||||
// Auth converts auth.Auth as a middleware
|
||||
func Auth(authMethod auth.Method) func(*Context) {
|
||||
return func(ctx *Context) {
|
||||
var err error
|
||||
ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
|
||||
if err != nil {
|
||||
log.Error("Failed to verify user %v: %v", ctx.Req.RemoteAddr, err)
|
||||
ctx.Error(http.StatusUnauthorized, "Verify")
|
||||
return
|
||||
}
|
||||
if ctx.Doer != nil {
|
||||
if ctx.Locale.Language() != ctx.Doer.Language {
|
||||
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
|
||||
}
|
||||
ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == auth.BasicMethodName
|
||||
ctx.IsSigned = true
|
||||
ctx.Data["IsSigned"] = ctx.IsSigned
|
||||
ctx.Data["SignedUser"] = ctx.Doer
|
||||
ctx.Data["SignedUserID"] = ctx.Doer.ID
|
||||
ctx.Data["SignedUserName"] = ctx.Doer.Name
|
||||
ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
|
||||
} else {
|
||||
ctx.Data["SignedUserID"] = int64(0)
|
||||
ctx.Data["SignedUserName"] = ""
|
||||
|
||||
// ensure the session uid is deleted
|
||||
_ = ctx.Session.Delete("uid")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Contexter initializes a classic context for a request.
|
||||
func Contexter(ctx context.Context) func(next http.Handler) http.Handler {
|
||||
_, rnd := templates.HTMLRenderer(ctx)
|
||||
|
||||
@@ -80,7 +80,7 @@ func HandleOrgAssignment(ctx *Context, args ...bool) {
|
||||
orgName := ctx.Params(":org")
|
||||
|
||||
var err error
|
||||
ctx.Org.Organization, err = organization.GetOrgByName(orgName)
|
||||
ctx.Org.Organization, err = organization.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
if organization.IsErrOrgNotExist(err) {
|
||||
redirectUserID, err := user_model.LookupUserRedirect(orgName)
|
||||
|
||||
@@ -743,9 +743,9 @@ func RepoAssignment(ctx *Context) (cancel context.CancelFunc) {
|
||||
|
||||
if ctx.FormString("go-get") == "1" {
|
||||
ctx.Data["GoGetImport"] = ComposeGoGetImport(owner.Name, repo.Name)
|
||||
prefix := repo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName)
|
||||
ctx.Data["GoDocDirectory"] = prefix + "{/dir}"
|
||||
ctx.Data["GoDocFile"] = prefix + "{/dir}/{file}#L{line}"
|
||||
fullURLPrefix := repo.HTMLURL() + "/src/branch/" + util.PathEscapeSegments(ctx.Repo.BranchName)
|
||||
ctx.Data["GoDocDirectory"] = fullURLPrefix + "{/dir}"
|
||||
ctx.Data["GoDocFile"] = fullURLPrefix + "{/dir}/{file}#L{line}"
|
||||
}
|
||||
return cancel
|
||||
}
|
||||
|
||||
+17
-14
@@ -20,11 +20,12 @@ type BlamePart struct {
|
||||
|
||||
// BlameReader returns part of file blame one by one
|
||||
type BlameReader struct {
|
||||
cmd *Command
|
||||
output io.WriteCloser
|
||||
reader io.ReadCloser
|
||||
done chan error
|
||||
lastSha *string
|
||||
cmd *Command
|
||||
output io.WriteCloser
|
||||
reader io.ReadCloser
|
||||
bufferedReader *bufio.Reader
|
||||
done chan error
|
||||
lastSha *string
|
||||
}
|
||||
|
||||
var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
|
||||
@@ -33,8 +34,6 @@ var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
|
||||
func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
var blamePart *BlamePart
|
||||
|
||||
reader := bufio.NewReader(r.reader)
|
||||
|
||||
if r.lastSha != nil {
|
||||
blamePart = &BlamePart{*r.lastSha, make([]string, 0)}
|
||||
}
|
||||
@@ -44,7 +43,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
var err error
|
||||
|
||||
for err != io.EOF {
|
||||
line, isPrefix, err = reader.ReadLine()
|
||||
line, isPrefix, err = r.bufferedReader.ReadLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return blamePart, err
|
||||
}
|
||||
@@ -66,7 +65,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
r.lastSha = &sha1
|
||||
// need to munch to end of line...
|
||||
for isPrefix {
|
||||
_, isPrefix, err = reader.ReadLine()
|
||||
_, isPrefix, err = r.bufferedReader.ReadLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return blamePart, err
|
||||
}
|
||||
@@ -81,7 +80,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
|
||||
// need to munch to end of line...
|
||||
for isPrefix {
|
||||
_, isPrefix, err = reader.ReadLine()
|
||||
_, isPrefix, err = r.bufferedReader.ReadLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return blamePart, err
|
||||
}
|
||||
@@ -96,6 +95,7 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
// Close BlameReader - don't run NextPart after invoking that
|
||||
func (r *BlameReader) Close() error {
|
||||
err := <-r.done
|
||||
r.bufferedReader = nil
|
||||
_ = r.reader.Close()
|
||||
_ = r.output.Close()
|
||||
return err
|
||||
@@ -126,10 +126,13 @@ func CreateBlameReader(ctx context.Context, repoPath, commitID, file string) (*B
|
||||
done <- err
|
||||
}(cmd, repoPath, stdout, done)
|
||||
|
||||
bufferedReader := bufio.NewReader(reader)
|
||||
|
||||
return &BlameReader{
|
||||
cmd: cmd,
|
||||
output: stdout,
|
||||
reader: reader,
|
||||
done: done,
|
||||
cmd: cmd,
|
||||
output: stdout,
|
||||
reader: reader,
|
||||
bufferedReader: bufferedReader,
|
||||
done: done,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestReadingBlameOutput(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"f32b0a9dfd09a60f616f29158f772cedd89942d2",
|
||||
[]string{},
|
||||
[]string{"", "Do not make any changes to this repo it is used for unit testing"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -163,10 +163,8 @@ func CloneWithArgs(ctx context.Context, args TrustedCmdArgs, from, to string, op
|
||||
|
||||
envs := os.Environ()
|
||||
u, err := url.Parse(from)
|
||||
if err == nil && (strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https")) {
|
||||
if proxy.Match(u.Host) {
|
||||
envs = append(envs, fmt.Sprintf("https_proxy=%s", proxy.GetProxyURL()))
|
||||
}
|
||||
if err == nil {
|
||||
envs = proxy.EnvWithProxy(u)
|
||||
}
|
||||
|
||||
stderr := new(bytes.Buffer)
|
||||
|
||||
@@ -101,6 +101,15 @@ func (te *TreeEntry) FollowLinks() (*TreeEntry, error) {
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// returns the subtree, or nil if this is not a tree
|
||||
func (te *TreeEntry) Tree() *Tree {
|
||||
t, err := te.ptree.repo.getTree(te.ID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// GetSubJumpablePathName return the full path of subdirectory jumpable ( contains only one directory )
|
||||
func (te *TreeEntry) GetSubJumpablePathName() string {
|
||||
if te.IsSubModule() || !te.IsDir() {
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
analyzer_custom "github.com/blevesearch/bleve/v2/analysis/analyzer/custom"
|
||||
analyzer_keyword "github.com/blevesearch/bleve/v2/analysis/analyzer/keyword"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/camelcase"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/lowercase"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/unicodenorm"
|
||||
"github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode"
|
||||
@@ -107,7 +108,7 @@ func (d *RepoIndexerData) Type() string {
|
||||
const (
|
||||
repoIndexerAnalyzer = "repoIndexerAnalyzer"
|
||||
repoIndexerDocType = "repoIndexerDocType"
|
||||
repoIndexerLatestVersion = 5
|
||||
repoIndexerLatestVersion = 6
|
||||
)
|
||||
|
||||
// createBleveIndexer create a bleve repo indexer if one does not already exist
|
||||
@@ -138,7 +139,7 @@ func createBleveIndexer(path string, latestVersion int) (bleve.Index, error) {
|
||||
"type": analyzer_custom.Name,
|
||||
"char_filters": []string{},
|
||||
"tokenizer": unicode.Name,
|
||||
"token_filters": []string{unicodeNormalizeName, lowercase.Name},
|
||||
"token_filters": []string{unicodeNormalizeName, camelcase.Name, lowercase.Name},
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
"github.com/blevesearch/bleve/v2"
|
||||
"github.com/blevesearch/bleve/v2/analysis/analyzer/custom"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/camelcase"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/lowercase"
|
||||
"github.com/blevesearch/bleve/v2/analysis/token/unicodenorm"
|
||||
"github.com/blevesearch/bleve/v2/analysis/tokenizer/unicode"
|
||||
@@ -27,7 +28,7 @@ import (
|
||||
const (
|
||||
issueIndexerAnalyzer = "issueIndexer"
|
||||
issueIndexerDocType = "issueIndexerDocType"
|
||||
issueIndexerLatestVersion = 1
|
||||
issueIndexerLatestVersion = 2
|
||||
)
|
||||
|
||||
// indexerID a bleve-compatible unique identifier for an integer id
|
||||
@@ -134,7 +135,7 @@ func createIssueIndexer(path string, latestVersion int) (bleve.Index, error) {
|
||||
"type": custom.Name,
|
||||
"char_filters": []string{},
|
||||
"tokenizer": unicode.Name,
|
||||
"token_filters": []string{unicodeNormalizeName, lowercase.Name},
|
||||
"token_filters": []string{unicodeNormalizeName, camelcase.Name, lowercase.Name},
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
package lfs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
@@ -12,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
// DetermineEndpoint determines an endpoint from the clone url or uses the specified LFS url.
|
||||
@@ -95,7 +95,7 @@ func endpointFromLocalPath(path string) *url.URL {
|
||||
return nil
|
||||
}
|
||||
|
||||
path = fmt.Sprintf("file://%s%s", slash, filepath.ToSlash(path))
|
||||
path = "file://" + slash + util.PathEscapeSegments(filepath.ToSlash(path))
|
||||
|
||||
u, _ := url.Parse(path)
|
||||
|
||||
|
||||
@@ -317,41 +317,3 @@ func IsMarkupFile(name, markup string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsReadmeFile reports whether name looks like a README file
|
||||
// based on its name.
|
||||
func IsReadmeFile(name string) bool {
|
||||
name = strings.ToLower(name)
|
||||
if len(name) < 6 {
|
||||
return false
|
||||
} else if len(name) == 6 {
|
||||
return name == "readme"
|
||||
}
|
||||
return name[:7] == "readme."
|
||||
}
|
||||
|
||||
// IsReadmeFileExtension reports whether name looks like a README file
|
||||
// based on its name. It will look through the provided extensions and check if the file matches
|
||||
// one of the extensions and provide the index in the extension list.
|
||||
// If the filename is `readme.` with an unmatched extension it will match with the index equaling
|
||||
// the length of the provided extension list.
|
||||
// Note that the '.' should be provided in ext, e.g ".md"
|
||||
func IsReadmeFileExtension(name string, ext ...string) (int, bool) {
|
||||
name = strings.ToLower(name)
|
||||
if len(name) < 6 || name[:6] != "readme" {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
for i, extension := range ext {
|
||||
extension = strings.ToLower(extension)
|
||||
if name[6:] == extension {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
|
||||
if name[6] == '.' {
|
||||
return len(ext), true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
@@ -2,94 +2,3 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package markup_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "code.gitea.io/gitea/modules/markup"
|
||||
|
||||
_ "code.gitea.io/gitea/modules/markup/markdown"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMisc_IsReadmeFile(t *testing.T) {
|
||||
trueTestCases := []string{
|
||||
"readme",
|
||||
"README",
|
||||
"readME.mdown",
|
||||
"README.md",
|
||||
"readme.i18n.md",
|
||||
}
|
||||
falseTestCases := []string{
|
||||
"test.md",
|
||||
"wow.MARKDOWN",
|
||||
"LOL.mDoWn",
|
||||
"test",
|
||||
"abcdefg",
|
||||
"abcdefghijklmnopqrstuvwxyz",
|
||||
"test.md.test",
|
||||
"readmf",
|
||||
}
|
||||
|
||||
for _, testCase := range trueTestCases {
|
||||
assert.True(t, IsReadmeFile(testCase))
|
||||
}
|
||||
for _, testCase := range falseTestCases {
|
||||
assert.False(t, IsReadmeFile(testCase))
|
||||
}
|
||||
|
||||
type extensionTestcase struct {
|
||||
name string
|
||||
expected bool
|
||||
idx int
|
||||
}
|
||||
|
||||
exts := []string{".md", ".txt", ""}
|
||||
testCasesExtensions := []extensionTestcase{
|
||||
{
|
||||
name: "readme",
|
||||
expected: true,
|
||||
idx: 2,
|
||||
},
|
||||
{
|
||||
name: "readme.md",
|
||||
expected: true,
|
||||
idx: 0,
|
||||
},
|
||||
{
|
||||
name: "README.md",
|
||||
expected: true,
|
||||
idx: 0,
|
||||
},
|
||||
{
|
||||
name: "ReAdMe.Md",
|
||||
expected: true,
|
||||
idx: 0,
|
||||
},
|
||||
{
|
||||
name: "readme.txt",
|
||||
expected: true,
|
||||
idx: 1,
|
||||
},
|
||||
{
|
||||
name: "readme.doc",
|
||||
expected: true,
|
||||
idx: 3,
|
||||
},
|
||||
{
|
||||
name: "readmee.md",
|
||||
},
|
||||
{
|
||||
name: "readme..",
|
||||
expected: true,
|
||||
idx: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCasesExtensions {
|
||||
idx, ok := IsReadmeFileExtension(testCase.name, exts...)
|
||||
assert.Equal(t, testCase.expected, ok)
|
||||
assert.Equal(t, testCase.idx, idx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
activities_model "code.gitea.io/gitea/models/activities"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
@@ -17,6 +20,7 @@ type Collector struct {
|
||||
Accesses *prometheus.Desc
|
||||
Actions *prometheus.Desc
|
||||
Attachments *prometheus.Desc
|
||||
BuildInfo *prometheus.Desc
|
||||
Comments *prometheus.Desc
|
||||
Follows *prometheus.Desc
|
||||
HookTasks *prometheus.Desc
|
||||
@@ -62,6 +66,16 @@ func NewCollector() Collector {
|
||||
"Number of Attachments",
|
||||
nil, nil,
|
||||
),
|
||||
BuildInfo: prometheus.NewDesc(
|
||||
namespace+"build_info",
|
||||
"Build information",
|
||||
[]string{
|
||||
"goarch",
|
||||
"goos",
|
||||
"goversion",
|
||||
"version",
|
||||
}, nil,
|
||||
),
|
||||
Comments: prometheus.NewDesc(
|
||||
namespace+"comments",
|
||||
"Number of Comments",
|
||||
@@ -195,6 +209,7 @@ func (c Collector) Describe(ch chan<- *prometheus.Desc) {
|
||||
ch <- c.Accesses
|
||||
ch <- c.Actions
|
||||
ch <- c.Attachments
|
||||
ch <- c.BuildInfo
|
||||
ch <- c.Comments
|
||||
ch <- c.Follows
|
||||
ch <- c.HookTasks
|
||||
@@ -241,6 +256,15 @@ func (c Collector) Collect(ch chan<- prometheus.Metric) {
|
||||
prometheus.GaugeValue,
|
||||
float64(stats.Counter.Attachment),
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.BuildInfo,
|
||||
prometheus.GaugeValue,
|
||||
1,
|
||||
runtime.GOARCH,
|
||||
runtime.GOOS,
|
||||
runtime.Version(),
|
||||
setting.AppVer,
|
||||
)
|
||||
ch <- prometheus.MustNewConstMetric(
|
||||
c.Comments,
|
||||
prometheus.GaugeValue,
|
||||
|
||||
@@ -8,8 +8,7 @@ import "time"
|
||||
|
||||
// Commentable can be commented upon
|
||||
type Commentable interface {
|
||||
GetLocalIndex() int64
|
||||
GetForeignIndex() int64
|
||||
Reviewable
|
||||
GetContext() DownloaderContext
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,15 @@ func (issue *Issue) GetExternalName() string { return issue.PosterName }
|
||||
// GetExternalID ExternalUserMigrated interface
|
||||
func (issue *Issue) GetExternalID() int64 { return issue.PosterID }
|
||||
|
||||
func (issue *Issue) GetLocalIndex() int64 { return issue.Number }
|
||||
func (issue *Issue) GetForeignIndex() int64 { return issue.ForeignIndex }
|
||||
func (issue *Issue) GetLocalIndex() int64 { return issue.Number }
|
||||
|
||||
func (issue *Issue) GetForeignIndex() int64 {
|
||||
// see the comment of Reviewable.GetForeignIndex
|
||||
// if there is no ForeignIndex, then use LocalIndex
|
||||
if issue.ForeignIndex == 0 {
|
||||
return issue.Number
|
||||
}
|
||||
return issue.ForeignIndex
|
||||
}
|
||||
|
||||
func (issue *Issue) GetContext() DownloaderContext { return issue.Context }
|
||||
|
||||
@@ -8,6 +8,16 @@ import "time"
|
||||
// Reviewable can be reviewed
|
||||
type Reviewable interface {
|
||||
GetLocalIndex() int64
|
||||
|
||||
// GetForeignIndex presents the foreign index, which could be misused:
|
||||
// For example, if there are 2 Gitea sites: site-A exports a dataset, then site-B imports it:
|
||||
// * if site-A exports files by using its LocalIndex
|
||||
// * from site-A's view, LocalIndex is site-A's IssueIndex while ForeignIndex is site-B's IssueIndex
|
||||
// * but from site-B's view, LocalIndex is site-B's IssueIndex while ForeignIndex is site-A's IssueIndex
|
||||
//
|
||||
// So the exporting/importing must be paired, but the meaning of them looks confusing then:
|
||||
// * either site-A and site-B both use LocalIndex during dumping/restoring
|
||||
// * or site-A and site-B both use ForeignIndex
|
||||
GetForeignIndex() int64
|
||||
}
|
||||
|
||||
@@ -37,7 +47,7 @@ type Review struct {
|
||||
// GetExternalName ExternalUserMigrated interface
|
||||
func (r *Review) GetExternalName() string { return r.ReviewerName }
|
||||
|
||||
// ExternalID ExternalUserMigrated interface
|
||||
// GetExternalID ExternalUserMigrated interface
|
||||
func (r *Review) GetExternalID() int64 { return r.ReviewerID }
|
||||
|
||||
// ReviewComment represents a review comment
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -82,3 +83,16 @@ func Proxy() func(req *http.Request) (*url.URL, error) {
|
||||
return http.ProxyFromEnvironment(req)
|
||||
}
|
||||
}
|
||||
|
||||
// EnvWithProxy returns os.Environ(), with a https_proxy env, if the given url
|
||||
// needs to be proxied.
|
||||
func EnvWithProxy(u *url.URL) []string {
|
||||
envs := os.Environ()
|
||||
if strings.EqualFold(u.Scheme, "http") || strings.EqualFold(u.Scheme, "https") {
|
||||
if Match(u.Host) {
|
||||
envs = append(envs, "https_proxy="+GetProxyURL())
|
||||
}
|
||||
}
|
||||
|
||||
return envs
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
|
||||
assert.NoError(t, organization.CreateOrganization(org, user), "CreateOrganization")
|
||||
|
||||
// Check Owner team.
|
||||
ownerTeam, err := org.GetOwnerTeam()
|
||||
ownerTeam, err := org.GetOwnerTeam(db.DefaultContext)
|
||||
assert.NoError(t, err, "GetOwnerTeam")
|
||||
assert.True(t, ownerTeam.IncludesAllRepositories, "Owner team includes all repositories")
|
||||
|
||||
@@ -63,7 +63,7 @@ func TestIncludesAllRepositoriesTeams(t *testing.T) {
|
||||
}
|
||||
}
|
||||
// Get fresh copy of Owner team after creating repos.
|
||||
ownerTeam, err = org.GetOwnerTeam()
|
||||
ownerTeam, err = org.GetOwnerTeam(db.DefaultContext)
|
||||
assert.NoError(t, err, "GetOwnerTeam")
|
||||
|
||||
// Create teams and check repositories.
|
||||
|
||||
@@ -57,7 +57,7 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User,
|
||||
repoPath := repo_model.RepoPath(u.Name, opts.RepoName)
|
||||
|
||||
if u.IsOrganization() {
|
||||
t, err := organization.OrgFromUser(u).GetOwnerTeam()
|
||||
t, err := organization.OrgFromUser(u).GetOwnerTeam(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ type Repository struct {
|
||||
Language string `json:"language"`
|
||||
LanguagesURL string `json:"languages_url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Link string `json:"link"`
|
||||
SSHURL string `json:"ssh_url"`
|
||||
CloneURL string `json:"clone_url"`
|
||||
OriginalURL string `json:"original_url"`
|
||||
@@ -95,6 +96,7 @@ type Repository struct {
|
||||
AllowRebaseUpdate bool `json:"allow_rebase_update"`
|
||||
DefaultDeleteBranchAfterMerge bool `json:"default_delete_branch_after_merge"`
|
||||
DefaultMergeStyle string `json:"default_merge_style"`
|
||||
DefaultAllowMaintainerEdit bool `json:"default_allow_maintainer_edit"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
Internal bool `json:"internal"`
|
||||
MirrorInterval string `json:"mirror_interval"`
|
||||
@@ -188,6 +190,8 @@ type EditRepoOption struct {
|
||||
DefaultDeleteBranchAfterMerge *bool `json:"default_delete_branch_after_merge,omitempty"`
|
||||
// set to a merge style to be used by this repository: "merge", "rebase", "rebase-merge", or "squash".
|
||||
DefaultMergeStyle *string `json:"default_merge_style,omitempty"`
|
||||
// set to `true` to allow edits from maintainers by default
|
||||
DefaultAllowMaintainerEdit *bool `json:"default_allow_maintainer_edit,omitempty"`
|
||||
// set to `true` to archive this repository.
|
||||
Archived *bool `json:"archived,omitempty"`
|
||||
// SizeLimit of the repository.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
package structs
|
||||
|
||||
// VisibleType defines the visibility (Organization only)
|
||||
// VisibleType defines the visibility of user and org
|
||||
type VisibleType int
|
||||
|
||||
const (
|
||||
@@ -13,11 +13,11 @@ const (
|
||||
// VisibleTypeLimited Visible for every connected user
|
||||
VisibleTypeLimited
|
||||
|
||||
// VisibleTypePrivate Visible only for organization's members
|
||||
// VisibleTypePrivate Visible only for self or admin user
|
||||
VisibleTypePrivate
|
||||
)
|
||||
|
||||
// VisibilityModes is a map of org Visibility types
|
||||
// VisibilityModes is a map of Visibility types
|
||||
var VisibilityModes = map[string]VisibleType{
|
||||
"public": VisibleTypePublic,
|
||||
"limited": VisibleTypeLimited,
|
||||
@@ -72,6 +72,10 @@ func NewFuncMap() []template.FuncMap {
|
||||
return setting.StaticURLPrefix + "/assets"
|
||||
},
|
||||
"AppUrl": func() string {
|
||||
// The usage of AppUrl should be avoided as much as possible,
|
||||
// because the AppURL(ROOT_URL) may not match user's visiting site and the ROOT_URL in app.ini may be incorrect.
|
||||
// And it's difficult for Gitea to guess absolute URL correctly with zero configuration,
|
||||
// because Gitea doesn't know whether the scheme is HTTP or HTTPS unless the reverse proxy could tell Gitea.
|
||||
return setting.AppURL
|
||||
},
|
||||
"AppVer": func() string {
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EnsureAbsolutePath ensure that a path is absolute, making it
|
||||
@@ -201,3 +202,41 @@ func CommonSkip(name string) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsReadmeFileName reports whether name looks like a README file
|
||||
// based on its name.
|
||||
func IsReadmeFileName(name string) bool {
|
||||
name = strings.ToLower(name)
|
||||
if len(name) < 6 {
|
||||
return false
|
||||
} else if len(name) == 6 {
|
||||
return name == "readme"
|
||||
}
|
||||
return name[:7] == "readme."
|
||||
}
|
||||
|
||||
// IsReadmeFileExtension reports whether name looks like a README file
|
||||
// based on its name. It will look through the provided extensions and check if the file matches
|
||||
// one of the extensions and provide the index in the extension list.
|
||||
// If the filename is `readme.` with an unmatched extension it will match with the index equaling
|
||||
// the length of the provided extension list.
|
||||
// Note that the '.' should be provided in ext, e.g ".md"
|
||||
func IsReadmeFileExtension(name string, ext ...string) (int, bool) {
|
||||
name = strings.ToLower(name)
|
||||
if len(name) < 6 || name[:6] != "readme" {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
for i, extension := range ext {
|
||||
extension = strings.ToLower(extension)
|
||||
if name[6:] == extension {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
|
||||
if name[6] == '.' {
|
||||
return len(ext), true
|
||||
}
|
||||
|
||||
return 0, false
|
||||
}
|
||||
|
||||
@@ -55,3 +55,84 @@ func TestFileURLToPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMisc_IsReadmeFileName(t *testing.T) {
|
||||
trueTestCases := []string{
|
||||
"readme",
|
||||
"README",
|
||||
"readME.mdown",
|
||||
"README.md",
|
||||
"readme.i18n.md",
|
||||
}
|
||||
falseTestCases := []string{
|
||||
"test.md",
|
||||
"wow.MARKDOWN",
|
||||
"LOL.mDoWn",
|
||||
"test",
|
||||
"abcdefg",
|
||||
"abcdefghijklmnopqrstuvwxyz",
|
||||
"test.md.test",
|
||||
"readmf",
|
||||
}
|
||||
|
||||
for _, testCase := range trueTestCases {
|
||||
assert.True(t, IsReadmeFileName(testCase))
|
||||
}
|
||||
for _, testCase := range falseTestCases {
|
||||
assert.False(t, IsReadmeFileName(testCase))
|
||||
}
|
||||
|
||||
type extensionTestcase struct {
|
||||
name string
|
||||
expected bool
|
||||
idx int
|
||||
}
|
||||
|
||||
exts := []string{".md", ".txt", ""}
|
||||
testCasesExtensions := []extensionTestcase{
|
||||
{
|
||||
name: "readme",
|
||||
expected: true,
|
||||
idx: 2,
|
||||
},
|
||||
{
|
||||
name: "readme.md",
|
||||
expected: true,
|
||||
idx: 0,
|
||||
},
|
||||
{
|
||||
name: "README.md",
|
||||
expected: true,
|
||||
idx: 0,
|
||||
},
|
||||
{
|
||||
name: "ReAdMe.Md",
|
||||
expected: true,
|
||||
idx: 0,
|
||||
},
|
||||
{
|
||||
name: "readme.txt",
|
||||
expected: true,
|
||||
idx: 1,
|
||||
},
|
||||
{
|
||||
name: "readme.doc",
|
||||
expected: true,
|
||||
idx: 3,
|
||||
},
|
||||
{
|
||||
name: "readmee.md",
|
||||
},
|
||||
{
|
||||
name: "readme..",
|
||||
expected: true,
|
||||
idx: 3,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCasesExtensions {
|
||||
idx, ok := IsReadmeFileExtension(testCase.name, exts...)
|
||||
assert.Equal(t, testCase.expected, ok)
|
||||
assert.Equal(t, testCase.idx, idx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/auth"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
|
||||
"gitea.com/go-chi/binding"
|
||||
@@ -17,15 +18,14 @@ import (
|
||||
const (
|
||||
// ErrGitRefName is git reference name error
|
||||
ErrGitRefName = "GitRefNameError"
|
||||
|
||||
// ErrGlobPattern is returned when glob pattern is invalid
|
||||
ErrGlobPattern = "GlobPattern"
|
||||
|
||||
// ErrRegexPattern is returned when a regex pattern is invalid
|
||||
ErrRegexPattern = "RegexPattern"
|
||||
|
||||
// ErrUsername is username error
|
||||
ErrUsername = "UsernameError"
|
||||
// ErrInvalidGroupTeamMap is returned when a group team mapping is invalid
|
||||
ErrInvalidGroupTeamMap = "InvalidGroupTeamMap"
|
||||
)
|
||||
|
||||
// AddBindingRules adds additional binding rules
|
||||
@@ -37,6 +37,7 @@ func AddBindingRules() {
|
||||
addRegexPatternRule()
|
||||
addGlobOrRegexPatternRule()
|
||||
addUsernamePatternRule()
|
||||
addValidGroupTeamMapRule()
|
||||
}
|
||||
|
||||
func addGitRefNameBindingRule() {
|
||||
@@ -167,6 +168,23 @@ func addUsernamePatternRule() {
|
||||
})
|
||||
}
|
||||
|
||||
func addValidGroupTeamMapRule() {
|
||||
binding.AddRule(&binding.Rule{
|
||||
IsMatch: func(rule string) bool {
|
||||
return strings.HasPrefix(rule, "ValidGroupTeamMap")
|
||||
},
|
||||
IsValid: func(errs binding.Errors, name string, val interface{}) (bool, binding.Errors) {
|
||||
_, err := auth.UnmarshalGroupTeamMapping(fmt.Sprintf("%v", val))
|
||||
if err != nil {
|
||||
errs.Add([]string{name}, ErrInvalidGroupTeamMap, err.Error())
|
||||
return false, errs
|
||||
}
|
||||
|
||||
return true, errs
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func portOnly(hostport string) string {
|
||||
colon := strings.IndexByte(hostport, ':')
|
||||
if colon == -1 {
|
||||
|
||||
@@ -136,6 +136,8 @@ func Validate(errs binding.Errors, data map[string]interface{}, f Form, l transl
|
||||
data["ErrorMsg"] = trName + l.Tr("form.regex_pattern_error", errs[0].Message)
|
||||
case validation.ErrUsername:
|
||||
data["ErrorMsg"] = trName + l.Tr("form.username_error")
|
||||
case validation.ErrInvalidGroupTeamMap:
|
||||
data["ErrorMsg"] = trName + l.Tr("form.invalid_group_team_map_error", errs[0].Message)
|
||||
default:
|
||||
msg := errs[0].Classification
|
||||
if msg != "" && errs[0].Message != "" {
|
||||
|
||||
@@ -477,6 +477,7 @@ include_error = ` must contain substring '%s'.`
|
||||
glob_pattern_error = ` glob pattern is invalid: %s.`
|
||||
regex_pattern_error = ` regex pattern is invalid: %s.`
|
||||
username_error = ` can only contain alphanumeric chars ('0-9','a-z','A-Z'), dash ('-'), underscore ('_') and dot ('.'). It cannot begin or end with non-alphanumeric chars, and consecutive non-alphanumeric chars are also forbidden.`
|
||||
invalid_group_team_map_error = ` mapping is invalid: %s`
|
||||
unknown_error = Unknown error:
|
||||
captcha_incorrect = The CAPTCHA code is incorrect.
|
||||
password_not_match = The passwords do not match.
|
||||
@@ -1233,6 +1234,9 @@ projects.board.color = "Color"
|
||||
projects.open = Open
|
||||
projects.close = Close
|
||||
projects.board.assigned_to = Assigned to
|
||||
projects.card_type.desc = "Card Previews"
|
||||
projects.card_type.images_and_text = "Images and Text"
|
||||
projects.card_type.text_only = "Text Only"
|
||||
|
||||
issues.desc = Organize bug reports, tasks and milestones.
|
||||
issues.filter_assignees = Filter Assignee
|
||||
@@ -1870,14 +1874,10 @@ settings.enable_timetracker = Enable Time Tracking
|
||||
settings.allow_only_contributors_to_track_time = Let Only Contributors Track Time
|
||||
settings.pulls_desc = Enable Repository Pull Requests
|
||||
settings.pulls.ignore_whitespace = Ignore Whitespace for Conflicts
|
||||
settings.pulls.allow_merge_commits = Enable Commit Merging
|
||||
settings.pulls.allow_rebase_merge = Enable Rebasing to Merge Commits
|
||||
settings.pulls.allow_rebase_merge_commit = Enable Rebasing with explicit merge commits (--no-ff)
|
||||
settings.pulls.allow_squash_commits = Enable Squashing to Merge Commits
|
||||
settings.pulls.allow_manual_merge = Enable Mark PR as manually merged
|
||||
settings.pulls.enable_autodetect_manual_merge = Enable autodetect manual merge (Note: In some special cases, misjudgments can occur)
|
||||
settings.pulls.allow_rebase_update = Enable updating pull request branch by rebase
|
||||
settings.pulls.default_delete_branch_after_merge = Delete pull request branch after merge by default
|
||||
settings.pulls.default_allow_edits_from_maintainers = Allow edits from maintainers by default
|
||||
settings.releases_desc = Enable Repository Releases
|
||||
settings.packages_desc = Enable Repository Packages Registry
|
||||
settings.projects_desc = Enable Repository Projects
|
||||
@@ -2146,7 +2146,8 @@ settings.block_on_official_review_requests_desc = Merging will not be possible w
|
||||
settings.block_outdated_branch = Block merge if pull request is outdated
|
||||
settings.block_outdated_branch_desc = Merging will not be possible when head branch is behind base branch.
|
||||
settings.default_branch_desc = Select a default repository branch for pull requests and code commits:
|
||||
settings.default_merge_style_desc = Default merge style for pull requests:
|
||||
settings.merge_style_desc = Merge Styles
|
||||
settings.default_merge_style_desc = Default Merge Style
|
||||
settings.choose_branch = Choose a branch…
|
||||
settings.no_protected_branch = There are no protected branches.
|
||||
settings.edit_protected_branch = Edit
|
||||
@@ -2761,6 +2762,8 @@ auths.oauth2_required_claim_value_helper = Set this value to restrict login from
|
||||
auths.oauth2_group_claim_name = Claim name providing group names for this source. (Optional)
|
||||
auths.oauth2_admin_group = Group Claim value for administrator users. (Optional - requires claim name above)
|
||||
auths.oauth2_restricted_group = Group Claim value for restricted users. (Optional - requires claim name above)
|
||||
auths.oauth2_map_group_to_team = Map claimed groups to Organization teams. (Optional - requires claim name above)
|
||||
auths.oauth2_map_group_to_team_removal = Remove users from synchronized teams if user does not belong to corresponding group.
|
||||
auths.enable_auto_register = Enable Auto Registration
|
||||
auths.sspi_auto_create_users = Automatically create users
|
||||
auths.sspi_auto_create_users_helper = Allow SSPI auth method to automatically create new accounts for users that login for the first time
|
||||
|
||||
@@ -286,9 +286,18 @@ func CommonRoutes(ctx gocontext.Context) *web.Route {
|
||||
}, reqPackageAccess(perm.AccessModeWrite))
|
||||
r.Get("/symbols/{filename}/{guid:[0-9a-fA-F]{32}[fF]{8}}/{filename2}", nuget.DownloadSymbolFile)
|
||||
r.Get("/Packages(Id='{id:[^']+}',Version='{version:[^']+}')", nuget.RegistrationLeafV2)
|
||||
r.Get("/Packages()", nuget.SearchServiceV2)
|
||||
r.Get("/FindPackagesById()", nuget.EnumeratePackageVersionsV2)
|
||||
r.Get("/Search()", nuget.SearchServiceV2)
|
||||
r.Group("/Packages()", func() {
|
||||
r.Get("", nuget.SearchServiceV2)
|
||||
r.Get("/$count", nuget.SearchServiceV2Count)
|
||||
})
|
||||
r.Group("/FindPackagesById()", func() {
|
||||
r.Get("", nuget.EnumeratePackageVersionsV2)
|
||||
r.Get("/$count", nuget.EnumeratePackageVersionsV2Count)
|
||||
})
|
||||
r.Group("/Search()", func() {
|
||||
r.Get("", nuget.SearchServiceV2)
|
||||
r.Get("/$count", nuget.SearchServiceV2Count)
|
||||
})
|
||||
}, reqPackageAccess(perm.AccessModeRead))
|
||||
})
|
||||
r.Group("/npm", func() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
@@ -94,8 +95,7 @@ func FeedCapabilityResource(ctx *context.Context) {
|
||||
|
||||
var searchTermExtract = regexp.MustCompile(`'([^']+)'`)
|
||||
|
||||
// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
|
||||
func SearchServiceV2(ctx *context.Context) {
|
||||
func getSearchTerm(ctx *context.Context) string {
|
||||
searchTerm := strings.Trim(ctx.FormTrim("searchTerm"), "'")
|
||||
if searchTerm == "" {
|
||||
// $filter contains a query like:
|
||||
@@ -106,7 +106,11 @@ func SearchServiceV2(ctx *context.Context) {
|
||||
searchTerm = strings.TrimSpace(match[1])
|
||||
}
|
||||
}
|
||||
return searchTerm
|
||||
}
|
||||
|
||||
// https://github.com/NuGet/NuGet.Client/blob/dev/src/NuGet.Core/NuGet.Protocol/LegacyFeed/V2FeedQueryBuilder.cs
|
||||
func SearchServiceV2(ctx *context.Context) {
|
||||
skip, take := ctx.FormInt("skip"), ctx.FormInt("take")
|
||||
if skip == 0 {
|
||||
skip = ctx.FormInt("$skip")
|
||||
@@ -116,9 +120,11 @@ func SearchServiceV2(ctx *context.Context) {
|
||||
}
|
||||
|
||||
pvs, total, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeNuGet,
|
||||
Name: packages_model.SearchValue{Value: searchTerm},
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeNuGet,
|
||||
Name: packages_model.SearchValue{
|
||||
Value: getSearchTerm(ctx),
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
Paginator: db.NewAbsoluteListOptions(
|
||||
skip,
|
||||
@@ -145,6 +151,24 @@ func SearchServiceV2(ctx *context.Context) {
|
||||
xmlResponse(ctx, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions-complete.html#_Toc453752351
|
||||
func SearchServiceV2Count(ctx *context.Context) {
|
||||
count, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeNuGet,
|
||||
Name: packages_model.SearchValue{
|
||||
Value: getSearchTerm(ctx),
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.PlainText(http.StatusOK, strconv.FormatInt(count, 10))
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/search-query-service-resource#search-for-packages
|
||||
func SearchServiceV3(ctx *context.Context) {
|
||||
pvs, count, err := packages_model.SearchVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
@@ -288,6 +312,25 @@ func EnumeratePackageVersionsV2(ctx *context.Context) {
|
||||
xmlResponse(ctx, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// http://docs.oasis-open.org/odata/odata/v4.0/errata03/os/complete/part2-url-conventions/odata-v4.0-errata03-os-part2-url-conventions-complete.html#_Toc453752351
|
||||
func EnumeratePackageVersionsV2Count(ctx *context.Context) {
|
||||
count, err := packages_model.CountVersions(ctx, &packages_model.PackageSearchOptions{
|
||||
OwnerID: ctx.Package.Owner.ID,
|
||||
Type: packages_model.TypeNuGet,
|
||||
Name: packages_model.SearchValue{
|
||||
ExactMatch: true,
|
||||
Value: strings.Trim(ctx.FormTrim("id"), "'"),
|
||||
},
|
||||
IsInternal: util.OptionalBoolFalse,
|
||||
})
|
||||
if err != nil {
|
||||
apiError(ctx, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.PlainText(http.StatusOK, strconv.FormatInt(count, 10))
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/nuget/api/package-base-address-resource#enumerate-package-versions
|
||||
func EnumeratePackageVersionsV3(ctx *context.Context) {
|
||||
packageName := ctx.Params("id")
|
||||
|
||||
@@ -507,7 +507,7 @@ func orgAssignment(args ...bool) func(ctx *context.APIContext) {
|
||||
|
||||
var err error
|
||||
if assignOrg {
|
||||
ctx.Org.Organization, err = organization.GetOrgByName(ctx.Params(":org"))
|
||||
ctx.Org.Organization, err = organization.GetOrgByName(ctx, ctx.Params(":org"))
|
||||
if err != nil {
|
||||
if organization.IsErrOrgNotExist(err) {
|
||||
redirectUserID, err := user_model.LookupUserRedirect(ctx.Params(":org"))
|
||||
@@ -687,7 +687,7 @@ func Routes(ctx gocontext.Context) *web.Route {
|
||||
}
|
||||
|
||||
// Get user from session if logged in.
|
||||
m.Use(context.APIAuth(group))
|
||||
m.Use(auth.APIAuth(group))
|
||||
|
||||
m.Use(context.ToggleAPI(&context.ToggleOptions{
|
||||
SignInRequired: setting.Service.RequireSignInView,
|
||||
|
||||
@@ -108,7 +108,7 @@ func CreateFork(ctx *context.APIContext) {
|
||||
if form.Organization == nil {
|
||||
forker = ctx.Doer
|
||||
} else {
|
||||
org, err := organization.GetOrgByName(*form.Organization)
|
||||
org, err := organization.GetOrgByName(ctx, *form.Organization)
|
||||
if err != nil {
|
||||
if organization.IsErrOrgNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
|
||||
@@ -469,7 +469,7 @@ func CreateOrgRepo(ctx *context.APIContext) {
|
||||
// "403":
|
||||
// "$ref": "#/responses/forbidden"
|
||||
opt := web.GetForm(ctx).(*api.CreateRepoOption)
|
||||
org, err := organization.GetOrgByName(ctx.Params(":org"))
|
||||
org, err := organization.GetOrgByName(ctx, ctx.Params(":org"))
|
||||
if err != nil {
|
||||
if organization.IsErrOrgNotExist(err) {
|
||||
ctx.Error(http.StatusUnprocessableEntity, "", err)
|
||||
@@ -868,6 +868,7 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
|
||||
AllowRebaseUpdate: true,
|
||||
DefaultDeleteBranchAfterMerge: false,
|
||||
DefaultMergeStyle: repo_model.MergeStyleMerge,
|
||||
DefaultAllowMaintainerEdit: false,
|
||||
}
|
||||
} else {
|
||||
config = unit.PullRequestsConfig()
|
||||
@@ -903,6 +904,9 @@ func updateRepoUnits(ctx *context.APIContext, opts api.EditRepoOption) error {
|
||||
if opts.DefaultMergeStyle != nil {
|
||||
config.DefaultMergeStyle = repo_model.MergeStyle(*opts.DefaultMergeStyle)
|
||||
}
|
||||
if opts.DefaultAllowMaintainerEdit != nil {
|
||||
config.DefaultAllowMaintainerEdit = *opts.DefaultAllowMaintainerEdit
|
||||
}
|
||||
|
||||
units = append(units, repo_model.RepoUnit{
|
||||
RepoID: repo.ID,
|
||||
|
||||
@@ -204,6 +204,8 @@ func parseOAuth2Config(form forms.AuthenticationForm) *oauth2.Source {
|
||||
GroupClaimName: form.Oauth2GroupClaimName,
|
||||
RestrictedGroup: form.Oauth2RestrictedGroup,
|
||||
AdminGroup: form.Oauth2AdminGroup,
|
||||
GroupTeamMap: form.Oauth2GroupTeamMap,
|
||||
GroupTeamMapRemoval: form.Oauth2GroupTeamMapRemoval,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
auth_service "code.gitea.io/gitea/services/auth"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
"code.gitea.io/gitea/services/externalaccount"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
|
||||
@@ -267,5 +268,11 @@ func LinkAccountPostRegister(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
source := authSource.Cfg.(*oauth2.Source)
|
||||
if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
|
||||
ctx.ServerError("SyncGroupsToTeams", err)
|
||||
return
|
||||
}
|
||||
|
||||
handleSignIn(ctx, u, false)
|
||||
}
|
||||
|
||||
+65
-30
@@ -17,7 +17,9 @@ import (
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
org_model "code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
auth_module "code.gitea.io/gitea/modules/auth"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@@ -27,6 +29,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
auth_service "code.gitea.io/gitea/services/auth"
|
||||
source_service "code.gitea.io/gitea/services/auth/source"
|
||||
"code.gitea.io/gitea/services/auth/source/oauth2"
|
||||
"code.gitea.io/gitea/services/externalaccount"
|
||||
"code.gitea.io/gitea/services/forms"
|
||||
@@ -963,12 +966,19 @@ func SignInOAuthCallback(ctx *context.Context) {
|
||||
IsActive: util.OptionalBoolOf(!setting.OAuth2Client.RegisterEmailConfirm),
|
||||
}
|
||||
|
||||
setUserGroupClaims(authSource, u, &gothUser)
|
||||
source := authSource.Cfg.(*oauth2.Source)
|
||||
|
||||
setUserAdminAndRestrictedFromGroupClaims(source, u, &gothUser)
|
||||
|
||||
if !createAndHandleCreatedUser(ctx, base.TplName(""), nil, u, overwriteDefault, &gothUser, setting.OAuth2Client.AccountLinking != setting.OAuth2AccountLinkingDisabled) {
|
||||
// error already handled
|
||||
return
|
||||
}
|
||||
|
||||
if err := syncGroupsToTeams(ctx, source, &gothUser, u); err != nil {
|
||||
ctx.ServerError("SyncGroupsToTeams", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// no existing user is found, request attach or new account
|
||||
showLinkingLogin(ctx, gothUser)
|
||||
@@ -979,7 +989,7 @@ func SignInOAuthCallback(ctx *context.Context) {
|
||||
handleOAuth2SignIn(ctx, authSource, u, gothUser)
|
||||
}
|
||||
|
||||
func claimValueToStringSlice(claimValue interface{}) []string {
|
||||
func claimValueToStringSet(claimValue interface{}) container.Set[string] {
|
||||
var groups []string
|
||||
|
||||
switch rawGroup := claimValue.(type) {
|
||||
@@ -993,37 +1003,45 @@ func claimValueToStringSlice(claimValue interface{}) []string {
|
||||
str := fmt.Sprintf("%s", rawGroup)
|
||||
groups = strings.Split(str, ",")
|
||||
}
|
||||
return groups
|
||||
return container.SetOf(groups...)
|
||||
}
|
||||
|
||||
func setUserGroupClaims(loginSource *auth.Source, u *user_model.User, gothUser *goth.User) bool {
|
||||
source := loginSource.Cfg.(*oauth2.Source)
|
||||
if source.GroupClaimName == "" || (source.AdminGroup == "" && source.RestrictedGroup == "") {
|
||||
return false
|
||||
func syncGroupsToTeams(ctx *context.Context, source *oauth2.Source, gothUser *goth.User, u *user_model.User) error {
|
||||
if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
|
||||
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
groups := getClaimedGroups(source, gothUser)
|
||||
|
||||
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, source.GroupTeamMapRemoval); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getClaimedGroups(source *oauth2.Source, gothUser *goth.User) container.Set[string] {
|
||||
groupClaims, has := gothUser.RawData[source.GroupClaimName]
|
||||
if !has {
|
||||
return false
|
||||
return nil
|
||||
}
|
||||
|
||||
groups := claimValueToStringSlice(groupClaims)
|
||||
return claimValueToStringSet(groupClaims)
|
||||
}
|
||||
|
||||
func setUserAdminAndRestrictedFromGroupClaims(source *oauth2.Source, u *user_model.User, gothUser *goth.User) bool {
|
||||
groups := getClaimedGroups(source, gothUser)
|
||||
|
||||
wasAdmin, wasRestricted := u.IsAdmin, u.IsRestricted
|
||||
|
||||
if source.AdminGroup != "" {
|
||||
u.IsAdmin = false
|
||||
u.IsAdmin = groups.Contains(source.AdminGroup)
|
||||
}
|
||||
if source.RestrictedGroup != "" {
|
||||
u.IsRestricted = false
|
||||
}
|
||||
|
||||
for _, g := range groups {
|
||||
if source.AdminGroup != "" && g == source.AdminGroup {
|
||||
u.IsAdmin = true
|
||||
} else if source.RestrictedGroup != "" && g == source.RestrictedGroup {
|
||||
u.IsRestricted = true
|
||||
}
|
||||
u.IsRestricted = groups.Contains(source.RestrictedGroup)
|
||||
}
|
||||
|
||||
return wasAdmin != u.IsAdmin || wasRestricted != u.IsRestricted
|
||||
@@ -1070,6 +1088,15 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
|
||||
needs2FA = err == nil
|
||||
}
|
||||
|
||||
oauth2Source := source.Cfg.(*oauth2.Source)
|
||||
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(oauth2Source.GroupTeamMap)
|
||||
if err != nil {
|
||||
ctx.ServerError("UnmarshalGroupTeamMapping", err)
|
||||
return
|
||||
}
|
||||
|
||||
groups := getClaimedGroups(oauth2Source, &gothUser)
|
||||
|
||||
// If this user is enrolled in 2FA and this source doesn't override it,
|
||||
// we can't sign the user in just yet. Instead, redirect them to the 2FA authentication page.
|
||||
if !needs2FA {
|
||||
@@ -1088,7 +1115,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
|
||||
u.SetLastLogin()
|
||||
|
||||
// Update GroupClaims
|
||||
changed := setUserGroupClaims(source, u, &gothUser)
|
||||
changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser)
|
||||
cols := []string{"last_login_unix"}
|
||||
if changed {
|
||||
cols = append(cols, "is_admin", "is_restricted")
|
||||
@@ -1099,6 +1126,13 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
|
||||
return
|
||||
}
|
||||
|
||||
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
|
||||
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
|
||||
ctx.ServerError("SyncGroupsToTeams", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// update external user information
|
||||
if err := externalaccount.UpdateExternalUser(u, gothUser); err != nil {
|
||||
if !errors.Is(err, util.ErrNotExist) {
|
||||
@@ -1121,7 +1155,7 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
|
||||
return
|
||||
}
|
||||
|
||||
changed := setUserGroupClaims(source, u, &gothUser)
|
||||
changed := setUserAdminAndRestrictedFromGroupClaims(oauth2Source, u, &gothUser)
|
||||
if changed {
|
||||
if err := user_model.UpdateUserCols(ctx, u, "is_admin", "is_restricted"); err != nil {
|
||||
ctx.ServerError("UpdateUserCols", err)
|
||||
@@ -1129,6 +1163,13 @@ func handleOAuth2SignIn(ctx *context.Context, source *auth.Source, u *user_model
|
||||
}
|
||||
}
|
||||
|
||||
if oauth2Source.GroupTeamMap != "" || oauth2Source.GroupTeamMapRemoval {
|
||||
if err := source_service.SyncGroupsToTeams(ctx, u, groups, groupTeamMapping, oauth2Source.GroupTeamMapRemoval); err != nil {
|
||||
ctx.ServerError("SyncGroupsToTeams", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := updateSession(ctx, nil, map[string]interface{}{
|
||||
// User needs to use 2FA, save data and redirect to 2FA page.
|
||||
"twofaUid": u.ID,
|
||||
@@ -1188,15 +1229,9 @@ func oAuth2UserLoginCallback(authSource *auth.Source, request *http.Request, res
|
||||
}
|
||||
|
||||
if oauth2Source.RequiredClaimValue != "" {
|
||||
groups := claimValueToStringSlice(claimInterface)
|
||||
found := false
|
||||
for _, group := range groups {
|
||||
if group == oauth2Source.RequiredClaimValue {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
groups := claimValueToStringSet(claimInterface)
|
||||
|
||||
if !groups.Contains(oauth2Source.RequiredClaimValue) {
|
||||
return nil, goth.User{}, user_model.ErrUserProhibitLogin{Name: gothUser.UserID}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ func feedActionsToFeedItems(ctx *context.Context, actions activities_model.Actio
|
||||
|
||||
var content, desc, title string
|
||||
|
||||
link := &feeds.Link{Href: act.GetCommentLink()}
|
||||
link := &feeds.Link{Href: act.GetCommentHTMLURL()}
|
||||
|
||||
// title
|
||||
title = act.ActUser.DisplayName() + " "
|
||||
|
||||
@@ -121,7 +121,7 @@ func canWriteUnit(ctx *context.Context) bool {
|
||||
// NewProject render creating a project page
|
||||
func NewProject(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
|
||||
ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig()
|
||||
ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
|
||||
ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
|
||||
ctx.Data["HomeLink"] = ctx.ContextUser.HomeLink()
|
||||
shared_user.RenderUserHeader(ctx)
|
||||
@@ -137,7 +137,7 @@ func NewProjectPost(ctx *context.Context) {
|
||||
if ctx.HasError() {
|
||||
ctx.Data["CanWriteProjects"] = canWriteUnit(ctx)
|
||||
ctx.Data["PageIsViewProjects"] = true
|
||||
ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig()
|
||||
ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
|
||||
ctx.HTML(http.StatusOK, tplProjectsNew)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func List(ctx *context.Context) {
|
||||
}
|
||||
|
||||
ctx.Data["workflows"] = workflows
|
||||
ctx.Data["RepoLink"] = ctx.Repo.Repository.HTMLURL()
|
||||
ctx.Data["RepoLink"] = ctx.Repo.Repository.Link()
|
||||
|
||||
page := ctx.FormInt("page")
|
||||
if page <= 0 {
|
||||
|
||||
@@ -262,9 +262,9 @@ func renderBlame(ctx *context.Context, blameParts []git.BlamePart, commitNames m
|
||||
|
||||
var avatar string
|
||||
if commit.User != nil {
|
||||
avatar = string(templates.Avatar(commit.User, 18, "mr-3"))
|
||||
avatar = string(templates.Avatar(commit.User, 18, "gt-mr-3"))
|
||||
} else {
|
||||
avatar = string(templates.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "mr-3"))
|
||||
avatar = string(templates.AvatarByEmail(commit.Author.Email, commit.Author.Name, 18, "gt-mr-3"))
|
||||
}
|
||||
|
||||
br.Avatar = gotemplate.HTML(avatar)
|
||||
|
||||
@@ -820,6 +820,13 @@ func CompareDiff(ctx *context.Context) {
|
||||
|
||||
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypePullRequests)
|
||||
|
||||
if unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypePullRequests); err == nil {
|
||||
config := unit.PullRequestsConfig()
|
||||
ctx.Data["AllowMaintainerEdit"] = config.DefaultAllowMaintainerEdit
|
||||
} else {
|
||||
ctx.Data["AllowMaintainerEdit"] = false
|
||||
}
|
||||
|
||||
ctx.HTML(http.StatusOK, tplCompare)
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ func MustAllowUserComment(ctx *context.Context) {
|
||||
|
||||
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
|
||||
ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
|
||||
ctx.Redirect(issue.HTMLURL())
|
||||
ctx.Redirect(issue.Link())
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -927,7 +927,7 @@ func NewIssueChooseTemplate(ctx *context.Context) {
|
||||
|
||||
if len(issueTemplates) == 0 {
|
||||
// The "issues/new" and "issues/new/choose" share the same query parameters "project" and "milestone", if no template here, just redirect to the "issues/new" page with these parameters.
|
||||
ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.HTMLURL(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
|
||||
ctx.Redirect(fmt.Sprintf("%s/issues/new?%s", ctx.Repo.Repository.Link(), ctx.Req.URL.RawQuery), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -950,11 +950,11 @@ func DeleteIssue(ctx *context.Context) {
|
||||
}
|
||||
|
||||
if issue.IsPull {
|
||||
ctx.Redirect(fmt.Sprintf("%s/pulls", ctx.Repo.Repository.HTMLURL()), http.StatusSeeOther)
|
||||
ctx.Redirect(fmt.Sprintf("%s/pulls", ctx.Repo.Repository.Link()), http.StatusSeeOther)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(fmt.Sprintf("%s/issues", ctx.Repo.Repository.HTMLURL()), http.StatusSeeOther)
|
||||
ctx.Redirect(fmt.Sprintf("%s/issues", ctx.Repo.Repository.Link()), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// ValidateRepoMetas check and returns repository's meta information
|
||||
@@ -1425,7 +1425,7 @@ func ViewIssue(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
// Add link to the issue of the already running stopwatch
|
||||
ctx.Data["OtherStopwatchURL"] = otherIssue.HTMLURL()
|
||||
ctx.Data["OtherStopwatchURL"] = otherIssue.Link()
|
||||
}
|
||||
}
|
||||
ctx.Data["CanUseTimetracker"] = ctx.Repo.CanUseTimetracker(issue, ctx.Doer)
|
||||
@@ -2658,7 +2658,7 @@ func NewComment(ctx *context.Context) {
|
||||
|
||||
if issue.IsLocked && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull) && !ctx.Doer.IsAdmin {
|
||||
ctx.Flash.Error(ctx.Tr("repo.issues.comment_on_locked"))
|
||||
ctx.Redirect(issue.HTMLURL())
|
||||
ctx.Redirect(issue.Link())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2669,7 +2669,7 @@ func NewComment(ctx *context.Context) {
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.Flash.Error(ctx.Data["ErrorMsg"].(string))
|
||||
ctx.Redirect(issue.HTMLURL())
|
||||
ctx.Redirect(issue.Link())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ func GetContentHistoryList(ctx *context.Context) {
|
||||
}
|
||||
|
||||
src := html.EscapeString(item.UserAvatarLink)
|
||||
class := avatars.DefaultAvatarClass + " mr-3"
|
||||
class := avatars.DefaultAvatarClass + " gt-mr-3"
|
||||
name := html.EscapeString(username)
|
||||
avatarHTML := string(templates.AvatarHTML(src, 28, class, username))
|
||||
timeSinceText := string(timeutil.TimeSinceUnix(item.EditedUnix, ctx.Locale))
|
||||
|
||||
@@ -34,7 +34,7 @@ func AddDependency(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// Redirect
|
||||
defer ctx.Redirect(issue.HTMLURL())
|
||||
defer ctx.Redirect(issue.Link())
|
||||
|
||||
// Dependency
|
||||
dep, err := issues_model.GetIssueByID(ctx, depID)
|
||||
@@ -124,5 +124,5 @@ func RemoveDependency(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// Redirect
|
||||
ctx.Redirect(issue.HTMLURL())
|
||||
ctx.Redirect(issue.Link())
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func RetrieveLabels(ctx *context.Context) {
|
||||
}
|
||||
ctx.Data["OrgLabels"] = orgLabels
|
||||
|
||||
org, err := organization.GetOrgByName(ctx.Repo.Owner.LowerName)
|
||||
org, err := organization.GetOrgByName(ctx, ctx.Repo.Owner.LowerName)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetOrgByName", err)
|
||||
return
|
||||
|
||||
@@ -21,13 +21,13 @@ func LockIssue(ctx *context.Context) {
|
||||
|
||||
if issue.IsLocked {
|
||||
ctx.Flash.Error(ctx.Tr("repo.issues.lock_duplicate"))
|
||||
ctx.Redirect(issue.HTMLURL())
|
||||
ctx.Redirect(issue.Link())
|
||||
return
|
||||
}
|
||||
|
||||
if !form.HasValidReason() {
|
||||
ctx.Flash.Error(ctx.Tr("repo.issues.lock.unknown_reason"))
|
||||
ctx.Redirect(issue.HTMLURL())
|
||||
ctx.Redirect(issue.Link())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func LockIssue(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(issue.HTMLURL())
|
||||
ctx.Redirect(issue.Link())
|
||||
}
|
||||
|
||||
// UnlockIssue unlocks a previously locked issue.
|
||||
@@ -52,7 +52,7 @@ func UnlockIssue(ctx *context.Context) {
|
||||
|
||||
if !issue.IsLocked {
|
||||
ctx.Flash.Error(ctx.Tr("repo.issues.unlock_error"))
|
||||
ctx.Redirect(issue.HTMLURL())
|
||||
ctx.Redirect(issue.Link())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -64,5 +64,5 @@ func UnlockIssue(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(issue.HTMLURL())
|
||||
ctx.Redirect(issue.Link())
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func IssueStopwatch(c *context.Context) {
|
||||
c.Flash.Success(c.Tr("repo.issues.tracker_auto_close"))
|
||||
}
|
||||
|
||||
url := issue.HTMLURL()
|
||||
url := issue.Link()
|
||||
c.Redirect(url, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ func CancelStopwatch(c *context.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
url := issue.HTMLURL()
|
||||
url := issue.Link()
|
||||
c.Redirect(url, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ func AddTimeManually(c *context.Context) {
|
||||
c.NotFound("CanUseTimetracker", nil)
|
||||
return
|
||||
}
|
||||
url := issue.HTMLURL()
|
||||
url := issue.Link()
|
||||
|
||||
if c.HasError() {
|
||||
c.Flash.Error(c.GetErrMsg())
|
||||
@@ -83,5 +83,5 @@ func DeleteTime(c *context.Context) {
|
||||
}
|
||||
|
||||
c.Flash.Success(c.Tr("repo.issues.del_time_history", util.SecToTime(t.Time)))
|
||||
c.Redirect(issue.HTMLURL())
|
||||
c.Redirect(issue.Link())
|
||||
}
|
||||
|
||||
@@ -52,5 +52,5 @@ func IssueWatch(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(issue.HTMLURL())
|
||||
ctx.Redirect(issue.Link())
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
attachment_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
@@ -123,7 +124,8 @@ func Projects(ctx *context.Context) {
|
||||
// NewProject render creating a project page
|
||||
func NewProject(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.projects.new")
|
||||
ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig()
|
||||
ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
|
||||
ctx.Data["CardTypes"] = project_model.GetCardConfig()
|
||||
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
|
||||
ctx.HTML(http.StatusOK, tplProjectsNew)
|
||||
}
|
||||
@@ -135,7 +137,8 @@ func NewProjectPost(ctx *context.Context) {
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
|
||||
ctx.Data["ProjectTypes"] = project_model.GetProjectsConfig()
|
||||
ctx.Data["BoardTypes"] = project_model.GetBoardConfig()
|
||||
ctx.Data["CardTypes"] = project_model.GetCardConfig()
|
||||
ctx.HTML(http.StatusOK, tplProjectsNew)
|
||||
return
|
||||
}
|
||||
@@ -146,6 +149,7 @@ func NewProjectPost(ctx *context.Context) {
|
||||
Description: form.Content,
|
||||
CreatorID: ctx.Doer.ID,
|
||||
BoardType: form.BoardType,
|
||||
CardType: form.CardType,
|
||||
Type: project_model.TypeRepository,
|
||||
}); err != nil {
|
||||
ctx.ServerError("NewProject", err)
|
||||
@@ -212,6 +216,7 @@ func EditProject(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
|
||||
ctx.Data["PageIsEditProjects"] = true
|
||||
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
|
||||
ctx.Data["CardTypes"] = project_model.GetCardConfig()
|
||||
|
||||
p, err := project_model.GetProjectByID(ctx, ctx.ParamsInt64(":id"))
|
||||
if err != nil {
|
||||
@@ -229,6 +234,7 @@ func EditProject(ctx *context.Context) {
|
||||
|
||||
ctx.Data["title"] = p.Title
|
||||
ctx.Data["content"] = p.Description
|
||||
ctx.Data["card_type"] = p.CardType
|
||||
|
||||
ctx.HTML(http.StatusOK, tplProjectsNew)
|
||||
}
|
||||
@@ -239,6 +245,7 @@ func EditProjectPost(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.projects.edit")
|
||||
ctx.Data["PageIsEditProjects"] = true
|
||||
ctx.Data["CanWriteProjects"] = ctx.Repo.Permission.CanWrite(unit.TypeProjects)
|
||||
ctx.Data["CardTypes"] = project_model.GetCardConfig()
|
||||
|
||||
if ctx.HasError() {
|
||||
ctx.HTML(http.StatusOK, tplProjectsNew)
|
||||
@@ -261,6 +268,7 @@ func EditProjectPost(ctx *context.Context) {
|
||||
|
||||
p.Title = form.Title
|
||||
p.Description = form.Content
|
||||
p.CardType = form.CardType
|
||||
if err = project_model.UpdateProject(ctx, p); err != nil {
|
||||
ctx.ServerError("UpdateProjects", err)
|
||||
return
|
||||
@@ -302,6 +310,18 @@ func ViewProject(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
if project.CardType != project_model.CardTypeTextOnly {
|
||||
issuesAttachmentMap := make(map[int64][]*attachment_model.Attachment)
|
||||
for _, issuesList := range issuesMap {
|
||||
for _, issue := range issuesList {
|
||||
if issueAttachment, err := attachment_model.GetAttachmentsByIssueIDImagesLatest(ctx, issue.ID); err == nil {
|
||||
issuesAttachmentMap[issue.ID] = issueAttachment
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.Data["issuesAttachmentMap"] = issuesAttachmentMap
|
||||
}
|
||||
|
||||
linkedPrsMap := make(map[int64][]*issues_model.Issue)
|
||||
for _, issuesList := range issuesMap {
|
||||
for _, issue := range issuesList {
|
||||
|
||||
@@ -339,8 +339,8 @@ func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) {
|
||||
ctx.Data["HeadTarget"] = pull.MustHeadUserName(ctx) + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch
|
||||
}
|
||||
ctx.Data["BaseTarget"] = pull.BaseBranch
|
||||
ctx.Data["HeadBranchHTMLURL"] = pull.GetHeadBranchHTMLURL()
|
||||
ctx.Data["BaseBranchHTMLURL"] = pull.GetBaseBranchHTMLURL()
|
||||
ctx.Data["HeadBranchLink"] = pull.GetHeadBranchLink()
|
||||
ctx.Data["BaseBranchLink"] = pull.GetBaseBranchLink()
|
||||
}
|
||||
|
||||
// PrepareMergedViewPullInfo show meta information for a merged pull request view page
|
||||
|
||||
@@ -98,7 +98,7 @@ func CreateCodeComment(ctx *context.Context) {
|
||||
renderConversation(ctx, comment)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(comment.HTMLURL())
|
||||
ctx.Redirect(comment.Link())
|
||||
}
|
||||
|
||||
// UpdateResolveConversation add or remove an Conversation resolved mark
|
||||
|
||||
@@ -295,7 +295,7 @@ func LatestRelease(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Redirect(release.HTMLURL())
|
||||
ctx.Redirect(release.Link())
|
||||
}
|
||||
|
||||
// NewRelease render creating or edit release page
|
||||
|
||||
@@ -345,7 +345,7 @@ func acceptOrRejectRepoTransfer(ctx *context.Context, accept bool) error {
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.transfer.rejected"))
|
||||
}
|
||||
|
||||
ctx.Redirect(ctx.Repo.Repository.HTMLURL())
|
||||
ctx.Redirect(ctx.Repo.Repository.Link())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -570,6 +570,7 @@ func SearchRepo(ctx *context.Context) {
|
||||
Mirror: repo.IsMirror,
|
||||
Stars: repo.NumStars,
|
||||
HTMLURL: repo.HTMLURL(),
|
||||
Link: repo.Link(),
|
||||
Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func Search(ctx *context.Context) {
|
||||
ctx.Data["CodeIndexerUnavailable"] = !code_indexer.IsAvailable()
|
||||
}
|
||||
|
||||
ctx.Data["SourcePath"] = ctx.Repo.Repository.HTMLURL()
|
||||
ctx.Data["SourcePath"] = ctx.Repo.Repository.Link()
|
||||
ctx.Data["SearchResults"] = searchResults
|
||||
ctx.Data["SearchResultLanguages"] = searchResultLanguages
|
||||
|
||||
|
||||
@@ -544,6 +544,7 @@ func SettingsPost(ctx *context.Context) {
|
||||
AllowRebaseUpdate: form.PullsAllowRebaseUpdate,
|
||||
DefaultDeleteBranchAfterMerge: form.DefaultDeleteBranchAfterMerge,
|
||||
DefaultMergeStyle: repo_model.MergeStyle(form.PullsDefaultMergeStyle),
|
||||
DefaultAllowMaintainerEdit: form.DefaultAllowMaintainerEdit,
|
||||
},
|
||||
})
|
||||
} else if !unit_model.TypePullRequests.UnitGlobalDisabled() {
|
||||
@@ -1021,7 +1022,7 @@ func AddTeamPost(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
team, err := organization.OrgFromUser(ctx.Repo.Owner).GetTeam(name)
|
||||
team, err := organization.OrgFromUser(ctx.Repo.Owner).GetTeam(ctx, name)
|
||||
if err != nil {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
ctx.Flash.Error(ctx.Tr("form.team_not_exist"))
|
||||
|
||||
+73
-102
@@ -56,18 +56,15 @@ type namedBlob struct {
|
||||
blob *git.Blob
|
||||
}
|
||||
|
||||
// locate a README for a tree in one of the supported paths.
|
||||
//
|
||||
// entries is passed to reduce calls to ListEntries(), so
|
||||
// this has precondition:
|
||||
//
|
||||
// entries == ctx.Repo.Commit.SubTree(ctx.Repo.TreePath).ListEntries()
|
||||
//
|
||||
// FIXME: There has to be a more efficient way of doing this
|
||||
func getReadmeFileFromPath(ctx *context.Context, commit *git.Commit, treePath string) (*namedBlob, error) {
|
||||
tree, err := commit.SubTree(treePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
entries, err := tree.ListEntries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry) (*namedBlob, error) {
|
||||
// Create a list of extensions in priority order
|
||||
// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
|
||||
// 2. Txt files - e.g. README.txt
|
||||
@@ -75,16 +72,38 @@ func getReadmeFileFromPath(ctx *context.Context, commit *git.Commit, treePath st
|
||||
exts := append(localizedExtensions(".md", ctx.Language()), ".txt", "") // sorted by priority
|
||||
extCount := len(exts)
|
||||
readmeFiles := make([]*namedBlob, extCount+1)
|
||||
|
||||
docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
// as a special case for the top-level repo introduction README,
|
||||
// fall back to subfolders, looking for e.g. docs/README.md, .gitea/README.zh-CN.txt, .github/README.txt, ...
|
||||
// (note that docsEntries is ignored unless we are at the root)
|
||||
lowerName := strings.ToLower(entry.Name())
|
||||
switch lowerName {
|
||||
case "docs":
|
||||
if entry.Name() == "docs" || docsEntries[0] == nil {
|
||||
docsEntries[0] = entry
|
||||
}
|
||||
case ".gitea":
|
||||
if entry.Name() == ".gitea" || docsEntries[1] == nil {
|
||||
docsEntries[1] = entry
|
||||
}
|
||||
case ".github":
|
||||
if entry.Name() == ".github" || docsEntries[2] == nil {
|
||||
docsEntries[2] = entry
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if i, ok := markup.IsReadmeFileExtension(entry.Name(), exts...); ok {
|
||||
if i, ok := util.IsReadmeFileExtension(entry.Name(), exts...); ok {
|
||||
log.Debug("Potential readme file: %s", entry.Name())
|
||||
if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].name, entry.Blob().Name()) {
|
||||
name := entry.Name()
|
||||
isSymlink := entry.IsLink()
|
||||
target := entry
|
||||
if isSymlink {
|
||||
var err error
|
||||
target, err = entry.FollowLinks()
|
||||
if err != nil && !git.IsErrBadLink(err) {
|
||||
return nil, err
|
||||
@@ -107,6 +126,33 @@ func getReadmeFileFromPath(ctx *context.Context, commit *git.Commit, treePath st
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Repo.TreePath == "" && readmeFile == nil {
|
||||
for _, subTreeEntry := range docsEntries {
|
||||
if subTreeEntry == nil {
|
||||
continue
|
||||
}
|
||||
subTree := subTreeEntry.Tree()
|
||||
if subTree == nil {
|
||||
// this should be impossible; if subTreeEntry exists so should this.
|
||||
continue
|
||||
}
|
||||
var err error
|
||||
childEntries, err := subTree.ListEntries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
readmeFile, err = findReadmeFileInEntries(ctx, childEntries)
|
||||
if err != nil && !git.IsErrNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
if readmeFile != nil {
|
||||
readmeFile.name = subTreeEntry.Name() + "/" + readmeFile.name
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return readmeFile, nil
|
||||
}
|
||||
|
||||
@@ -127,12 +173,20 @@ func renderDirectory(ctx *context.Context, treeLink string) {
|
||||
ctx.Data["CanUploadFile"] = setting.Repository.Upload.Enabled && !ctx.Repo.Repository.IsArchived
|
||||
}
|
||||
|
||||
readmeFile, readmeTreelink := findReadmeFile(ctx, entries, treeLink)
|
||||
if ctx.Written() || readmeFile == nil {
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
||||
renderReadmeFile(ctx, readmeFile, readmeTreelink)
|
||||
readmeFile, err := findReadmeFileInEntries(ctx, entries)
|
||||
if err != nil {
|
||||
ctx.ServerError("findReadmeFileInEntries", err)
|
||||
return
|
||||
}
|
||||
if readmeFile == nil {
|
||||
return
|
||||
}
|
||||
|
||||
renderReadmeFile(ctx, readmeFile, treeLink)
|
||||
}
|
||||
|
||||
// localizedExtensions prepends the provided language code with and without a
|
||||
@@ -157,89 +211,6 @@ func localizedExtensions(ext, languageCode string) (localizedExts []string) {
|
||||
return []string{lowerLangCode + ext, ext}
|
||||
}
|
||||
|
||||
func findReadmeFile(ctx *context.Context, entries git.Entries, treeLink string) (*namedBlob, string) {
|
||||
// Create a list of extensions in priority order
|
||||
// 1. Markdown files - with and without localisation - e.g. README.en-us.md or README.md
|
||||
// 2. Txt files - e.g. README.txt
|
||||
// 3. No extension - e.g. README
|
||||
exts := append(localizedExtensions(".md", ctx.Language()), ".txt", "") // sorted by priority
|
||||
extCount := len(exts)
|
||||
readmeFiles := make([]*namedBlob, extCount+1)
|
||||
|
||||
docsEntries := make([]*git.TreeEntry, 3) // (one of docs/, .gitea/ or .github/)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
lowerName := strings.ToLower(entry.Name())
|
||||
switch lowerName {
|
||||
case "docs":
|
||||
if entry.Name() == "docs" || docsEntries[0] == nil {
|
||||
docsEntries[0] = entry
|
||||
}
|
||||
case ".gitea":
|
||||
if entry.Name() == ".gitea" || docsEntries[1] == nil {
|
||||
docsEntries[1] = entry
|
||||
}
|
||||
case ".github":
|
||||
if entry.Name() == ".github" || docsEntries[2] == nil {
|
||||
docsEntries[2] = entry
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if i, ok := markup.IsReadmeFileExtension(entry.Name(), exts...); ok {
|
||||
log.Debug("Potential readme file: %s", entry.Name())
|
||||
name := entry.Name()
|
||||
isSymlink := entry.IsLink()
|
||||
target := entry
|
||||
if isSymlink {
|
||||
var err error
|
||||
target, err = entry.FollowLinks()
|
||||
if err != nil && !git.IsErrBadLink(err) {
|
||||
ctx.ServerError("FollowLinks", err)
|
||||
return nil, ""
|
||||
}
|
||||
}
|
||||
if target != nil && (target.IsExecutable() || target.IsRegular()) {
|
||||
readmeFiles[i] = &namedBlob{
|
||||
name,
|
||||
isSymlink,
|
||||
target.Blob(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var readmeFile *namedBlob
|
||||
readmeTreelink := treeLink
|
||||
for _, f := range readmeFiles {
|
||||
if f != nil {
|
||||
readmeFile = f
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.Repo.TreePath == "" && readmeFile == nil {
|
||||
for _, entry := range docsEntries {
|
||||
if entry == nil {
|
||||
continue
|
||||
}
|
||||
var err error
|
||||
readmeFile, err = getReadmeFileFromPath(ctx, ctx.Repo.Commit, entry.GetSubJumpablePathName())
|
||||
if err != nil {
|
||||
ctx.ServerError("getReadmeFileFromPath", err)
|
||||
return nil, ""
|
||||
}
|
||||
if readmeFile != nil {
|
||||
readmeFile.name = entry.Name() + "/" + readmeFile.name
|
||||
readmeTreelink = treeLink + "/" + util.PathEscapeSegments(entry.GetSubJumpablePathName())
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return readmeFile, readmeTreelink
|
||||
}
|
||||
|
||||
type fileInfo struct {
|
||||
isTextFile bool
|
||||
isLFSFile bool
|
||||
@@ -318,7 +289,7 @@ func renderReadmeFile(ctx *context.Context, readmeFile *namedBlob, readmeTreelin
|
||||
|
||||
if fInfo.isLFSFile {
|
||||
filenameBase64 := base64.RawURLEncoding.EncodeToString([]byte(readmeFile.name))
|
||||
ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.HTMLURL(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64))
|
||||
ctx.Data["RawFileLink"] = fmt.Sprintf("%s.git/info/lfs/objects/%s/%s", ctx.Repo.Repository.Link(), url.PathEscape(fInfo.lfsMeta.Oid), url.PathEscape(filenameBase64))
|
||||
}
|
||||
|
||||
if !fInfo.isTextFile {
|
||||
@@ -342,7 +313,7 @@ func renderReadmeFile(ctx *context.Context, readmeFile *namedBlob, readmeTreelin
|
||||
ctx.Data["EscapeStatus"], ctx.Data["FileContent"], err = markupRender(ctx, &markup.RenderContext{
|
||||
Ctx: ctx,
|
||||
RelativePath: path.Join(ctx.Repo.TreePath, readmeFile.name), // ctx.Repo.TreePath is the directory not the Readme so we must append the Readme filename (and path).
|
||||
URLPrefix: readmeTreelink,
|
||||
URLPrefix: path.Dir(readmeTreelink),
|
||||
Metas: ctx.Repo.Repository.ComposeDocumentMetas(),
|
||||
GitRepo: ctx.Repo.GitRepo,
|
||||
}, rd)
|
||||
@@ -452,7 +423,7 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry, treeLink, rawLink st
|
||||
rd := charset.ToUTF8WithFallbackReader(io.MultiReader(bytes.NewReader(buf), dataRc))
|
||||
|
||||
shouldRenderSource := ctx.FormString("display") == "source"
|
||||
readmeExist := markup.IsReadmeFile(blob.Name())
|
||||
readmeExist := util.IsReadmeFileName(blob.Name())
|
||||
ctx.Data["ReadmeExist"] = readmeExist
|
||||
|
||||
markupType := markup.Type(blob.Name())
|
||||
@@ -738,7 +709,7 @@ func Home(ctx *context.Context) {
|
||||
}
|
||||
|
||||
ctx.Data["EnableFeed"] = true
|
||||
ctx.Data["FeedURL"] = ctx.Repo.Repository.HTMLURL()
|
||||
ctx.Data["FeedURL"] = ctx.Repo.Repository.Link()
|
||||
}
|
||||
|
||||
checkHomeCodeViewable(ctx)
|
||||
|
||||
@@ -376,7 +376,7 @@ func PackageSettingsPost(ctx *context.Context) {
|
||||
ctx.Flash.Success(ctx.Tr("packages.settings.delete.success"))
|
||||
}
|
||||
|
||||
ctx.Redirect(ctx.Package.Owner.HTMLURL() + "/-/packages")
|
||||
ctx.Redirect(ctx.Package.Owner.HomeLink() + "/-/packages")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func Profile(ctx *context.Context) {
|
||||
}
|
||||
|
||||
// advertise feed via meta tag
|
||||
ctx.Data["FeedURL"] = ctx.ContextUser.HTMLURL()
|
||||
ctx.Data["FeedURL"] = ctx.ContextUser.HomeLink()
|
||||
|
||||
// Show OpenID URIs
|
||||
openIDs, err := user_model.GetUserOpenIDs(ctx.ContextUser.ID)
|
||||
|
||||
+1
-1
@@ -203,7 +203,7 @@ func Routes(ctx gocontext.Context) *web.Route {
|
||||
}
|
||||
|
||||
// Get user from session if logged in.
|
||||
common = append(common, context.Auth(group))
|
||||
common = append(common, auth_service.Auth(group))
|
||||
|
||||
// GetHead allows a HEAD request redirect to GET if HEAD method is not defined for that route
|
||||
common = append(common, middleware.GetHead)
|
||||
|
||||
@@ -59,7 +59,7 @@ func CreateCommitStatus(ctx context.Context, job *actions_model.ActionRunJob) er
|
||||
Creator: creator,
|
||||
CommitStatus: &git_model.CommitStatus{
|
||||
SHA: sha,
|
||||
TargetURL: run.HTMLURL(),
|
||||
TargetURL: run.Link(),
|
||||
Description: "",
|
||||
Context: ctxname,
|
||||
CreatorID: payload.Pusher.ID,
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/web/middleware"
|
||||
)
|
||||
|
||||
// Auth is a middleware to authenticate a web user
|
||||
func Auth(authMethod Method) func(*context.Context) {
|
||||
return func(ctx *context.Context) {
|
||||
if err := authShared(ctx, authMethod); err != nil {
|
||||
log.Error("Failed to verify user: %v", err)
|
||||
ctx.Error(http.StatusUnauthorized, "Verify")
|
||||
return
|
||||
}
|
||||
if ctx.Doer == nil {
|
||||
// ensure the session uid is deleted
|
||||
_ = ctx.Session.Delete("uid")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// APIAuth is a middleware to authenticate an api user
|
||||
func APIAuth(authMethod Method) func(*context.APIContext) {
|
||||
return func(ctx *context.APIContext) {
|
||||
if err := authShared(ctx.Context, authMethod); err != nil {
|
||||
ctx.Error(http.StatusUnauthorized, "APIAuth", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func authShared(ctx *context.Context, authMethod Method) error {
|
||||
var err error
|
||||
ctx.Doer, err = authMethod.Verify(ctx.Req, ctx.Resp, ctx, ctx.Session)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ctx.Doer != nil {
|
||||
if ctx.Locale.Language() != ctx.Doer.Language {
|
||||
ctx.Locale = middleware.Locale(ctx.Resp, ctx.Req)
|
||||
}
|
||||
ctx.IsBasicAuth = ctx.Data["AuthedMethod"].(string) == BasicMethodName
|
||||
ctx.IsSigned = true
|
||||
ctx.Data["IsSigned"] = ctx.IsSigned
|
||||
ctx.Data["SignedUser"] = ctx.Doer
|
||||
ctx.Data["SignedUserID"] = ctx.Doer.ID
|
||||
ctx.Data["SignedUserName"] = ctx.Doer.Name
|
||||
ctx.Data["IsAdmin"] = ctx.Doer.IsAdmin
|
||||
} else {
|
||||
ctx.Data["SignedUserID"] = int64(0)
|
||||
ctx.Data["SignedUserName"] = ""
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -10,9 +10,10 @@ import (
|
||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
auth_module "code.gitea.io/gitea/modules/auth"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
source_service "code.gitea.io/gitea/services/auth/source"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
@@ -64,61 +65,66 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
|
||||
orgCache := make(map[string]*organization.Organization)
|
||||
teamCache := make(map[string]*organization.Team)
|
||||
source.SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, orgCache, teamCache)
|
||||
}
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(user, source.authSource, sr.SSHPublicKey) {
|
||||
return user, asymkey_model.RewriteAllPublicKeys()
|
||||
if err := asymkey_model.RewriteAllPublicKeys(); err != nil {
|
||||
return user, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback.
|
||||
if len(sr.Username) == 0 {
|
||||
sr.Username = userName
|
||||
}
|
||||
|
||||
if len(sr.Mail) == 0 {
|
||||
sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
|
||||
}
|
||||
|
||||
user = &user_model.User{
|
||||
LowerName: strings.ToLower(sr.Username),
|
||||
Name: sr.Username,
|
||||
FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
|
||||
Email: sr.Mail,
|
||||
LoginType: source.authSource.Type,
|
||||
LoginSource: source.authSource.ID,
|
||||
LoginName: userName,
|
||||
IsAdmin: sr.IsAdmin,
|
||||
}
|
||||
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
||||
IsRestricted: util.OptionalBoolOf(sr.IsRestricted),
|
||||
IsActive: util.OptionalBoolTrue,
|
||||
}
|
||||
|
||||
err := user_model.CreateUser(user, overwriteDefault)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
mailer.SendRegisterNotifyMail(user)
|
||||
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(user, source.authSource, sr.SSHPublicKey) {
|
||||
if err := asymkey_model.RewriteAllPublicKeys(); err != nil {
|
||||
return user, err
|
||||
}
|
||||
}
|
||||
if len(source.AttributeAvatar) > 0 {
|
||||
if err := user_service.UploadAvatar(user, sr.Avatar); err != nil {
|
||||
return user, err
|
||||
}
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// Fallback.
|
||||
if len(sr.Username) == 0 {
|
||||
sr.Username = userName
|
||||
}
|
||||
|
||||
if len(sr.Mail) == 0 {
|
||||
sr.Mail = fmt.Sprintf("%s@localhost", sr.Username)
|
||||
}
|
||||
|
||||
user = &user_model.User{
|
||||
LowerName: strings.ToLower(sr.Username),
|
||||
Name: sr.Username,
|
||||
FullName: composeFullName(sr.Name, sr.Surname, sr.Username),
|
||||
Email: sr.Mail,
|
||||
LoginType: source.authSource.Type,
|
||||
LoginSource: source.authSource.ID,
|
||||
LoginName: userName,
|
||||
IsAdmin: sr.IsAdmin,
|
||||
}
|
||||
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
||||
IsRestricted: util.OptionalBoolOf(sr.IsRestricted),
|
||||
IsActive: util.OptionalBoolTrue,
|
||||
}
|
||||
|
||||
err := user_model.CreateUser(user, overwriteDefault)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
|
||||
mailer.SendRegisterNotifyMail(user)
|
||||
|
||||
if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(user, source.authSource, sr.SSHPublicKey) {
|
||||
err = asymkey_model.RewriteAllPublicKeys()
|
||||
}
|
||||
if err == nil && len(source.AttributeAvatar) > 0 {
|
||||
_ = user_service.UploadAvatar(user, sr.Avatar)
|
||||
}
|
||||
if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
|
||||
orgCache := make(map[string]*organization.Organization)
|
||||
teamCache := make(map[string]*organization.Team)
|
||||
source.SyncLdapGroupsToTeams(user, sr.LdapTeamAdd, sr.LdapTeamRemove, orgCache, teamCache)
|
||||
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
if err := source_service.SyncGroupsToTeams(db.DefaultContext, user, sr.Groups, groupTeamMapping, source.GroupTeamMapRemoval); err != nil {
|
||||
return user, err
|
||||
}
|
||||
}
|
||||
|
||||
return user, err
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// IsSkipLocalTwoFA returns if this source should skip local 2fa for password authentication
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
// Copyright 2021 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
// SyncLdapGroupsToTeams maps LDAP groups to organization and team memberships
|
||||
func (source *Source) SyncLdapGroupsToTeams(user *user_model.User, ldapTeamAdd, ldapTeamRemove map[string][]string, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) {
|
||||
var err error
|
||||
if source.GroupsEnabled && source.GroupTeamMapRemoval {
|
||||
// when the user is not a member of configs LDAP group, remove mapped organizations/teams memberships
|
||||
removeMappedMemberships(user, ldapTeamRemove, orgCache, teamCache)
|
||||
}
|
||||
for orgName, teamNames := range ldapTeamAdd {
|
||||
org, ok := orgCache[orgName]
|
||||
if !ok {
|
||||
org, err = organization.GetOrgByName(orgName)
|
||||
if err != nil {
|
||||
// organization must be created before LDAP group sync
|
||||
log.Warn("LDAP group sync: Could not find organisation %s: %v", orgName, err)
|
||||
continue
|
||||
}
|
||||
orgCache[orgName] = org
|
||||
}
|
||||
|
||||
for _, teamName := range teamNames {
|
||||
team, ok := teamCache[orgName+teamName]
|
||||
if !ok {
|
||||
team, err = org.GetTeam(teamName)
|
||||
if err != nil {
|
||||
// team must be created before LDAP group sync
|
||||
log.Warn("LDAP group sync: Could not find team %s: %v", teamName, err)
|
||||
continue
|
||||
}
|
||||
teamCache[orgName+teamName] = team
|
||||
}
|
||||
if isMember, err := organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID); !isMember && err == nil {
|
||||
log.Trace("LDAP group sync: adding user [%s] to team [%s]", user.Name, org.Name)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
err := models.AddTeamMember(team, user.ID)
|
||||
if err != nil {
|
||||
log.Error("LDAP group sync: Could not add user to team: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// remove membership to organizations/teams if user is not member of corresponding LDAP group
|
||||
// e.g. lets assume user is member of LDAP group "x", but LDAP group team map contains LDAP groups "x" and "y"
|
||||
// then users membership gets removed for all organizations/teams mapped by LDAP group "y"
|
||||
func removeMappedMemberships(user *user_model.User, ldapTeamRemove map[string][]string, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) {
|
||||
var err error
|
||||
for orgName, teamNames := range ldapTeamRemove {
|
||||
org, ok := orgCache[orgName]
|
||||
if !ok {
|
||||
org, err = organization.GetOrgByName(orgName)
|
||||
if err != nil {
|
||||
// organization must be created before LDAP group sync
|
||||
log.Warn("LDAP group sync: Could not find organisation %s: %v", orgName, err)
|
||||
continue
|
||||
}
|
||||
orgCache[orgName] = org
|
||||
}
|
||||
for _, teamName := range teamNames {
|
||||
team, ok := teamCache[orgName+teamName]
|
||||
if !ok {
|
||||
team, err = org.GetTeam(teamName)
|
||||
if err != nil {
|
||||
// team must must be created before LDAP group sync
|
||||
log.Warn("LDAP group sync: Could not find team %s: %v", teamName, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if isMember, err := organization.IsTeamMember(db.DefaultContext, org.ID, team.ID, user.ID); isMember && err == nil {
|
||||
log.Trace("LDAP group sync: removing user [%s] from team [%s]", user.Name, org.Name)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
err = models.RemoveTeamMember(team, user.ID)
|
||||
if err != nil {
|
||||
log.Error("LDAP group sync: Could not remove user from team: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,26 +11,24 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
)
|
||||
|
||||
// SearchResult : user data
|
||||
type SearchResult struct {
|
||||
Username string // Username
|
||||
Name string // Name
|
||||
Surname string // Surname
|
||||
Mail string // E-mail address
|
||||
SSHPublicKey []string // SSH Public Key
|
||||
IsAdmin bool // if user is administrator
|
||||
IsRestricted bool // if user is restricted
|
||||
LowerName string // LowerName
|
||||
Avatar []byte
|
||||
LdapTeamAdd map[string][]string // organizations teams to add
|
||||
LdapTeamRemove map[string][]string // organizations teams to remove
|
||||
Username string // Username
|
||||
Name string // Name
|
||||
Surname string // Surname
|
||||
Mail string // E-mail address
|
||||
SSHPublicKey []string // SSH Public Key
|
||||
IsAdmin bool // if user is administrator
|
||||
IsRestricted bool // if user is restricted
|
||||
LowerName string // LowerName
|
||||
Avatar []byte
|
||||
Groups container.Set[string]
|
||||
}
|
||||
|
||||
func (source *Source) sanitizedUserQuery(username string) (string, bool) {
|
||||
@@ -196,9 +194,8 @@ 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, applyGroupFilter bool) []string {
|
||||
var ldapGroups []string
|
||||
var searchFilter string
|
||||
func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGroupFilter bool) container.Set[string] {
|
||||
ldapGroups := make(container.Set[string])
|
||||
|
||||
groupFilter, ok := source.sanitizedGroupFilter(source.GroupFilter)
|
||||
if !ok {
|
||||
@@ -210,12 +207,12 @@ func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGr
|
||||
return ldapGroups
|
||||
}
|
||||
|
||||
var searchFilter string
|
||||
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(
|
||||
groupDN,
|
||||
ldap.ScopeWholeSubtree,
|
||||
@@ -237,44 +234,12 @@ func (source *Source) listLdapGroupMemberships(l *ldap.Conn, uid string, applyGr
|
||||
log.Error("LDAP search was successful, but found no DN!")
|
||||
continue
|
||||
}
|
||||
ldapGroups = append(ldapGroups, entry.DN)
|
||||
ldapGroups.Add(entry.DN)
|
||||
}
|
||||
|
||||
return ldapGroups
|
||||
}
|
||||
|
||||
// parse LDAP groups and return map of ldap groups to organizations teams
|
||||
func (source *Source) mapLdapGroupsToTeams() map[string]map[string][]string {
|
||||
ldapGroupsToTeams := make(map[string]map[string][]string)
|
||||
err := json.Unmarshal([]byte(source.GroupTeamMap), &ldapGroupsToTeams)
|
||||
if err != nil {
|
||||
log.Error("Failed to unmarshall LDAP teams map: %v", err)
|
||||
return ldapGroupsToTeams
|
||||
}
|
||||
return ldapGroupsToTeams
|
||||
}
|
||||
|
||||
// getMappedMemberships : returns the organizations and teams to modify the users membership
|
||||
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{}
|
||||
membershipsToRemove := map[string][]string{}
|
||||
for group, memberships := range ldapGroupsToTeams {
|
||||
isUserInGroup := util.SliceContainsString(usersLdapGroups, group)
|
||||
if isUserInGroup {
|
||||
for org, teams := range memberships {
|
||||
membershipsToAdd[org] = teams
|
||||
}
|
||||
} else if !isUserInGroup {
|
||||
for org, teams := range memberships {
|
||||
membershipsToRemove[org] = teams
|
||||
}
|
||||
}
|
||||
}
|
||||
return membershipsToAdd, membershipsToRemove
|
||||
}
|
||||
|
||||
func (source *Source) getUserAttributeListedInGroup(entry *ldap.Entry) string {
|
||||
if strings.ToLower(source.UserUID) == "dn" {
|
||||
return entry.DN
|
||||
@@ -399,23 +364,6 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
|
||||
surname := sr.Entries[0].GetAttributeValue(source.AttributeSurname)
|
||||
mail := sr.Entries[0].GetAttributeValue(source.AttributeMail)
|
||||
|
||||
teamsToAdd := make(map[string][]string)
|
||||
teamsToRemove := make(map[string][]string)
|
||||
|
||||
// Check group membership
|
||||
if source.GroupsEnabled {
|
||||
userAttributeListedInGroup := source.getUserAttributeListedInGroup(sr.Entries[0])
|
||||
usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
|
||||
|
||||
if source.GroupFilter != "" && len(usersLdapGroups) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if source.GroupTeamMap != "" || source.GroupTeamMapRemoval {
|
||||
teamsToAdd, teamsToRemove = source.getMappedMemberships(usersLdapGroups, userAttributeListedInGroup)
|
||||
}
|
||||
}
|
||||
|
||||
if isAttributeSSHPublicKeySet {
|
||||
sshPublicKey = sr.Entries[0].GetAttributeValues(source.AttributeSSHPublicKey)
|
||||
}
|
||||
@@ -431,6 +379,17 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
|
||||
Avatar = sr.Entries[0].GetRawAttributeValue(source.AttributeAvatar)
|
||||
}
|
||||
|
||||
// Check group membership
|
||||
var usersLdapGroups container.Set[string]
|
||||
if source.GroupsEnabled {
|
||||
userAttributeListedInGroup := source.getUserAttributeListedInGroup(sr.Entries[0])
|
||||
usersLdapGroups = source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
|
||||
|
||||
if source.GroupFilter != "" && len(usersLdapGroups) == 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if !directBind && source.AttributesInBind {
|
||||
// binds user (checking password) after looking-up attributes in BindDN context
|
||||
err = bindUser(l, userDN, passwd)
|
||||
@@ -440,17 +399,16 @@ func (source *Source) SearchEntry(name, passwd string, directBind bool) *SearchR
|
||||
}
|
||||
|
||||
return &SearchResult{
|
||||
LowerName: strings.ToLower(username),
|
||||
Username: username,
|
||||
Name: firstname,
|
||||
Surname: surname,
|
||||
Mail: mail,
|
||||
SSHPublicKey: sshPublicKey,
|
||||
IsAdmin: isAdmin,
|
||||
IsRestricted: isRestricted,
|
||||
Avatar: Avatar,
|
||||
LdapTeamAdd: teamsToAdd,
|
||||
LdapTeamRemove: teamsToRemove,
|
||||
LowerName: strings.ToLower(username),
|
||||
Username: username,
|
||||
Name: firstname,
|
||||
Surname: surname,
|
||||
Mail: mail,
|
||||
SSHPublicKey: sshPublicKey,
|
||||
IsAdmin: isAdmin,
|
||||
IsRestricted: isRestricted,
|
||||
Avatar: Avatar,
|
||||
Groups: usersLdapGroups,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,33 +470,29 @@ func (source *Source) SearchEntries() ([]*SearchResult, error) {
|
||||
result := make([]*SearchResult, 0, len(sr.Entries))
|
||||
|
||||
for _, v := range sr.Entries {
|
||||
teamsToAdd := make(map[string][]string)
|
||||
teamsToRemove := make(map[string][]string)
|
||||
|
||||
var usersLdapGroups container.Set[string]
|
||||
if source.GroupsEnabled {
|
||||
userAttributeListedInGroup := source.getUserAttributeListedInGroup(v)
|
||||
|
||||
if source.GroupFilter != "" {
|
||||
usersLdapGroups := source.listLdapGroupMemberships(l, userAttributeListedInGroup, true)
|
||||
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)
|
||||
usersLdapGroups = source.listLdapGroupMemberships(l, userAttributeListedInGroup, false)
|
||||
}
|
||||
}
|
||||
|
||||
user := &SearchResult{
|
||||
Username: v.GetAttributeValue(source.AttributeUsername),
|
||||
Name: v.GetAttributeValue(source.AttributeName),
|
||||
Surname: v.GetAttributeValue(source.AttributeSurname),
|
||||
Mail: v.GetAttributeValue(source.AttributeMail),
|
||||
IsAdmin: checkAdmin(l, source, v.DN),
|
||||
LdapTeamAdd: teamsToAdd,
|
||||
LdapTeamRemove: teamsToRemove,
|
||||
Username: v.GetAttributeValue(source.AttributeUsername),
|
||||
Name: v.GetAttributeValue(source.AttributeName),
|
||||
Surname: v.GetAttributeValue(source.AttributeSurname),
|
||||
Mail: v.GetAttributeValue(source.AttributeMail),
|
||||
IsAdmin: checkAdmin(l, source, v.DN),
|
||||
Groups: usersLdapGroups,
|
||||
}
|
||||
|
||||
if !user.IsAdmin {
|
||||
|
||||
@@ -13,8 +13,10 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
auth_module "code.gitea.io/gitea/modules/auth"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
source_service "code.gitea.io/gitea/services/auth/source"
|
||||
user_service "code.gitea.io/gitea/services/user"
|
||||
)
|
||||
|
||||
@@ -65,6 +67,11 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
orgCache := make(map[string]*organization.Organization)
|
||||
teamCache := make(map[string]*organization.Team)
|
||||
|
||||
groupTeamMapping, err := auth_module.UnmarshalGroupTeamMapping(source.GroupTeamMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, su := range sr {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -173,7 +180,9 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
|
||||
}
|
||||
// Synchronize LDAP groups with organization and team memberships
|
||||
if source.GroupsEnabled && (source.GroupTeamMap != "" || source.GroupTeamMapRemoval) {
|
||||
source.SyncLdapGroupsToTeams(usr, su.LdapTeamAdd, su.LdapTeamRemove, orgCache, teamCache)
|
||||
if err := source_service.SyncGroupsToTeamsCached(ctx, usr, su.Groups, groupTeamMapping, source.GroupTeamMapRemoval, orgCache, teamCache); err != nil {
|
||||
log.Error("SyncGroupsToTeamsCached: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/json"
|
||||
)
|
||||
|
||||
// ________ _____ __ .__ ________
|
||||
// \_____ \ / _ \ __ ___/ |_| |__ \_____ \
|
||||
// / | \ / /_\ \| | \ __\ | \ / ____/
|
||||
// / | \/ | \ | /| | | Y \/ \
|
||||
// \_______ /\____|__ /____/ |__| |___| /\_______ \
|
||||
// \/ \/ \/ \/
|
||||
|
||||
// Source holds configuration for the OAuth2 login source.
|
||||
type Source struct {
|
||||
Provider string
|
||||
@@ -24,13 +17,15 @@ type Source struct {
|
||||
CustomURLMapping *CustomURLMapping
|
||||
IconURL string
|
||||
|
||||
Scopes []string
|
||||
RequiredClaimName string
|
||||
RequiredClaimValue string
|
||||
GroupClaimName string
|
||||
AdminGroup string
|
||||
RestrictedGroup string
|
||||
SkipLocalTwoFA bool `json:",omitempty"`
|
||||
Scopes []string
|
||||
RequiredClaimName string
|
||||
RequiredClaimValue string
|
||||
GroupClaimName string
|
||||
AdminGroup string
|
||||
GroupTeamMap string
|
||||
GroupTeamMapRemoval bool
|
||||
RestrictedGroup string
|
||||
SkipLocalTwoFA bool `json:",omitempty"`
|
||||
|
||||
// reference to the authSource
|
||||
authSource *auth.Source
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
)
|
||||
|
||||
type syncType int
|
||||
|
||||
const (
|
||||
syncAdd syncType = iota
|
||||
syncRemove
|
||||
)
|
||||
|
||||
// SyncGroupsToTeams maps authentication source groups to organization and team memberships
|
||||
func SyncGroupsToTeams(ctx context.Context, user *user_model.User, sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string, performRemoval bool) error {
|
||||
orgCache := make(map[string]*organization.Organization)
|
||||
teamCache := make(map[string]*organization.Team)
|
||||
return SyncGroupsToTeamsCached(ctx, user, sourceUserGroups, sourceGroupTeamMapping, performRemoval, orgCache, teamCache)
|
||||
}
|
||||
|
||||
// SyncGroupsToTeamsCached maps authentication source groups to organization and team memberships
|
||||
func SyncGroupsToTeamsCached(ctx context.Context, user *user_model.User, sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string, performRemoval bool, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) error {
|
||||
membershipsToAdd, membershipsToRemove := resolveMappedMemberships(sourceUserGroups, sourceGroupTeamMapping)
|
||||
|
||||
if performRemoval {
|
||||
if err := syncGroupsToTeamsCached(ctx, user, membershipsToRemove, syncRemove, orgCache, teamCache); err != nil {
|
||||
return fmt.Errorf("could not sync[remove] user groups: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := syncGroupsToTeamsCached(ctx, user, membershipsToAdd, syncAdd, orgCache, teamCache); err != nil {
|
||||
return fmt.Errorf("could not sync[add] user groups: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveMappedMemberships(sourceUserGroups container.Set[string], sourceGroupTeamMapping map[string]map[string][]string) (map[string][]string, map[string][]string) {
|
||||
membershipsToAdd := map[string][]string{}
|
||||
membershipsToRemove := map[string][]string{}
|
||||
for group, memberships := range sourceGroupTeamMapping {
|
||||
isUserInGroup := sourceUserGroups.Contains(group)
|
||||
if isUserInGroup {
|
||||
for org, teams := range memberships {
|
||||
membershipsToAdd[org] = teams
|
||||
}
|
||||
} else {
|
||||
for org, teams := range memberships {
|
||||
membershipsToRemove[org] = teams
|
||||
}
|
||||
}
|
||||
}
|
||||
return membershipsToAdd, membershipsToRemove
|
||||
}
|
||||
|
||||
func syncGroupsToTeamsCached(ctx context.Context, user *user_model.User, orgTeamMap map[string][]string, action syncType, orgCache map[string]*organization.Organization, teamCache map[string]*organization.Team) error {
|
||||
for orgName, teamNames := range orgTeamMap {
|
||||
var err error
|
||||
org, ok := orgCache[orgName]
|
||||
if !ok {
|
||||
org, err = organization.GetOrgByName(ctx, orgName)
|
||||
if err != nil {
|
||||
if organization.IsErrOrgNotExist(err) {
|
||||
// organization must be created before group sync
|
||||
log.Warn("group sync: Could not find organisation %s: %v", orgName, err)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
orgCache[orgName] = org
|
||||
}
|
||||
for _, teamName := range teamNames {
|
||||
team, ok := teamCache[orgName+teamName]
|
||||
if !ok {
|
||||
team, err = org.GetTeam(ctx, teamName)
|
||||
if err != nil {
|
||||
if organization.IsErrTeamNotExist(err) {
|
||||
// team must be created before group sync
|
||||
log.Warn("group sync: Could not find team %s: %v", teamName, err)
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
teamCache[orgName+teamName] = team
|
||||
}
|
||||
|
||||
isMember, err := organization.IsTeamMember(ctx, org.ID, team.ID, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if action == syncAdd && !isMember {
|
||||
if err := models.AddTeamMember(team, user.ID); err != nil {
|
||||
log.Error("group sync: Could not add user to team: %v", err)
|
||||
return err
|
||||
}
|
||||
} else if action == syncRemove && isMember {
|
||||
if err := models.RemoveTeamMember(team, user.ID); err != nil {
|
||||
log.Error("group sync: Could not remove user from team: %v", err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -81,6 +81,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.Acc
|
||||
allowRebaseUpdate := false
|
||||
defaultDeleteBranchAfterMerge := false
|
||||
defaultMergeStyle := repo_model.MergeStyleMerge
|
||||
defaultAllowMaintainerEdit := false
|
||||
if unit, err := repo.GetUnit(ctx, unit_model.TypePullRequests); err == nil {
|
||||
config := unit.PullRequestsConfig()
|
||||
hasPullRequests = true
|
||||
@@ -92,6 +93,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.Acc
|
||||
allowRebaseUpdate = config.AllowRebaseUpdate
|
||||
defaultDeleteBranchAfterMerge = config.DefaultDeleteBranchAfterMerge
|
||||
defaultMergeStyle = config.GetDefaultMergeStyle()
|
||||
defaultAllowMaintainerEdit = config.DefaultAllowMaintainerEdit
|
||||
}
|
||||
hasProjects := false
|
||||
if _, err := repo.GetUnit(ctx, unit_model.TypeProjects); err == nil {
|
||||
@@ -182,6 +184,7 @@ func innerToRepo(ctx context.Context, repo *repo_model.Repository, mode perm.Acc
|
||||
AllowRebaseUpdate: allowRebaseUpdate,
|
||||
DefaultDeleteBranchAfterMerge: defaultDeleteBranchAfterMerge,
|
||||
DefaultMergeStyle: string(defaultMergeStyle),
|
||||
DefaultAllowMaintainerEdit: defaultAllowMaintainerEdit,
|
||||
AvatarURL: repo.AvatarLink(),
|
||||
Internal: !repo.IsPrivate && repo.Owner.Visibility == api.VisibleTypePrivate,
|
||||
MirrorInterval: mirrorInterval,
|
||||
|
||||
@@ -72,13 +72,15 @@ type AuthenticationForm struct {
|
||||
Oauth2GroupClaimName string
|
||||
Oauth2AdminGroup string
|
||||
Oauth2RestrictedGroup string
|
||||
Oauth2GroupTeamMap string `binding:"ValidGroupTeamMap"`
|
||||
Oauth2GroupTeamMapRemoval bool
|
||||
SkipLocalTwoFA bool
|
||||
SSPIAutoCreateUsers bool
|
||||
SSPIAutoActivateUsers bool
|
||||
SSPIStripDomainNames bool
|
||||
SSPISeparatorReplacement string `binding:"AlphaDashDot;MaxSize(5)"`
|
||||
SSPIDefaultLanguage string
|
||||
GroupTeamMap string
|
||||
GroupTeamMap string `binding:"ValidGroupTeamMap"`
|
||||
GroupTeamMapRemoval bool
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user