Merge remote-tracking branch 'upstream/main' into limit-repo-size

This commit is contained in:
DmitryFrolovTri
2023-02-14 07:59:50 +00:00
277 changed files with 2807 additions and 2066 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ linters:
fast: false
run:
go: 1.20
go: "1.20"
timeout: 10m
skip-dirs:
- node_modules
+17
View File
@@ -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为例):
所有打包的二进制程序均包 SQLiteMySQL 和 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`:
+48 -7
View File
@@ -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)
+1 -1
View File
@@ -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) {
+16
View File
@@ -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)
+1 -1
View File
@@ -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
)
+26
View File
@@ -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)
+5 -2
View File
@@ -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
+26 -7
View File
@@ -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
+1 -1
View File
@@ -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 {
+7 -7
View File
@@ -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
+6 -6
View File
@@ -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
+2
View File
@@ -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),
}
+17
View File
@@ -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))
}
+6 -14
View File
@@ -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 {
+8 -8
View File
@@ -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))
}
+1 -1
View File
@@ -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
+21
View File
@@ -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
+30 -5
View File
@@ -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
}
+1
View File
@@ -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(),
+15
View File
@@ -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)
+5
View File
@@ -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
View File
@@ -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)
}
+1
View File
@@ -125,6 +125,7 @@ type PullRequestsConfig struct {
AllowRebaseUpdate bool
DefaultDeleteBranchAfterMerge bool
DefaultMergeStyle MergeStyle
DefaultAllowMaintainerEdit bool
}
// FromDB fills up a PullRequestsConfig from serialized format.
+22
View File
@@ -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
}
+7 -17
View File
@@ -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
}
-30
View File
@@ -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 {
-32
View File
@@ -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)
+1 -1
View File
@@ -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)
+3 -3
View File
@@ -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
View File
@@ -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
}
+1 -1
View File
@@ -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
View File
@@ -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)
+9
View File
@@ -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() {
+3 -2
View File
@@ -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
}
+3 -2
View File
@@ -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
}
+2 -2
View File
@@ -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)
-38
View File
@@ -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
}
-91
View File
@@ -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)
}
}
+24
View File
@@ -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,
+1 -2
View File
@@ -8,8 +8,7 @@ import "time"
// Commentable can be commented upon
type Commentable interface {
GetLocalIndex() int64
GetForeignIndex() int64
Reviewable
GetContext() DownloaderContext
}
+11 -2
View File
@@ -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 }
+11 -1
View File
@@ -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
+14
View File
@@ -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
}
+2 -2
View File
@@ -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.
+1 -1
View File
@@ -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
}
+4
View File
@@ -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,
+4
View File
@@ -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 {
+39
View File
@@ -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
}
+81
View File
@@ -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)
}
}
+21 -3
View File
@@ -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 {
+2
View File
@@ -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 != "" {
+9 -6
View File
@@ -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
+12 -3
View File
@@ -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() {
+48 -5
View File
@@ -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")
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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)
+5 -1
View File
@@ -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,
+2
View File
@@ -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,
}
}
+7
View File
@@ -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
View File
@@ -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}
}
}
+1 -1
View File
@@ -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() + " "
+2 -2
View File
@@ -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
}
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -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)
+7
View File
@@ -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)
}
+7 -7
View File
@@ -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
}
+1 -1
View File
@@ -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))
+2 -2
View File
@@ -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())
}
+1 -1
View File
@@ -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
+5 -5
View File
@@ -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())
}
+2 -2
View File
@@ -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)
}
+2 -2
View File
@@ -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())
}
+1 -1
View File
@@ -52,5 +52,5 @@ func IssueWatch(ctx *context.Context) {
return
}
ctx.Redirect(issue.HTMLURL())
ctx.Redirect(issue.Link())
}
+22 -2
View File
@@ -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 {
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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,
}
}
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -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
}
}
+1 -1
View File
@@ -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
View File
@@ -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)
+1 -1
View File
@@ -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,
+60
View File
@@ -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)
}
}
}
}
+45 -91
View File
@@ -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 {
+10 -1
View File
@@ -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)
}
}
}
+9 -14
View File
@@ -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
+116
View File
@@ -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
}
+3
View File
@@ -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,
+3 -1
View File
@@ -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