mirror of
https://github.com/go-gitea/gitea
synced 2024-11-11 04:34:25 +00:00
Merge branch 'main' into allow-force-push-protected-branches
This commit is contained in:
commit
fdbe77fd8b
54
.github/stale.yml
vendored
54
.github/stale.yml
vendored
@ -1,54 +0,0 @@
|
||||
# Configuration for probot-stale - https://github.com/probot/stale
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request becomes stale
|
||||
daysUntilStale: 60
|
||||
|
||||
# Number of days of inactivity before an Issue or Pull Request with the stale label is closed.
|
||||
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
|
||||
daysUntilClose: 14
|
||||
|
||||
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
|
||||
exemptLabels:
|
||||
- status/blocked
|
||||
- kind/security
|
||||
- lgtm/done
|
||||
- reviewed/confirmed
|
||||
- priority/critical
|
||||
- kind/proposal
|
||||
|
||||
# Set to true to ignore issues in a project (defaults to false)
|
||||
exemptProjects: false
|
||||
|
||||
# Set to true to ignore issues in a milestone (defaults to false)
|
||||
exemptMilestones: false
|
||||
|
||||
# Label to use when marking as stale
|
||||
staleLabel: stale
|
||||
|
||||
# Comment to post when marking as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had recent activity.
|
||||
I am here to help clear issues left open even if solved or waiting for more insight.
|
||||
This issue will be closed if no further activity occurs during the next 2 weeks.
|
||||
If the issue is still valid just add a comment to keep it alive.
|
||||
Thank you for your contributions.
|
||||
|
||||
# Comment to post when closing a stale Issue or Pull Request.
|
||||
closeComment: >
|
||||
This issue has been automatically closed because of inactivity.
|
||||
You can re-open it if needed.
|
||||
|
||||
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||
limitPerRun: 1
|
||||
|
||||
# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls':
|
||||
pulls:
|
||||
daysUntilStale: 60
|
||||
daysUntilClose: 60
|
||||
markComment: >
|
||||
This pull request has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs during the next 2 months. Thank you
|
||||
for your contributions.
|
||||
closeComment: >
|
||||
This pull request has been automatically closed because of inactivity.
|
||||
You can re-open it if needed.
|
9
.github/workflows/release-tag-rc.yml
vendored
9
.github/workflows/release-tag-rc.yml
vendored
@ -44,7 +44,7 @@ jobs:
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
run: |
|
||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
@ -56,6 +56,10 @@ jobs:
|
||||
- name: upload binaries to s3
|
||||
run: |
|
||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
||||
- name: Install GH CLI
|
||||
uses: dev-hanz-ops/install-gh-cli-action@v0.1.0
|
||||
with:
|
||||
gh-cli-version: 2.39.1
|
||||
- name: create github release
|
||||
run: |
|
||||
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/*
|
||||
@ -74,6 +78,8 @@ jobs:
|
||||
id: meta
|
||||
with:
|
||||
images: gitea/gitea
|
||||
flavor: |
|
||||
latest=false
|
||||
# 1.2.3-rc0
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
@ -105,6 +111,7 @@ jobs:
|
||||
images: gitea/gitea
|
||||
# each tag below will have the suffix of -rootless
|
||||
flavor: |
|
||||
latest=false
|
||||
suffix=-rootless
|
||||
# 1.2.3-rc0
|
||||
tags: |
|
||||
|
12
.github/workflows/release-tag-version.yml
vendored
12
.github/workflows/release-tag-version.yml
vendored
@ -46,7 +46,7 @@ jobs:
|
||||
- name: Get cleaned branch name
|
||||
id: clean_name
|
||||
run: |
|
||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\///' -e 's/release\/v//')
|
||||
REF_NAME=$(echo "${{ github.ref }}" | sed -e 's/refs\/heads\///' -e 's/refs\/tags\/v//' -e 's/release\/v//')
|
||||
echo "Cleaned name is ${REF_NAME}"
|
||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
- name: configure aws
|
||||
@ -58,9 +58,13 @@ jobs:
|
||||
- name: upload binaries to s3
|
||||
run: |
|
||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
||||
- name: Install GH CLI
|
||||
uses: dev-hanz-ops/install-gh-cli-action@v0.1.0
|
||||
with:
|
||||
gh-cli-version: 2.39.1
|
||||
- name: create github release
|
||||
run: |
|
||||
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --draft --notes-from-tag dist/release/*
|
||||
gh release create ${{ github.ref_name }} --title ${{ github.ref_name }} --notes-from-tag dist/release/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||
docker-rootful:
|
||||
@ -82,7 +86,6 @@ jobs:
|
||||
# 1.2
|
||||
# 1.2.3
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{version}}
|
||||
@ -114,14 +117,13 @@ jobs:
|
||||
images: gitea/gitea
|
||||
# each tag below will have the suffix of -rootless
|
||||
flavor: |
|
||||
suffix=-rootless
|
||||
suffix=-rootless,onlatest=true
|
||||
# this will generate tags in the following format (with -rootless suffix added):
|
||||
# latest
|
||||
# 1
|
||||
# 1.2
|
||||
# 1.2.3
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{version}}
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/migrations"
|
||||
migrate_base "code.gitea.io/gitea/models/migrations/base"
|
||||
"code.gitea.io/gitea/modules/container"
|
||||
"code.gitea.io/gitea/modules/doctor"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -22,6 +23,19 @@ import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// CmdDoctor represents the available doctor sub-command.
|
||||
var CmdDoctor = &cli.Command{
|
||||
Name: "doctor",
|
||||
Usage: "Diagnose and optionally fix problems",
|
||||
Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
|
||||
|
||||
Subcommands: []*cli.Command{
|
||||
cmdDoctorCheck,
|
||||
cmdRecreateTable,
|
||||
cmdDoctorConvert,
|
||||
},
|
||||
}
|
||||
|
||||
var cmdDoctorCheck = &cli.Command{
|
||||
Name: "check",
|
||||
Usage: "Diagnose and optionally fix problems",
|
||||
@ -60,19 +74,6 @@ var cmdDoctorCheck = &cli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
// CmdDoctor represents the available doctor sub-command.
|
||||
var CmdDoctor = &cli.Command{
|
||||
Name: "doctor",
|
||||
Usage: "Diagnose and optionally fix problems",
|
||||
Description: "A command to diagnose problems with the current Gitea instance according to the given configuration. Some problems can optionally be fixed by modifying the database or data storage.",
|
||||
|
||||
Subcommands: []*cli.Command{
|
||||
cmdDoctorCheck,
|
||||
cmdRecreateTable,
|
||||
cmdDoctorConvert,
|
||||
},
|
||||
}
|
||||
|
||||
var cmdRecreateTable = &cli.Command{
|
||||
Name: "recreate-table",
|
||||
Usage: "Recreate tables from XORM definitions and copy the data.",
|
||||
@ -177,6 +178,7 @@ func runDoctorCheck(ctx *cli.Context) error {
|
||||
if ctx.IsSet("list") {
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
|
||||
_, _ = w.Write([]byte("Default\tName\tTitle\n"))
|
||||
doctor.SortChecks(doctor.Checks)
|
||||
for _, check := range doctor.Checks {
|
||||
if check.IsDefault {
|
||||
_, _ = w.Write([]byte{'*'})
|
||||
@ -192,26 +194,20 @@ func runDoctorCheck(ctx *cli.Context) error {
|
||||
|
||||
var checks []*doctor.Check
|
||||
if ctx.Bool("all") {
|
||||
checks = doctor.Checks
|
||||
checks = make([]*doctor.Check, len(doctor.Checks))
|
||||
copy(checks, doctor.Checks)
|
||||
} else if ctx.IsSet("run") {
|
||||
addDefault := ctx.Bool("default")
|
||||
names := ctx.StringSlice("run")
|
||||
for i, name := range names {
|
||||
names[i] = strings.ToLower(strings.TrimSpace(name))
|
||||
}
|
||||
|
||||
runNamesSet := container.SetOf(ctx.StringSlice("run")...)
|
||||
for _, check := range doctor.Checks {
|
||||
if addDefault && check.IsDefault {
|
||||
if (addDefault && check.IsDefault) || runNamesSet.Contains(check.Name) {
|
||||
checks = append(checks, check)
|
||||
continue
|
||||
}
|
||||
for _, name := range names {
|
||||
if name == check.Name {
|
||||
checks = append(checks, check)
|
||||
break
|
||||
}
|
||||
runNamesSet.Remove(check.Name)
|
||||
}
|
||||
}
|
||||
if len(runNamesSet) > 0 {
|
||||
return fmt.Errorf("unknown checks: %q", strings.Join(runNamesSet.Values(), ","))
|
||||
}
|
||||
} else {
|
||||
for _, check := range doctor.Checks {
|
||||
if check.IsDefault {
|
||||
@ -219,6 +215,5 @@ func runDoctorCheck(ctx *cli.Context) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks)
|
||||
}
|
||||
|
33
cmd/doctor_test.go
Normal file
33
cmd/doctor_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/doctor"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func TestDoctorRun(t *testing.T) {
|
||||
doctor.Register(&doctor.Check{
|
||||
Title: "Test Check",
|
||||
Name: "test-check",
|
||||
Run: func(ctx context.Context, logger log.Logger, autofix bool) error { return nil },
|
||||
|
||||
SkipDatabaseInitialization: true,
|
||||
})
|
||||
app := cli.NewApp()
|
||||
app.Commands = []*cli.Command{cmdDoctorCheck}
|
||||
err := app.Run([]string{"./gitea", "check", "--run", "test-check"})
|
||||
assert.NoError(t, err)
|
||||
err = app.Run([]string{"./gitea", "check", "--run", "no-such"})
|
||||
assert.ErrorContains(t, err, `unknown checks: "no-such"`)
|
||||
err = app.Run([]string{"./gitea", "check", "--run", "test-check,no-such"})
|
||||
assert.ErrorContains(t, err, `unknown checks: "no-such"`)
|
||||
}
|
@ -376,7 +376,9 @@ Gitea or set your environment appropriately.`, "")
|
||||
oldCommitIDs[count] = string(fields[0])
|
||||
newCommitIDs[count] = string(fields[1])
|
||||
refFullNames[count] = git.RefName(fields[2])
|
||||
if refFullNames[count] == git.BranchPrefix+"master" && newCommitIDs[count] != git.EmptySHA && count == total {
|
||||
|
||||
commitID, _ := git.IDFromString(newCommitIDs[count])
|
||||
if refFullNames[count] == git.BranchPrefix+"master" && !commitID.IsZero() && count == total {
|
||||
masterPushed = true
|
||||
}
|
||||
count++
|
||||
@ -669,7 +671,8 @@ Gitea or set your environment appropriately.`, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rs.OldOID != git.EmptySHA {
|
||||
commitID, _ := git.IDFromString(rs.OldOID)
|
||||
if !commitID.IsZero() {
|
||||
err = writeDataPktLine(ctx, os.Stdout, []byte("option old-oid "+rs.OldOID))
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -492,6 +492,11 @@ INTERNAL_TOKEN=
|
||||
;; Cache successful token hashes. API tokens are stored in the DB as pbkdf2 hashes however, this means that there is a potentially significant hashing load when there are multiple API operations.
|
||||
;; This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security.
|
||||
;SUCCESSFUL_TOKENS_CACHE_SIZE = 20
|
||||
;;
|
||||
;; Reject API tokens sent in URL query string (Accept Header-based API tokens only). This avoids security vulnerabilities
|
||||
;; stemming from cached/logged plain-text API tokens.
|
||||
;; In future releases, this will become the default behavior
|
||||
;DISABLE_QUERY_AUTH_TOKEN = false
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
@ -19,10 +19,10 @@ Some jurisdictions (such as EU), requires certain legal pages (e.g. Privacy Poli
|
||||
|
||||
## Getting Pages
|
||||
|
||||
Gitea source code ships with sample pages, available in `contrib/legal` directory. Copy them to `custom/public/`. For example, to add Privacy Policy:
|
||||
Gitea source code ships with sample pages, available in `contrib/legal` directory. Copy them to `custom/public/assets/`. For example, to add Privacy Policy:
|
||||
|
||||
```
|
||||
wget -O /path/to/custom/public/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample
|
||||
wget -O /path/to/custom/public/assets/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample
|
||||
```
|
||||
|
||||
Now you need to edit the page to meet your requirements. In particular you must change the email addresses, web addresses and references to "Your Gitea Instance" to match your situation.
|
||||
|
@ -19,10 +19,10 @@ menu:
|
||||
|
||||
## 获取页面
|
||||
|
||||
Gitea 源代码附带了示例页面,位于 `contrib/legal` 目录中。将它们复制到 `custom/public/` 目录下。例如,如果要添加隐私政策:
|
||||
Gitea 源代码附带了示例页面,位于 `contrib/legal` 目录中。将它们复制到 `custom/public/assets/` 目录下。例如,如果要添加隐私政策:
|
||||
|
||||
```
|
||||
wget -O /path/to/custom/public/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample
|
||||
wget -O /path/to/custom/public/assets/privacy.html https://raw.githubusercontent.com/go-gitea/gitea/main/contrib/legal/privacy.html.sample
|
||||
```
|
||||
|
||||
现在,你需要编辑该页面以满足你的需求。特别是,你必须更改电子邮件地址、网址以及与 "Your Gitea Instance" 相关的引用,以匹配你的情况。
|
||||
|
@ -42,11 +42,11 @@ Gitea 引用 `custom` 目录中的自定义配置文件来覆盖配置、模板
|
||||
|
||||
将自定义的公共文件(比如页面和图片)作为 webroot 放在 `custom/public/` 中来让 Gitea 提供这些自定义内容(符号链接将被追踪)。
|
||||
|
||||
举例说明:`image.png` 存放在 `custom/public/`中,那么它可以通过链接 http://gitea.domain.tld/assets/image.png 访问。
|
||||
举例说明:`image.png` 存放在 `custom/public/assets/`中,那么它可以通过链接 http://gitea.domain.tld/assets/image.png 访问。
|
||||
|
||||
## 修改默认头像
|
||||
|
||||
替换以下目录中的 png 图片: `custom/public/img/avatar\_default.png`
|
||||
替换以下目录中的 png 图片: `custom/public/assets/img/avatar\_default.png`
|
||||
|
||||
## 自定义 Gitea 页面
|
||||
|
||||
|
@ -61,7 +61,7 @@ Please note: authentication is only supported when the SMTP server communication
|
||||
|
||||
- STARTTLS (also known as Opportunistic TLS) via port 587. Initial connection is done over cleartext, but then be upgraded over TLS if the server supports it.
|
||||
- SMTPS connection (SMTP over TLS) via the default port 465. Connection to the server use TLS from the beginning.
|
||||
- Forced SMTPS connection with `IS_TLS_ENABLED=true`. (These are both known as Implicit TLS.)
|
||||
- Forced SMTPS connection with `PROTOCOL=smtps`. (These are both known as Implicit TLS.)
|
||||
This is due to protections imposed by the Go internal libraries against STRIPTLS attacks.
|
||||
|
||||
Note that Implicit TLS is recommended by [RFC8314](https://tools.ietf.org/html/rfc8314#section-3) since 2018.
|
||||
|
@ -55,13 +55,13 @@ PASSWD = `password`
|
||||
|
||||
要发送测试邮件以验证设置,请转到 Gitea > 站点管理 > 配置 > SMTP 邮件配置。
|
||||
|
||||
有关所有选项的完整列表,请查看[配置速查表](doc/administration/config-cheat-sheet.zh-cn.md)。
|
||||
有关所有选项的完整列表,请查看[配置速查表](administration/config-cheat-sheet.md)。
|
||||
|
||||
请注意:只有在使用 TLS 或 `HOST=localhost` 加密 SMTP 服务器通信时才支持身份验证。TLS 加密可以通过以下方式进行:
|
||||
|
||||
- 通过端口 587 的 STARTTLS(也称为 Opportunistic TLS)。初始连接是明文的,但如果服务器支持,则可以升级为 TLS。
|
||||
- 通过默认端口 465 的 SMTPS 连接。连接到服务器从一开始就使用 TLS。
|
||||
- 使用 `IS_TLS_ENABLED=true` 进行强制的 SMTPS 连接。(这两种方式都被称为 Implicit TLS)
|
||||
- 使用 `PROTOCOL=smtps` 进行强制的 SMTPS 连接。(这两种方式都被称为 Implicit TLS)
|
||||
这是由于 Go 内部库对 STRIPTLS 攻击的保护机制。
|
||||
|
||||
请注意,自2018年起,[RFC8314](https://tools.ietf.org/html/rfc8314#section-3) 推荐使用 Implicit TLS。
|
||||
|
@ -194,7 +194,7 @@ ALLOW_DATA_URI_IMAGES = true
|
||||
}
|
||||
```
|
||||
|
||||
将您的样式表添加到自定义目录中,例如 `custom/public/css/my-style-XXXXX.css`,并使用自定义的头文件 `custom/templates/custom/header.tmpl` 进行导入:
|
||||
将您的样式表添加到自定义目录中,例如 `custom/public/assets/css/my-style-XXXXX.css`,并使用自定义的头文件 `custom/templates/custom/header.tmpl` 进行导入:
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" href="{{AppSubUrl}}/assets/css/my-style-XXXXX.css" />
|
||||
|
@ -33,7 +33,7 @@ CERT_FILE = cert.pem
|
||||
KEY_FILE = key.pem
|
||||
```
|
||||
|
||||
请注意,如果您的证书由第三方证书颁发机构签名(即不是自签名的),则 cert.pem 应包含证书链。服务器证书必须是 cert.pem 中的第一个条目,后跟中介(如果有)。不必包含根证书,因为连接客户端必须已经拥有根证书才能建立信任关系。要了解有关配置值的更多信息,请查看 [配置备忘单](../config-cheat-sheet#server-server)。
|
||||
请注意,如果您的证书由第三方证书颁发机构签名(即不是自签名的),则 cert.pem 应包含证书链。服务器证书必须是 cert.pem 中的第一个条目,后跟中介(如果有)。不必包含根证书,因为连接客户端必须已经拥有根证书才能建立信任关系。要了解有关配置值的更多信息,请查看 [配置备忘单](administration/config-cheat-sheet#server-server)。
|
||||
|
||||
对于“CERT_FILE”或“KEY_FILE”字段,当文件路径是相对路径时,文件路径相对于“GITEA_CUSTOM”环境变量。它也可以是绝对路径。
|
||||
|
||||
|
@ -19,10 +19,7 @@ menu:
|
||||
|
||||
## Enabling/configuring API access
|
||||
|
||||
By default, `ENABLE_SWAGGER` is true, and
|
||||
`MAX_RESPONSE_ITEMS` is set to 50. See [Config Cheat
|
||||
Sheet](administration/config-cheat-sheet.md) for more
|
||||
information.
|
||||
By default, `ENABLE_SWAGGER` is true, and `MAX_RESPONSE_ITEMS` is set to 50. See [Config Cheat Sheet](administration/config-cheat-sheet.md) for more information.
|
||||
|
||||
## Authentication
|
||||
|
||||
|
@ -19,8 +19,7 @@ menu:
|
||||
|
||||
## 开启/配置 API 访问
|
||||
|
||||
通常情况下, `ENABLE_SWAGGER` 默认开启并且参数 `MAX_RESPONSE_ITEMS` 默认为 50。您可以从 [Config Cheat
|
||||
Sheet](administration/config-cheat-sheet.md) 中获取更多配置相关信息。
|
||||
通常情况下, `ENABLE_SWAGGER` 默认开启并且参数 `MAX_RESPONSE_ITEMS` 默认为 50。您可以从 [Config Cheat Sheet](administration/config-cheat-sheet.md) 中获取更多配置相关信息。
|
||||
|
||||
## 通过 API 认证
|
||||
|
||||
|
@ -39,7 +39,6 @@ If a bug fix is targeted on 1.20.1 but 1.20.1 is not released yet, you can get t
|
||||
|
||||
To migrate from Gogs to Gitea:
|
||||
|
||||
- [Gogs version 0.9.146 or less](installation/upgrade-from-gogs.md)
|
||||
- [Gogs version 0.11.46.0418](https://github.com/go-gitea/gitea/issues/4286)
|
||||
|
||||
To migrate from GitHub to Gitea, you can use Gitea's built-in migration form.
|
||||
|
@ -41,7 +41,6 @@ menu:
|
||||
|
||||
要从Gogs迁移到Gitea:
|
||||
|
||||
- [Gogs版本0.9.146或更低](installation/upgrade-from-gogs.md)
|
||||
- [Gogs版本0.11.46.0418](https://github.com/go-gitea/gitea/issues/4286)
|
||||
|
||||
要从GitHub迁移到Gitea,您可以使用Gitea内置的迁移表单。
|
||||
@ -190,7 +189,7 @@ Gitea 目前支持三个官方主题,分别是 `gitea-light`、`gitea-dark`
|
||||
|
||||
假设我们的主题是 `arc-blue`(这是一个真实的主题,可以在[此问题](https://github.com/go-gitea/gitea/issues/6011)中找到)
|
||||
|
||||
将`.css`文件命名为`theme-arc-blue.css`并将其添加到`custom/public/css`文件夹中
|
||||
将`.css`文件命名为`theme-arc-blue.css`并将其添加到`custom/public/assets/css`文件夹中
|
||||
|
||||
通过将`arc-blue`添加到`app.ini`中的`THEMES`列表中,允许用户使用该主题
|
||||
|
||||
|
@ -117,7 +117,7 @@ chmod 770 /etc/gitea
|
||||
- 使用 `gitea generate secret` 创建 `SECRET_KEY` 和 `INTERNAL_TOKEN`
|
||||
- 提供所有必要的密钥
|
||||
|
||||
详情参考 [命令行文档](/zh-cn/command-line/) 中有关 `gitea generate secret` 的内容。
|
||||
详情参考 [命令行文档](administration/command-line.md) 中有关 `gitea generate secret` 的内容。
|
||||
|
||||
### 配置 Gitea 工作路径
|
||||
|
||||
@ -209,6 +209,6 @@ remote: ./hooks/pre-receive.d/gitea: line 2: [...]: No such file or directory
|
||||
|
||||
如果您没有使用 Gitea 内置的 SSH 服务器,您还需要通过在管理选项中运行任务 `Update the '.ssh/authorized_keys' file with Gitea SSH keys.` 来重新编写授权密钥文件。
|
||||
|
||||
> 更多经验总结,请参考英文版 [Troubleshooting](/en-us/install-from-binary/#troubleshooting)
|
||||
> 更多经验总结,请参考英文版 [Troubleshooting](https://docs.gitea.com/installation/install-from-binary#troubleshooting)
|
||||
|
||||
如果从本页中没有找到你需要的内容,请访问 [帮助页面](help/support.md)
|
||||
|
@ -64,7 +64,7 @@ git checkout v@version@ # or git checkout pr-xyz
|
||||
|
||||
- `go` @minGoVersion@ 或更高版本,请参阅 [这里](https://golang.org/dl/)
|
||||
- `node` @minNodeVersion@ 或更高版本,并且安装 `npm`, 请参阅 [这里](https://nodejs.org/zh-cn/download/)
|
||||
- `make`, 请参阅 [这里](/zh-cn/hacking-on-gitea/)
|
||||
- `make`, 请参阅 [这里](development/hacking-on-gitea.md)
|
||||
|
||||
为了尽可能简化编译过程,提供了各种 [make任务](https://github.com/go-gitea/gitea/blob/main/Makefile)。
|
||||
|
||||
|
@ -114,7 +114,7 @@ If you cannot see the settings page, please make sure that you have the right pe
|
||||
|
||||
The format of the registration token is a random string `D0gvfu2iHfUjNqCYVljVyRV14fISpJxxxxxxxxxx`.
|
||||
|
||||
A registration token can also be obtained from the gitea [command-line interface](../../administration/command-line.en-us.md#actions-generate-runner-token):
|
||||
A registration token can also be obtained from the gitea [command-line interface](administration/command-line.md#actions-generate-runner-token):
|
||||
|
||||
```
|
||||
gitea --config /etc/gitea/app.ini actions generate-runner-token
|
||||
|
@ -113,7 +113,7 @@ Runner级别决定了从哪里获取注册令牌。
|
||||
|
||||
注册令牌的格式是一个随机字符串 `D0gvfu2iHfUjNqCYVljVyRV14fISpJxxxxxxxxxx`。
|
||||
|
||||
注册令牌也可以通过 Gitea 的 [命令行](../../administration/command-line.en-us.md#actions-generate-runner-token) 获得:
|
||||
注册令牌也可以通过 Gitea 的 [命令行](administration/command-line.md#actions-generate-runner-token) 获得:
|
||||
|
||||
### 注册Runner
|
||||
|
||||
|
@ -145,25 +145,25 @@ Adds the following fields:
|
||||
|
||||
Uses the following fields:
|
||||
|
||||
- Group Search Base (optional)
|
||||
- Group Search Base DN (optional)
|
||||
|
||||
- The LDAP DN used for groups.
|
||||
- Example: `ou=group,dc=mydomain,dc=com`
|
||||
|
||||
- Group Name Filter (optional)
|
||||
- Group Attribute Containing List Of Users (optional)
|
||||
- The attribute of the group object that lists/contains the group members.
|
||||
- Example: `memberUid` or `member`
|
||||
|
||||
- An LDAP filter declaring how to find valid groups in the above DN.
|
||||
- Example: `(|(cn=gitea_users)(cn=admins))`
|
||||
|
||||
- User Attribute in Group (optional)
|
||||
- User Attribute Listed in Group (optional)
|
||||
|
||||
- The user attribute that is used to reference a user in the group object.
|
||||
- Example: `uid` if the group objects contains a `member: bender` and the user object contains a `uid: bender`.
|
||||
- Example: `dn` if the group object contains a `member: uid=bender,ou=users,dc=planetexpress,dc=com`.
|
||||
|
||||
- Group Attribute for User (optional)
|
||||
- The attribute of the group object that lists/contains the group members.
|
||||
- Example: `memberUid` or `member`
|
||||
- Verify group membership in LDAP (optional)
|
||||
|
||||
- An LDAP filter declaring how to find valid groups in the above DN.
|
||||
- Example: `(|(cn=gitea_users)(cn=admins))`
|
||||
|
||||
## PAM (Pluggable Authentication Module)
|
||||
|
||||
@ -198,7 +198,7 @@ administrative user.
|
||||
field is set to `mail.com`, then Gitea will expect the `user email` field
|
||||
for an authenticated GIT instance to be `gituser@mail.com`.[^2]
|
||||
|
||||
**Note**: PAM support is added via [build-time flags](installation/install-from-source.md#build),
|
||||
**Note**: PAM support is added via [build-time flags](installation/from-source.md#build),
|
||||
and the official binaries provided do not have this enabled. PAM requires that
|
||||
the necessary libpam dynamic library be available and the necessary PAM
|
||||
development headers be accessible to the compiler.
|
||||
|
@ -162,7 +162,7 @@ PAM提供了一种机制,通过对用户进行PAM认证来自动将其添加
|
||||
- PAM电子邮件域:用户认证时要附加的电子邮件后缀。例如,如果登录系统期望一个名为gituse的用户,
|
||||
并且将此字段设置为mail.com,那么Gitea在验证一个GIT实例的用户时将期望user emai字段为gituser@mail.com[^2]。
|
||||
|
||||
**Note**: PAM 支持通过[build-time flags](installation/install-from-source.md#build)添加,
|
||||
**Note**: PAM 支持通过[build-time flags](installation/from-source.md#build)添加,
|
||||
而官方提供的二进制文件通常不会默认启用此功能。PAM需要确保系统上有必要的libpam动态库,并且编译器可以访问必要的PAM开发头文件。
|
||||
|
||||
[^1]: 例如,在Debian "Bullseye"上使用标准Linux登录,可以使用`common-session-noninteractive`。这个值对于其他版本的Debian,
|
||||
|
@ -234,7 +234,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
||||
}
|
||||
|
||||
var jobs []*ActionRunJob
|
||||
if err := e.Where("task_id=? AND status=?", 0, StatusWaiting).And(jobCond).Asc("id").Find(&jobs); err != nil {
|
||||
if err := e.Where("task_id=? AND status=?", 0, StatusWaiting).And(jobCond).Asc("updated", "id").Find(&jobs); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
|
@ -92,10 +92,9 @@ func CountUserGPGKeys(ctx context.Context, userID int64) (int64, error) {
|
||||
return db.GetEngine(ctx).Where("owner_id=? AND primary_key_id=''", userID).Count(&GPGKey{})
|
||||
}
|
||||
|
||||
// GetGPGKeyByID returns public key by given ID.
|
||||
func GetGPGKeyByID(ctx context.Context, keyID int64) (*GPGKey, error) {
|
||||
func GetGPGKeyForUserByID(ctx context.Context, ownerID, keyID int64) (*GPGKey, error) {
|
||||
key := new(GPGKey)
|
||||
has, err := db.GetEngine(ctx).ID(keyID).Get(key)
|
||||
has, err := db.GetEngine(ctx).Where("id=? AND owner_id=?", keyID, ownerID).Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
@ -225,7 +224,7 @@ func deleteGPGKey(ctx context.Context, keyID string) (int64, error) {
|
||||
|
||||
// DeleteGPGKey deletes GPG key information in database.
|
||||
func DeleteGPGKey(ctx context.Context, doer *user_model.User, id int64) (err error) {
|
||||
key, err := GetGPGKeyByID(ctx, id)
|
||||
key, err := GetGPGKeyForUserByID(ctx, doer.ID, id)
|
||||
if err != nil {
|
||||
if IsErrGPGKeyNotExist(err) {
|
||||
return nil
|
||||
@ -233,11 +232,6 @@ func DeleteGPGKey(ctx context.Context, doer *user_model.User, id int64) (err err
|
||||
return fmt.Errorf("GetPublicKeyByID: %w", err)
|
||||
}
|
||||
|
||||
// Check if user has access to delete this key.
|
||||
if !doer.IsAdmin && doer.ID != key.OwnerID {
|
||||
return ErrGPGKeyAccessDenied{doer.ID, key.ID}
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -131,24 +131,22 @@ func AddDeployKey(ctx context.Context, repoID int64, name, content string, readO
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
pkey := &PublicKey{
|
||||
Fingerprint: fingerprint,
|
||||
}
|
||||
has, err := db.GetByBean(ctx, pkey)
|
||||
pkey, exist, err := db.Get[PublicKey](ctx, builder.Eq{"fingerprint": fingerprint})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if has {
|
||||
} else if exist {
|
||||
if pkey.Type != KeyTypeDeploy {
|
||||
return nil, ErrKeyAlreadyExist{0, fingerprint, ""}
|
||||
}
|
||||
} else {
|
||||
// First time use this deploy key.
|
||||
pkey.Mode = accessMode
|
||||
pkey.Type = KeyTypeDeploy
|
||||
pkey.Content = content
|
||||
pkey.Name = name
|
||||
pkey = &PublicKey{
|
||||
Fingerprint: fingerprint,
|
||||
Mode: accessMode,
|
||||
Type: KeyTypeDeploy,
|
||||
Content: content,
|
||||
Name: name,
|
||||
}
|
||||
if err = addKey(ctx, pkey); err != nil {
|
||||
return nil, fmt.Errorf("addKey: %w", err)
|
||||
}
|
||||
@ -164,11 +162,10 @@ func AddDeployKey(ctx context.Context, repoID int64, name, content string, readO
|
||||
|
||||
// GetDeployKeyByID returns deploy key by given ID.
|
||||
func GetDeployKeyByID(ctx context.Context, id int64) (*DeployKey, error) {
|
||||
key := new(DeployKey)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(key)
|
||||
key, exist, err := db.GetByID[DeployKey](ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
} else if !exist {
|
||||
return nil, ErrDeployKeyNotExist{id, 0, 0}
|
||||
}
|
||||
return key, nil
|
||||
@ -176,14 +173,10 @@ func GetDeployKeyByID(ctx context.Context, id int64) (*DeployKey, error) {
|
||||
|
||||
// GetDeployKeyByRepo returns deploy key by given public key ID and repository ID.
|
||||
func GetDeployKeyByRepo(ctx context.Context, keyID, repoID int64) (*DeployKey, error) {
|
||||
key := &DeployKey{
|
||||
KeyID: keyID,
|
||||
RepoID: repoID,
|
||||
}
|
||||
has, err := db.GetByBean(ctx, key)
|
||||
key, exist, err := db.Get[DeployKey](ctx, builder.Eq{"key_id": keyID, "repo_id": repoID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
} else if !exist {
|
||||
return nil, ErrDeployKeyNotExist{0, keyID, repoID}
|
||||
}
|
||||
return key, nil
|
||||
|
@ -15,6 +15,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ___________.__ .__ __
|
||||
@ -31,9 +32,7 @@ import (
|
||||
// checkKeyFingerprint only checks if key fingerprint has been used as public key,
|
||||
// it is OK to use same key as deploy key for multiple repositories/users.
|
||||
func checkKeyFingerprint(ctx context.Context, fingerprint string) error {
|
||||
has, err := db.GetByBean(ctx, &PublicKey{
|
||||
Fingerprint: fingerprint,
|
||||
})
|
||||
has, err := db.Exist[PublicKey](ctx, builder.Eq{"fingerprint": fingerprint})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
|
@ -30,10 +30,15 @@ func VerifySSHKey(ctx context.Context, ownerID int64, fingerprint, token, signat
|
||||
return "", ErrKeyNotExist{}
|
||||
}
|
||||
|
||||
if err := sshsig.Verify(bytes.NewBuffer([]byte(token)), []byte(signature), []byte(key.Content), "gitea"); err != nil {
|
||||
log.Error("Unable to validate token signature. Error: %v", err)
|
||||
return "", ErrSSHInvalidTokenSignature{
|
||||
Fingerprint: key.Fingerprint,
|
||||
err = sshsig.Verify(bytes.NewBuffer([]byte(token)), []byte(signature), []byte(key.Content), "gitea")
|
||||
if err != nil {
|
||||
// edge case for Windows based shells that will add CR LF if piped to ssh-keygen command
|
||||
// see https://github.com/PowerShell/PowerShell/issues/5974
|
||||
if sshsig.Verify(bytes.NewBuffer([]byte(token+"\r\n")), []byte(signature), []byte(key.Content), "gitea") != nil {
|
||||
log.Error("Unable to validate token signature. Error: %v", err)
|
||||
return "", ErrSSHInvalidTokenSignature{
|
||||
Fingerprint: key.Fingerprint,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,8 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Session represents a session compatible for go-chi session
|
||||
@ -33,34 +35,28 @@ func UpdateSession(ctx context.Context, key string, data []byte) error {
|
||||
|
||||
// ReadSession reads the data for the provided session
|
||||
func ReadSession(ctx context.Context, key string) (*Session, error) {
|
||||
session := Session{
|
||||
Key: key,
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if has, err := db.GetByBean(ctx, &session); err != nil {
|
||||
session, exist, err := db.Get[Session](ctx, builder.Eq{"key": key})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
} else if !exist {
|
||||
session.Expiry = timeutil.TimeStampNow()
|
||||
if err := db.Insert(ctx, &session); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &session, committer.Commit()
|
||||
return session, committer.Commit()
|
||||
}
|
||||
|
||||
// ExistSession checks if a session exists
|
||||
func ExistSession(ctx context.Context, key string) (bool, error) {
|
||||
session := Session{
|
||||
Key: key,
|
||||
}
|
||||
return db.GetEngine(ctx).Get(&session)
|
||||
return db.Exist[Session](ctx, builder.Eq{"key": key})
|
||||
}
|
||||
|
||||
// DestroySession destroys a session
|
||||
@ -79,17 +75,13 @@ func RegenerateSession(ctx context.Context, oldKey, newKey string) (*Session, er
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if has, err := db.GetByBean(ctx, &Session{
|
||||
Key: newKey,
|
||||
}); err != nil {
|
||||
if has, err := db.Exist[Session](ctx, builder.Eq{"key": newKey}); err != nil {
|
||||
return nil, err
|
||||
} else if has {
|
||||
return nil, fmt.Errorf("session Key: %s already exists", newKey)
|
||||
}
|
||||
|
||||
if has, err := db.GetByBean(ctx, &Session{
|
||||
Key: oldKey,
|
||||
}); err != nil {
|
||||
if has, err := db.Exist[Session](ctx, builder.Eq{"key": oldKey}); err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
if err := db.Insert(ctx, &Session{
|
||||
@ -104,14 +96,13 @@ func RegenerateSession(ctx context.Context, oldKey, newKey string) (*Session, er
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := Session{
|
||||
Key: newKey,
|
||||
}
|
||||
if _, err := db.GetByBean(ctx, &s); err != nil {
|
||||
s, _, err := db.Get[Session](ctx, builder.Eq{"key": newKey})
|
||||
if err != nil {
|
||||
// is not exist, it should be impossible
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &s, committer.Commit()
|
||||
return s, committer.Commit()
|
||||
}
|
||||
|
||||
// CountSessions returns the number of sessions
|
||||
|
@ -265,10 +265,10 @@ func IsSSPIEnabled(ctx context.Context) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
exist, err := db.Exists[Source](ctx, FindSourcesOptions{
|
||||
exist, err := db.Exist[Source](ctx, FindSourcesOptions{
|
||||
IsActive: util.OptionalBoolTrue,
|
||||
LoginType: SSPI,
|
||||
})
|
||||
}.ToConds())
|
||||
if err != nil {
|
||||
log.Error("Active SSPI Sources: %v", err)
|
||||
return false
|
||||
|
@ -173,9 +173,44 @@ func Exec(ctx context.Context, sqlAndArgs ...any) (sql.Result, error) {
|
||||
return GetEngine(ctx).Exec(sqlAndArgs...)
|
||||
}
|
||||
|
||||
// GetByBean filled empty fields of the bean according non-empty fields to query in database.
|
||||
func GetByBean(ctx context.Context, bean any) (bool, error) {
|
||||
return GetEngine(ctx).Get(bean)
|
||||
func Get[T any](ctx context.Context, cond builder.Cond) (object *T, exist bool, err error) {
|
||||
if !cond.IsValid() {
|
||||
return nil, false, ErrConditionRequired{}
|
||||
}
|
||||
|
||||
var bean T
|
||||
has, err := GetEngine(ctx).Where(cond).NoAutoCondition().Get(&bean)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
} else if !has {
|
||||
return nil, false, nil
|
||||
}
|
||||
return &bean, true, nil
|
||||
}
|
||||
|
||||
func GetByID[T any](ctx context.Context, id int64) (object *T, exist bool, err error) {
|
||||
var bean T
|
||||
has, err := GetEngine(ctx).ID(id).NoAutoCondition().Get(&bean)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
} else if !has {
|
||||
return nil, false, nil
|
||||
}
|
||||
return &bean, true, nil
|
||||
}
|
||||
|
||||
func Exist[T any](ctx context.Context, cond builder.Cond) (bool, error) {
|
||||
if !cond.IsValid() {
|
||||
return false, ErrConditionRequired{}
|
||||
}
|
||||
|
||||
var bean T
|
||||
return GetEngine(ctx).Where(cond).NoAutoCondition().Exist(&bean)
|
||||
}
|
||||
|
||||
func ExistByID[T any](ctx context.Context, id int64) (bool, error) {
|
||||
var bean T
|
||||
return GetEngine(ctx).ID(id).NoAutoCondition().Exist(&bean)
|
||||
}
|
||||
|
||||
// DeleteByBean deletes all records according non-empty fields of the bean as conditions.
|
||||
@ -264,8 +299,3 @@ func inTransaction(ctx context.Context) (*xorm.Session, bool) {
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func Exists[T any](ctx context.Context, opts FindOptions) (bool, error) {
|
||||
var bean T
|
||||
return GetEngine(ctx).Where(opts.ToConds()).Exist(&bean)
|
||||
}
|
||||
|
@ -72,3 +72,21 @@ func (err ErrNotExist) Error() string {
|
||||
func (err ErrNotExist) Unwrap() error {
|
||||
return util.ErrNotExist
|
||||
}
|
||||
|
||||
// ErrConditionRequired represents an error which require condition.
|
||||
type ErrConditionRequired struct{}
|
||||
|
||||
// IsErrConditionRequired checks if an error is an ErrConditionRequired
|
||||
func IsErrConditionRequired(err error) bool {
|
||||
_, ok := err.(ErrConditionRequired)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (err ErrConditionRequired) Error() string {
|
||||
return "condition is required"
|
||||
}
|
||||
|
||||
// Unwrap unwraps this as a ErrNotExist err
|
||||
func (err ErrConditionRequired) Unwrap() error {
|
||||
return util.ErrInvalidArgument
|
||||
}
|
||||
|
@ -31,11 +31,11 @@ func TestIterate(t *testing.T) {
|
||||
assert.EqualValues(t, cnt, repoUnitCnt)
|
||||
|
||||
err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repoUnit *repo_model.RepoUnit) error {
|
||||
reopUnit2 := repo_model.RepoUnit{ID: repoUnit.ID}
|
||||
has, err := db.GetByBean(ctx, &reopUnit2)
|
||||
has, err := db.ExistByID[repo_model.RepoUnit](ctx, repoUnit.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
}
|
||||
if !has {
|
||||
return db.ErrNotExist{Resource: "repo_unit", ID: repoUnit.ID}
|
||||
}
|
||||
assert.EqualValues(t, repoUnit.RepoID, repoUnit.RepoID)
|
||||
|
@ -66,3 +66,12 @@
|
||||
tree_path: "README.md"
|
||||
created_unix: 946684812
|
||||
invalidated: true
|
||||
|
||||
-
|
||||
id: 8
|
||||
type: 0 # comment
|
||||
poster_id: 2
|
||||
issue_id: 4 # in repo_id 2
|
||||
content: "comment in private pository"
|
||||
created_unix: 946684811
|
||||
updated_unix: 946684811
|
||||
|
@ -61,7 +61,7 @@
|
||||
priority: 0
|
||||
is_closed: true
|
||||
is_pull: false
|
||||
num_comments: 0
|
||||
num_comments: 1
|
||||
created_unix: 946684830
|
||||
updated_unix: 978307200
|
||||
is_locked: false
|
||||
|
@ -205,10 +205,9 @@ func DeleteBranches(ctx context.Context, repoID, doerID int64, branchIDs []int64
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateBranch updates the branch information in the database. If the branch exist, it will update latest commit of this branch information
|
||||
// If it doest not exist, insert a new record into database
|
||||
func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) error {
|
||||
cnt, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, branchName).
|
||||
// UpdateBranch updates the branch information in the database.
|
||||
func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) (int64, error) {
|
||||
return db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, branchName).
|
||||
Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted, updated_unix").
|
||||
Update(&Branch{
|
||||
CommitID: commit.ID.String(),
|
||||
@ -217,21 +216,6 @@ func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string
|
||||
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
|
||||
IsDeleted: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cnt > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return db.Insert(ctx, &Branch{
|
||||
RepoID: repoID,
|
||||
Name: branchName,
|
||||
CommitID: commit.ID.String(),
|
||||
CommitMessage: commit.Summary(),
|
||||
PusherID: pusherID,
|
||||
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
|
||||
})
|
||||
}
|
||||
|
||||
// AddDeletedBranch adds a deleted branch to the database
|
||||
@ -308,6 +292,17 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, from, to str
|
||||
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
var branch Branch
|
||||
exist, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repo.ID, from).Get(&branch)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !exist || branch.IsDeleted {
|
||||
return ErrBranchNotExist{
|
||||
RepoID: repo.ID,
|
||||
BranchName: from,
|
||||
}
|
||||
}
|
||||
|
||||
// 1. update branch in database
|
||||
if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{
|
||||
Name: to,
|
||||
|
@ -12,7 +12,6 @@ import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type BranchList []*Branch
|
||||
@ -73,7 +72,7 @@ type FindBranchOptions struct {
|
||||
Keyword string
|
||||
}
|
||||
|
||||
func (opts *FindBranchOptions) Cond() builder.Cond {
|
||||
func (opts FindBranchOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID > 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
@ -91,41 +90,30 @@ func (opts *FindBranchOptions) Cond() builder.Cond {
|
||||
return cond
|
||||
}
|
||||
|
||||
func CountBranches(ctx context.Context, opts FindBranchOptions) (int64, error) {
|
||||
return db.GetEngine(ctx).Where(opts.Cond()).Count(&Branch{})
|
||||
}
|
||||
|
||||
func orderByBranches(sess *xorm.Session, opts FindBranchOptions) *xorm.Session {
|
||||
func (opts FindBranchOptions) ToOrders() string {
|
||||
orderBy := opts.OrderBy
|
||||
if !opts.IsDeletedBranch.IsFalse() { // if deleted branch included, put them at the end
|
||||
sess = sess.OrderBy("is_deleted ASC")
|
||||
if orderBy != "" {
|
||||
orderBy += ", "
|
||||
}
|
||||
orderBy += "is_deleted ASC"
|
||||
}
|
||||
|
||||
if opts.OrderBy == "" {
|
||||
if orderBy == "" {
|
||||
// the commit_time might be the same, so add the "name" to make sure the order is stable
|
||||
opts.OrderBy = "commit_time DESC, name ASC"
|
||||
return "commit_time DESC, name ASC"
|
||||
}
|
||||
return sess.OrderBy(opts.OrderBy)
|
||||
}
|
||||
|
||||
func FindBranches(ctx context.Context, opts FindBranchOptions) (BranchList, error) {
|
||||
sess := db.GetEngine(ctx).Where(opts.Cond())
|
||||
if opts.PageSize > 0 && !opts.IsListAll() {
|
||||
sess = db.SetSessionPagination(sess, &opts.ListOptions)
|
||||
}
|
||||
sess = orderByBranches(sess, opts)
|
||||
|
||||
var branches []*Branch
|
||||
return branches, sess.Find(&branches)
|
||||
return orderBy
|
||||
}
|
||||
|
||||
func FindBranchNames(ctx context.Context, opts FindBranchOptions) ([]string, error) {
|
||||
sess := db.GetEngine(ctx).Select("name").Where(opts.Cond())
|
||||
sess := db.GetEngine(ctx).Select("name").Where(opts.ToConds())
|
||||
if opts.PageSize > 0 && !opts.IsListAll() {
|
||||
sess = db.SetSessionPagination(sess, &opts.ListOptions)
|
||||
}
|
||||
sess = orderByBranches(sess, opts)
|
||||
|
||||
var branches []string
|
||||
if err := sess.Table("branch").Find(&branches); err != nil {
|
||||
if err := sess.Table("branch").OrderBy(opts.ToOrders()).Find(&branches); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return branches, nil
|
||||
|
@ -30,14 +30,14 @@ func TestAddDeletedBranch(t *testing.T) {
|
||||
assert.True(t, secondBranch.IsDeleted)
|
||||
|
||||
commit := &git.Commit{
|
||||
ID: git.MustIDFromString(secondBranch.CommitID),
|
||||
ID: repo.ObjectFormat.MustIDFromString(secondBranch.CommitID),
|
||||
CommitMessage: secondBranch.CommitMessage,
|
||||
Committer: &git.Signature{
|
||||
When: secondBranch.CommitTime.AsLocalTime(),
|
||||
},
|
||||
}
|
||||
|
||||
err := git_model.UpdateBranch(db.DefaultContext, repo.ID, secondBranch.PusherID, secondBranch.Name, commit)
|
||||
_, err := git_model.UpdateBranch(db.DefaultContext, repo.ID, secondBranch.PusherID, secondBranch.Name, commit)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
@ -45,10 +45,8 @@ func TestGetDeletedBranches(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
branches, err := git_model.FindBranches(db.DefaultContext, git_model.FindBranchOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
ListAll: true,
|
||||
},
|
||||
branches, err := db.Find[git_model.Branch](db.DefaultContext, git_model.FindBranchOptions{
|
||||
ListOptions: db.ListOptionsAll,
|
||||
RepoID: repo.ID,
|
||||
IsDeletedBranch: util.OptionalBoolTrue,
|
||||
})
|
||||
|
@ -114,7 +114,8 @@ WHEN NOT MATCHED
|
||||
|
||||
// GetNextCommitStatusIndex retried 3 times to generate a resource index
|
||||
func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) {
|
||||
if !git.IsValidSHAPattern(sha) {
|
||||
_, err := git.IDFromString(sha)
|
||||
if err != nil {
|
||||
return 0, git.ErrInvalidSHA{SHA: sha}
|
||||
}
|
||||
|
||||
@ -323,7 +324,9 @@ func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHA
|
||||
Select("max( id ) as id, repo_id").
|
||||
GroupBy("context_hash, repo_id").OrderBy("max( id ) desc")
|
||||
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
if !listOptions.IsListAll() {
|
||||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
}
|
||||
|
||||
err := sess.Find(&results)
|
||||
if err != nil {
|
||||
@ -423,7 +426,7 @@ func FindRepoRecentCommitStatusContexts(ctx context.Context, repoID int64, befor
|
||||
type NewCommitStatusOptions struct {
|
||||
Repo *repo_model.Repository
|
||||
Creator *user_model.User
|
||||
SHA string
|
||||
SHA git.ObjectID
|
||||
CommitStatus *CommitStatus
|
||||
}
|
||||
|
||||
@ -438,10 +441,6 @@ func NewCommitStatus(ctx context.Context, opts NewCommitStatusOptions) error {
|
||||
return fmt.Errorf("NewCommitStatus[%s, %s]: no user specified", repoPath, opts.SHA)
|
||||
}
|
||||
|
||||
if _, err := git.NewIDFromString(opts.SHA); err != nil {
|
||||
return fmt.Errorf("NewCommitStatus[%s, %s]: invalid sha: %w", repoPath, opts.SHA, err)
|
||||
}
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewCommitStatus[repo_id: %d, user_id: %d, sha: %s]: %w", opts.Repo.ID, opts.Creator.ID, opts.SHA, err)
|
||||
@ -449,7 +448,7 @@ func NewCommitStatus(ctx context.Context, opts NewCommitStatusOptions) error {
|
||||
defer committer.Close()
|
||||
|
||||
// Get the next Status Index
|
||||
idx, err := GetNextCommitStatusIndex(ctx, opts.Repo.ID, opts.SHA)
|
||||
idx, err := GetNextCommitStatusIndex(ctx, opts.Repo.ID, opts.SHA.String())
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate commit status index failed: %w", err)
|
||||
}
|
||||
@ -457,7 +456,7 @@ func NewCommitStatus(ctx context.Context, opts NewCommitStatusOptions) error {
|
||||
opts.CommitStatus.Description = strings.TrimSpace(opts.CommitStatus.Description)
|
||||
opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context)
|
||||
opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL)
|
||||
opts.CommitStatus.SHA = opts.SHA
|
||||
opts.CommitStatus.SHA = opts.SHA.String()
|
||||
opts.CommitStatus.CreatorID = opts.Creator.ID
|
||||
opts.CommitStatus.RepoID = opts.Repo.ID
|
||||
opts.CommitStatus.Index = idx
|
||||
|
@ -135,7 +135,7 @@ var ErrLFSObjectNotExist = db.ErrNotExist{Resource: "LFS Meta object"}
|
||||
|
||||
// NewLFSMetaObject stores a given populated LFSMetaObject structure in the database
|
||||
// if it is not already present.
|
||||
func NewLFSMetaObject(ctx context.Context, m *LFSMetaObject) (*LFSMetaObject, error) {
|
||||
func NewLFSMetaObject(ctx context.Context, repoID int64, p lfs.Pointer) (*LFSMetaObject, error) {
|
||||
var err error
|
||||
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
@ -144,16 +144,15 @@ func NewLFSMetaObject(ctx context.Context, m *LFSMetaObject) (*LFSMetaObject, er
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
has, err := db.GetByBean(ctx, m)
|
||||
m, exist, err := db.Get[LFSMetaObject](ctx, builder.Eq{"repository_id": repoID, "oid": p.Oid})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if has {
|
||||
} else if exist {
|
||||
m.Existing = true
|
||||
return m, committer.Commit()
|
||||
}
|
||||
|
||||
m = &LFSMetaObject{Pointer: p, RepositoryID: repoID}
|
||||
if err = db.Insert(ctx, m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
"github.com/gobwas/glob/syntax"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
var ErrBranchIsProtected = errors.New("branch is protected")
|
||||
@ -306,12 +307,11 @@ func (protectBranch *ProtectedBranch) IsUnprotectedFile(patterns []glob.Glob, pa
|
||||
|
||||
// GetProtectedBranchRuleByName getting protected branch rule by name
|
||||
func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName string) (*ProtectedBranch, error) {
|
||||
rel := &ProtectedBranch{RepoID: repoID, RuleName: ruleName}
|
||||
has, err := db.GetByBean(ctx, rel)
|
||||
// branch_name is legacy name, it actually is rule name
|
||||
rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "branch_name": ruleName})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
} else if !exist {
|
||||
return nil, nil
|
||||
}
|
||||
return rel, nil
|
||||
@ -319,12 +319,10 @@ func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName st
|
||||
|
||||
// GetProtectedBranchRuleByID getting protected branch rule by rule ID
|
||||
func GetProtectedBranchRuleByID(ctx context.Context, repoID, ruleID int64) (*ProtectedBranch, error) {
|
||||
rel := &ProtectedBranch{ID: ruleID, RepoID: repoID}
|
||||
has, err := db.GetByBean(ctx, rel)
|
||||
rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "id": ruleID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !has {
|
||||
} else if !exist {
|
||||
return nil, nil
|
||||
}
|
||||
return rel, nil
|
||||
|
@ -10,6 +10,8 @@ import (
|
||||
"code.gitea.io/gitea/models/db"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// IssueAssignees saves all issue assignees
|
||||
@ -59,7 +61,7 @@ func GetAssigneeIDsByIssue(ctx context.Context, issueID int64) ([]int64, error)
|
||||
|
||||
// IsUserAssignedToIssue returns true when the user is assigned to the issue
|
||||
func IsUserAssignedToIssue(ctx context.Context, issue *Issue, user *user_model.User) (isAssigned bool, err error) {
|
||||
return db.GetByBean(ctx, &IssueAssignees{IssueID: issue.ID, AssigneeID: user.ID})
|
||||
return db.Exist[IssueAssignees](ctx, builder.Eq{"assignee_id": user.ID, "issue_id": issue.ID})
|
||||
}
|
||||
|
||||
// ToggleIssueAssignee changes a user between assigned and not assigned for this issue, and make issue comment for it.
|
||||
|
@ -1024,6 +1024,7 @@ type FindCommentsOptions struct {
|
||||
Type CommentType
|
||||
IssueIDs []int64
|
||||
Invalidated util.OptionalBool
|
||||
IsPull util.OptionalBool
|
||||
}
|
||||
|
||||
// ToConds implements FindOptions interface
|
||||
@ -1058,6 +1059,9 @@ func (opts FindCommentsOptions) ToConds() builder.Cond {
|
||||
if !opts.Invalidated.IsNone() {
|
||||
cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()})
|
||||
}
|
||||
if opts.IsPull != util.OptionalBoolNone {
|
||||
cond = cond.And(builder.Eq{"issue.is_pull": opts.IsPull.IsTrue()})
|
||||
}
|
||||
return cond
|
||||
}
|
||||
|
||||
@ -1065,7 +1069,7 @@ func (opts FindCommentsOptions) ToConds() builder.Cond {
|
||||
func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) {
|
||||
comments := make([]*Comment, 0, 10)
|
||||
sess := db.GetEngine(ctx).Where(opts.ToConds())
|
||||
if opts.RepoID > 0 {
|
||||
if opts.RepoID > 0 || opts.IsPull != util.OptionalBoolNone {
|
||||
sess.Join("INNER", "issue", "issue.id = comment.issue_id")
|
||||
}
|
||||
|
||||
|
@ -218,9 +218,9 @@ func GetIssueContentHistoryByID(dbCtx context.Context, id int64) (*ContentHistor
|
||||
}
|
||||
|
||||
// GetIssueContentHistoryAndPrev get a history and the previous non-deleted history (to compare)
|
||||
func GetIssueContentHistoryAndPrev(dbCtx context.Context, id int64) (history, prevHistory *ContentHistory, err error) {
|
||||
func GetIssueContentHistoryAndPrev(dbCtx context.Context, issueID, id int64) (history, prevHistory *ContentHistory, err error) {
|
||||
history = &ContentHistory{}
|
||||
has, err := db.GetEngine(dbCtx).ID(id).Get(history)
|
||||
has, err := db.GetEngine(dbCtx).Where("id=? AND issue_id=?", id, issueID).Get(history)
|
||||
if err != nil {
|
||||
log.Error("failed to get issue content history %v. err=%v", id, err)
|
||||
return nil, nil, err
|
||||
|
@ -58,13 +58,13 @@ func TestContentHistory(t *testing.T) {
|
||||
hasHistory2, _ := issues_model.HasIssueContentHistory(dbCtx, 10, 1)
|
||||
assert.False(t, hasHistory2)
|
||||
|
||||
h6, h6Prev, _ := issues_model.GetIssueContentHistoryAndPrev(dbCtx, 6)
|
||||
h6, h6Prev, _ := issues_model.GetIssueContentHistoryAndPrev(dbCtx, 10, 6)
|
||||
assert.EqualValues(t, 6, h6.ID)
|
||||
assert.EqualValues(t, 5, h6Prev.ID)
|
||||
|
||||
// soft-delete
|
||||
_ = issues_model.SoftDeleteIssueContentHistory(dbCtx, 5)
|
||||
h6, h6Prev, _ = issues_model.GetIssueContentHistoryAndPrev(dbCtx, 6)
|
||||
h6, h6Prev, _ = issues_model.GetIssueContentHistoryAndPrev(dbCtx, 10, 6)
|
||||
assert.EqualValues(t, 6, h6.ID)
|
||||
assert.EqualValues(t, 4, h6Prev.ID)
|
||||
|
||||
|
@ -23,6 +23,7 @@ import (
|
||||
type IssuesOptions struct { //nolint
|
||||
db.Paginator
|
||||
RepoIDs []int64 // overwrites RepoCond if the length is not 0
|
||||
AllPublic bool // include also all public repositories
|
||||
RepoCond builder.Cond
|
||||
AssigneeID int64
|
||||
PosterID int64
|
||||
@ -197,6 +198,12 @@ func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session
|
||||
} else if len(opts.RepoIDs) > 1 {
|
||||
opts.RepoCond = builder.In("issue.repo_id", opts.RepoIDs)
|
||||
}
|
||||
if opts.AllPublic {
|
||||
if opts.RepoCond == nil {
|
||||
opts.RepoCond = builder.NewCond()
|
||||
}
|
||||
opts.RepoCond = opts.RepoCond.Or(builder.In("issue.repo_id", builder.Select("id").From("repository").Where(builder.Eq{"is_private": false})))
|
||||
}
|
||||
if opts.RepoCond != nil {
|
||||
sess.And(opts.RepoCond)
|
||||
}
|
||||
|
@ -14,8 +14,8 @@ import (
|
||||
// IssueUser represents an issue-user relation.
|
||||
type IssueUser struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UID int64 `xorm:"INDEX"` // User ID.
|
||||
IssueID int64 `xorm:"INDEX"`
|
||||
UID int64 `xorm:"INDEX unique(uid_to_issue)"` // User ID.
|
||||
IssueID int64 `xorm:"INDEX unique(uid_to_issue)"`
|
||||
IsRead bool
|
||||
IsMentioned bool
|
||||
}
|
||||
|
@ -304,15 +304,11 @@ func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) (
|
||||
return nil, ErrRepoLabelNotExist{0, repoID}
|
||||
}
|
||||
|
||||
l := &Label{
|
||||
Name: labelName,
|
||||
RepoID: repoID,
|
||||
}
|
||||
has, err := db.GetByBean(ctx, l)
|
||||
l, exist, err := db.Get[Label](ctx, builder.Eq{"name": labelName, "repo_id": repoID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrRepoLabelNotExist{0, l.RepoID}
|
||||
} else if !exist {
|
||||
return nil, ErrRepoLabelNotExist{0, repoID}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
@ -323,15 +319,11 @@ func GetLabelInRepoByID(ctx context.Context, repoID, labelID int64) (*Label, err
|
||||
return nil, ErrRepoLabelNotExist{labelID, repoID}
|
||||
}
|
||||
|
||||
l := &Label{
|
||||
ID: labelID,
|
||||
RepoID: repoID,
|
||||
}
|
||||
has, err := db.GetByBean(ctx, l)
|
||||
l, exist, err := db.Get[Label](ctx, builder.Eq{"id": labelID, "repo_id": repoID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrRepoLabelNotExist{l.ID, l.RepoID}
|
||||
} else if !exist {
|
||||
return nil, ErrRepoLabelNotExist{labelID, repoID}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
@ -408,15 +400,11 @@ func GetLabelInOrgByName(ctx context.Context, orgID int64, labelName string) (*L
|
||||
return nil, ErrOrgLabelNotExist{0, orgID}
|
||||
}
|
||||
|
||||
l := &Label{
|
||||
Name: labelName,
|
||||
OrgID: orgID,
|
||||
}
|
||||
has, err := db.GetByBean(ctx, l)
|
||||
l, exist, err := db.Get[Label](ctx, builder.Eq{"name": labelName, "org_id": orgID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrOrgLabelNotExist{0, l.OrgID}
|
||||
} else if !exist {
|
||||
return nil, ErrOrgLabelNotExist{0, orgID}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
@ -427,15 +415,11 @@ func GetLabelInOrgByID(ctx context.Context, orgID, labelID int64) (*Label, error
|
||||
return nil, ErrOrgLabelNotExist{labelID, orgID}
|
||||
}
|
||||
|
||||
l := &Label{
|
||||
ID: labelID,
|
||||
OrgID: orgID,
|
||||
}
|
||||
has, err := db.GetByBean(ctx, l)
|
||||
l, exist, err := db.Get[Label](ctx, builder.Eq{"id": labelID, "org_id": orgID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrOrgLabelNotExist{l.ID, l.OrgID}
|
||||
} else if !exist {
|
||||
return nil, ErrOrgLabelNotExist{labelID, orgID}
|
||||
}
|
||||
return l, nil
|
||||
}
|
||||
|
@ -295,16 +295,15 @@ func DeleteMilestoneByRepoID(ctx context.Context, repoID, id int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
numMilestones, err := CountMilestones(ctx, GetMilestonesOption{
|
||||
numMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
|
||||
RepoID: repo.ID,
|
||||
State: api.StateAll,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
numClosedMilestones, err := CountMilestones(ctx, GetMilestonesOption{
|
||||
RepoID: repo.ID,
|
||||
State: api.StateClosed,
|
||||
numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
|
||||
RepoID: repo.ID,
|
||||
IsClosed: util.OptionalBoolTrue,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -8,8 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
@ -25,31 +24,31 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 {
|
||||
return ids
|
||||
}
|
||||
|
||||
// GetMilestonesOption contain options to get milestones
|
||||
type GetMilestonesOption struct {
|
||||
// FindMilestoneOptions contain options to get milestones
|
||||
type FindMilestoneOptions struct {
|
||||
db.ListOptions
|
||||
RepoID int64
|
||||
State api.StateType
|
||||
IsClosed util.OptionalBool
|
||||
Name string
|
||||
SortType string
|
||||
RepoCond builder.Cond
|
||||
RepoIDs []int64
|
||||
}
|
||||
|
||||
func (opts GetMilestonesOption) toCond() builder.Cond {
|
||||
func (opts FindMilestoneOptions) ToConds() builder.Cond {
|
||||
cond := builder.NewCond()
|
||||
if opts.RepoID != 0 {
|
||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||
}
|
||||
|
||||
switch opts.State {
|
||||
case api.StateClosed:
|
||||
cond = cond.And(builder.Eq{"is_closed": true})
|
||||
case api.StateAll:
|
||||
break
|
||||
// api.StateOpen:
|
||||
default:
|
||||
cond = cond.And(builder.Eq{"is_closed": false})
|
||||
if opts.IsClosed != util.OptionalBoolNone {
|
||||
cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.IsTrue()})
|
||||
}
|
||||
if opts.RepoCond != nil && opts.RepoCond.IsValid() {
|
||||
cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond)))
|
||||
}
|
||||
if len(opts.RepoIDs) > 0 {
|
||||
cond = cond.And(builder.In("repo_id", opts.RepoIDs))
|
||||
}
|
||||
|
||||
if len(opts.Name) != 0 {
|
||||
cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name))
|
||||
}
|
||||
@ -57,34 +56,23 @@ func (opts GetMilestonesOption) toCond() builder.Cond {
|
||||
return cond
|
||||
}
|
||||
|
||||
// GetMilestones returns milestones filtered by GetMilestonesOption's
|
||||
func GetMilestones(ctx context.Context, opts GetMilestonesOption) (MilestoneList, int64, error) {
|
||||
sess := db.GetEngine(ctx).Where(opts.toCond())
|
||||
|
||||
if opts.Page != 0 {
|
||||
sess = db.SetSessionPagination(sess, &opts)
|
||||
}
|
||||
|
||||
func (opts FindMilestoneOptions) ToOrders() string {
|
||||
switch opts.SortType {
|
||||
case "furthestduedate":
|
||||
sess.Desc("deadline_unix")
|
||||
return "deadline_unix DESC"
|
||||
case "leastcomplete":
|
||||
sess.Asc("completeness")
|
||||
return "completeness ASC"
|
||||
case "mostcomplete":
|
||||
sess.Desc("completeness")
|
||||
return "completeness DESC"
|
||||
case "leastissues":
|
||||
sess.Asc("num_issues")
|
||||
return "num_issues ASC"
|
||||
case "mostissues":
|
||||
sess.Desc("num_issues")
|
||||
return "num_issues DESC"
|
||||
case "id":
|
||||
sess.Asc("id")
|
||||
return "id ASC"
|
||||
default:
|
||||
sess.Asc("deadline_unix").Asc("id")
|
||||
return "deadline_unix ASC, id ASC"
|
||||
}
|
||||
|
||||
miles := make([]*Milestone, 0, opts.PageSize)
|
||||
total, err := sess.FindAndCount(&miles)
|
||||
return miles, total, err
|
||||
}
|
||||
|
||||
// GetMilestoneIDsByNames returns a list of milestone ids by given names.
|
||||
@ -99,49 +87,6 @@ func GetMilestoneIDsByNames(ctx context.Context, names []string) ([]int64, error
|
||||
Find(&ids)
|
||||
}
|
||||
|
||||
// SearchMilestones search milestones
|
||||
func SearchMilestones(ctx context.Context, repoCond builder.Cond, page int, isClosed bool, sortType, keyword string) (MilestoneList, error) {
|
||||
miles := make([]*Milestone, 0, setting.UI.IssuePagingNum)
|
||||
sess := db.GetEngine(ctx).Where("is_closed = ?", isClosed)
|
||||
if len(keyword) > 0 {
|
||||
sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
|
||||
}
|
||||
if repoCond.IsValid() {
|
||||
sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
|
||||
}
|
||||
if page > 0 {
|
||||
sess = sess.Limit(setting.UI.IssuePagingNum, (page-1)*setting.UI.IssuePagingNum)
|
||||
}
|
||||
|
||||
switch sortType {
|
||||
case "furthestduedate":
|
||||
sess.Desc("deadline_unix")
|
||||
case "leastcomplete":
|
||||
sess.Asc("completeness")
|
||||
case "mostcomplete":
|
||||
sess.Desc("completeness")
|
||||
case "leastissues":
|
||||
sess.Asc("num_issues")
|
||||
case "mostissues":
|
||||
sess.Desc("num_issues")
|
||||
default:
|
||||
sess.Asc("deadline_unix")
|
||||
}
|
||||
return miles, sess.Find(&miles)
|
||||
}
|
||||
|
||||
// GetMilestonesByRepoIDs returns a list of milestones of given repositories and status.
|
||||
func GetMilestonesByRepoIDs(ctx context.Context, repoIDs []int64, page int, isClosed bool, sortType string) (MilestoneList, error) {
|
||||
return SearchMilestones(
|
||||
ctx,
|
||||
builder.In("repo_id", repoIDs),
|
||||
page,
|
||||
isClosed,
|
||||
sortType,
|
||||
"",
|
||||
)
|
||||
}
|
||||
|
||||
// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
|
||||
func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error {
|
||||
type totalTimesByMilestone struct {
|
||||
@ -183,47 +128,9 @@ func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountMilestones returns number of milestones in given repository with other options
|
||||
func CountMilestones(ctx context.Context, opts GetMilestonesOption) (int64, error) {
|
||||
return db.GetEngine(ctx).
|
||||
Where(opts.toCond()).
|
||||
Count(new(Milestone))
|
||||
}
|
||||
|
||||
// CountMilestonesByRepoCond map from repo conditions to number of milestones matching the options`
|
||||
func CountMilestonesByRepoCond(ctx context.Context, repoCond builder.Cond, isClosed bool) (map[int64]int64, error) {
|
||||
sess := db.GetEngine(ctx).Where("is_closed = ?", isClosed)
|
||||
if repoCond.IsValid() {
|
||||
sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
|
||||
}
|
||||
|
||||
countsSlice := make([]*struct {
|
||||
RepoID int64
|
||||
Count int64
|
||||
}, 0, 10)
|
||||
if err := sess.GroupBy("repo_id").
|
||||
Select("repo_id AS repo_id, COUNT(*) AS count").
|
||||
Table("milestone").
|
||||
Find(&countsSlice); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
countMap := make(map[int64]int64, len(countsSlice))
|
||||
for _, c := range countsSlice {
|
||||
countMap[c.RepoID] = c.Count
|
||||
}
|
||||
return countMap, nil
|
||||
}
|
||||
|
||||
// CountMilestonesByRepoCondAndKw map from repo conditions and the keyword of milestones' name to number of milestones matching the options`
|
||||
func CountMilestonesByRepoCondAndKw(ctx context.Context, repoCond builder.Cond, keyword string, isClosed bool) (map[int64]int64, error) {
|
||||
sess := db.GetEngine(ctx).Where("is_closed = ?", isClosed)
|
||||
if len(keyword) > 0 {
|
||||
sess = sess.And(builder.Like{"UPPER(name)", strings.ToUpper(keyword)})
|
||||
}
|
||||
if repoCond.IsValid() {
|
||||
sess.In("repo_id", builder.Select("id").From("repository").Where(repoCond))
|
||||
}
|
||||
func CountMilestonesMap(ctx context.Context, opts FindMilestoneOptions) (map[int64]int64, error) {
|
||||
sess := db.GetEngine(ctx).Where(opts.ToConds())
|
||||
|
||||
countsSlice := make([]*struct {
|
||||
RepoID int64
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"xorm.io/builder"
|
||||
@ -39,10 +40,15 @@ func TestGetMilestoneByRepoID(t *testing.T) {
|
||||
func TestGetMilestonesByRepoID(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
test := func(repoID int64, state api.StateType) {
|
||||
var isClosed util.OptionalBool
|
||||
switch state {
|
||||
case api.StateClosed, api.StateOpen:
|
||||
isClosed = util.OptionalBoolOf(state == api.StateClosed)
|
||||
}
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||
milestones, _, err := issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
|
||||
RepoID: repo.ID,
|
||||
State: state,
|
||||
milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
|
||||
RepoID: repo.ID,
|
||||
IsClosed: isClosed,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
@ -77,9 +83,9 @@ func TestGetMilestonesByRepoID(t *testing.T) {
|
||||
test(3, api.StateClosed)
|
||||
test(3, api.StateAll)
|
||||
|
||||
milestones, _, err := issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
|
||||
RepoID: unittest.NonexistentID,
|
||||
State: api.StateOpen,
|
||||
milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
|
||||
RepoID: unittest.NonexistentID,
|
||||
IsClosed: util.OptionalBoolFalse,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, milestones, 0)
|
||||
@ -90,13 +96,13 @@ func TestGetMilestones(t *testing.T) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
test := func(sortType string, sortCond func(*issues_model.Milestone) int) {
|
||||
for _, page := range []int{0, 1} {
|
||||
milestones, _, err := issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
|
||||
milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: setting.UI.IssuePagingNum,
|
||||
},
|
||||
RepoID: repo.ID,
|
||||
State: api.StateOpen,
|
||||
IsClosed: util.OptionalBoolFalse,
|
||||
SortType: sortType,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
@ -107,13 +113,13 @@ func TestGetMilestones(t *testing.T) {
|
||||
}
|
||||
assert.True(t, sort.IntsAreSorted(values))
|
||||
|
||||
milestones, _, err = issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
|
||||
milestones, err = db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: setting.UI.IssuePagingNum,
|
||||
},
|
||||
RepoID: repo.ID,
|
||||
State: api.StateClosed,
|
||||
IsClosed: util.OptionalBoolTrue,
|
||||
Name: "",
|
||||
SortType: sortType,
|
||||
})
|
||||
@ -150,9 +156,8 @@ func TestCountRepoMilestones(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
test := func(repoID int64) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||
count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
|
||||
count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
|
||||
RepoID: repoID,
|
||||
State: api.StateAll,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, repo.NumMilestones, count)
|
||||
@ -161,9 +166,8 @@ func TestCountRepoMilestones(t *testing.T) {
|
||||
test(2)
|
||||
test(3)
|
||||
|
||||
count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
|
||||
count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
|
||||
RepoID: unittest.NonexistentID,
|
||||
State: api.StateAll,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, count)
|
||||
@ -173,9 +177,9 @@ func TestCountRepoClosedMilestones(t *testing.T) {
|
||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||
test := func(repoID int64) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||
count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
|
||||
RepoID: repoID,
|
||||
State: api.StateClosed,
|
||||
count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
|
||||
RepoID: repoID,
|
||||
IsClosed: util.OptionalBoolTrue,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, repo.NumClosedMilestones, count)
|
||||
@ -184,9 +188,9 @@ func TestCountRepoClosedMilestones(t *testing.T) {
|
||||
test(2)
|
||||
test(3)
|
||||
|
||||
count, err := issues_model.CountMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
|
||||
RepoID: unittest.NonexistentID,
|
||||
State: api.StateClosed,
|
||||
count, err := db.Count[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
|
||||
RepoID: unittest.NonexistentID,
|
||||
IsClosed: util.OptionalBoolTrue,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, 0, count)
|
||||
@ -201,12 +205,19 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
|
||||
repo1OpenCount, repo1ClosedCount := milestonesCount(1)
|
||||
repo2OpenCount, repo2ClosedCount := milestonesCount(2)
|
||||
|
||||
openCounts, err := issues_model.CountMilestonesByRepoCond(db.DefaultContext, builder.In("repo_id", []int64{1, 2}), false)
|
||||
openCounts, err := issues_model.CountMilestonesMap(db.DefaultContext, issues_model.FindMilestoneOptions{
|
||||
RepoIDs: []int64{1, 2},
|
||||
IsClosed: util.OptionalBoolFalse,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, repo1OpenCount, openCounts[1])
|
||||
assert.EqualValues(t, repo2OpenCount, openCounts[2])
|
||||
|
||||
closedCounts, err := issues_model.CountMilestonesByRepoCond(db.DefaultContext, builder.In("repo_id", []int64{1, 2}), true)
|
||||
closedCounts, err := issues_model.CountMilestonesMap(db.DefaultContext,
|
||||
issues_model.FindMilestoneOptions{
|
||||
RepoIDs: []int64{1, 2},
|
||||
IsClosed: util.OptionalBoolTrue,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
|
||||
assert.EqualValues(t, repo2ClosedCount, closedCounts[2])
|
||||
@ -218,7 +229,15 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
|
||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
test := func(sortType string, sortCond func(*issues_model.Milestone) int) {
|
||||
for _, page := range []int{0, 1} {
|
||||
openMilestones, err := issues_model.GetMilestonesByRepoIDs(db.DefaultContext, []int64{repo1.ID, repo2.ID}, page, false, sortType)
|
||||
openMilestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: setting.UI.IssuePagingNum,
|
||||
},
|
||||
RepoIDs: []int64{repo1.ID, repo2.ID},
|
||||
IsClosed: util.OptionalBoolFalse,
|
||||
SortType: sortType,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, openMilestones, repo1.NumOpenMilestones+repo2.NumOpenMilestones)
|
||||
values := make([]int, len(openMilestones))
|
||||
@ -227,7 +246,16 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
|
||||
}
|
||||
assert.True(t, sort.IntsAreSorted(values))
|
||||
|
||||
closedMilestones, err := issues_model.GetMilestonesByRepoIDs(db.DefaultContext, []int64{repo1.ID, repo2.ID}, page, true, sortType)
|
||||
closedMilestones, err := db.Find[issues_model.Milestone](db.DefaultContext,
|
||||
issues_model.FindMilestoneOptions{
|
||||
ListOptions: db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: setting.UI.IssuePagingNum,
|
||||
},
|
||||
RepoIDs: []int64{repo1.ID, repo2.ID},
|
||||
IsClosed: util.OptionalBoolTrue,
|
||||
SortType: sortType,
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, closedMilestones, repo1.NumClosedMilestones+repo2.NumClosedMilestones)
|
||||
values = make([]int, len(closedMilestones))
|
||||
|
@ -660,13 +660,10 @@ func GetPullRequestByIssueIDWithNoAttributes(ctx context.Context, issueID int64)
|
||||
|
||||
// GetPullRequestByIssueID returns pull request by given issue ID.
|
||||
func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest, error) {
|
||||
pr := &PullRequest{
|
||||
IssueID: issueID,
|
||||
}
|
||||
has, err := db.GetByBean(ctx, pr)
|
||||
pr, exist, err := db.Get[PullRequest](ctx, builder.Eq{"issue_id": issueID})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
} else if !exist {
|
||||
return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""}
|
||||
}
|
||||
return pr, pr.LoadAttributes(ctx)
|
||||
|
@ -0,0 +1,20 @@
|
||||
-
|
||||
id: 1
|
||||
uid: 1
|
||||
issue_id: 1
|
||||
is_read: true
|
||||
is_mentioned: false
|
||||
|
||||
-
|
||||
id: 2
|
||||
uid: 2
|
||||
issue_id: 1
|
||||
is_read: true
|
||||
is_mentioned: false
|
||||
|
||||
-
|
||||
id: 3
|
||||
uid: 2
|
||||
issue_id: 1 # duplicated with id 2
|
||||
is_read: false
|
||||
is_mentioned: true
|
@ -550,6 +550,8 @@ var migrations = []Migration{
|
||||
NewMigration("Add auth_token table", v1_22.CreateAuthTokenTable),
|
||||
// v282 -> v283
|
||||
NewMigration("Add Index to pull_auto_merge.doer_id", v1_22.AddIndexToPullAutoMergeDoerID),
|
||||
// v283 -> v284
|
||||
NewMigration("Add combined Index to issue_user.uid and issue_id", v1_22.AddCombinedIndexToIssueUser),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current db version
|
||||
|
@ -32,7 +32,12 @@ func AddGitSizeAndLFSSizeToRepositoryTable(x *xorm.Engine) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = sess.Exec(`UPDATE repository SET git_size = size - lfs_size`)
|
||||
_, err = sess.Exec(`UPDATE repository SET size = 0 WHERE size IS NULL`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = sess.Exec(`UPDATE repository SET git_size = size - lfs_size WHERE size > lfs_size`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
14
models/migrations/v1_22/main_test.go
Normal file
14
models/migrations/v1_22/main_test.go
Normal file
@ -0,0 +1,14 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_22 //nolint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
base.MainTest(m)
|
||||
}
|
34
models/migrations/v1_22/v283.go
Normal file
34
models/migrations/v1_22/v283.go
Normal file
@ -0,0 +1,34 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_22 //nolint
|
||||
|
||||
import (
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func AddCombinedIndexToIssueUser(x *xorm.Engine) error {
|
||||
type OldIssueUser struct {
|
||||
IssueID int64
|
||||
UID int64
|
||||
Cnt int64
|
||||
}
|
||||
|
||||
var duplicatedIssueUsers []OldIssueUser
|
||||
if err := x.SQL("select * from (select issue_id, uid, count(1) as cnt from issue_user group by issue_id, uid) a where a.cnt > 1").
|
||||
Find(&duplicatedIssueUsers); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, issueUser := range duplicatedIssueUsers {
|
||||
if _, err := x.Exec("delete from issue_user where id in (SELECT id FROM issue_user WHERE issue_id = ? and uid = ? limit ?)", issueUser.IssueID, issueUser.UID, issueUser.Cnt-1); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
type IssueUser struct {
|
||||
UID int64 `xorm:"INDEX unique(uid_to_issue)"` // User ID.
|
||||
IssueID int64 `xorm:"INDEX unique(uid_to_issue)"`
|
||||
}
|
||||
|
||||
return x.Sync(&IssueUser{})
|
||||
}
|
28
models/migrations/v1_22/v283_test.go
Normal file
28
models/migrations/v1_22/v283_test.go
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package v1_22 //nolint
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/migrations/base"
|
||||
)
|
||||
|
||||
func Test_AddCombinedIndexToIssueUser(t *testing.T) {
|
||||
type IssueUser struct {
|
||||
UID int64 `xorm:"INDEX unique(uid_to_issue)"` // User ID.
|
||||
IssueID int64 `xorm:"INDEX unique(uid_to_issue)"`
|
||||
}
|
||||
|
||||
// Prepare and load the testing database
|
||||
x, deferable := base.PrepareTestEnv(t, 0, new(IssueUser))
|
||||
defer deferable()
|
||||
if x == nil || t.Failed() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := AddCombinedIndexToIssueUser(x); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
@ -162,7 +162,7 @@ func NewTeam(ctx context.Context, t *organization.Team) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
has, err := db.GetEngine(ctx).ID(t.OrgID).Get(new(user_model.User))
|
||||
has, err := db.ExistByID[user_model.User](ctx, t.OrgID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -171,10 +171,10 @@ func NewTeam(ctx context.Context, t *organization.Team) (err error) {
|
||||
}
|
||||
|
||||
t.LowerName = strings.ToLower(t.Name)
|
||||
has, err = db.GetEngine(ctx).
|
||||
Where("org_id=?", t.OrgID).
|
||||
And("lower_name=?", t.LowerName).
|
||||
Get(new(organization.Team))
|
||||
has, err = db.Exist[organization.Team](ctx, builder.Eq{
|
||||
"org_id": t.OrgID,
|
||||
"lower_name": t.LowerName,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -232,20 +232,20 @@ func UpdateTeam(ctx context.Context, t *organization.Team, authChanged, includeA
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
sess := db.GetEngine(ctx)
|
||||
|
||||
t.LowerName = strings.ToLower(t.Name)
|
||||
has, err := sess.
|
||||
Where("org_id=?", t.OrgID).
|
||||
And("lower_name=?", t.LowerName).
|
||||
And("id!=?", t.ID).
|
||||
Get(new(organization.Team))
|
||||
has, err := db.Exist[organization.Team](ctx, builder.Eq{
|
||||
"org_id": t.OrgID,
|
||||
"lower_name": t.LowerName,
|
||||
}.And(builder.Neq{"id": t.ID}),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
return organization.ErrTeamAlreadyExist{OrgID: t.OrgID, Name: t.LowerName}
|
||||
}
|
||||
|
||||
sess := db.GetEngine(ctx)
|
||||
if _, err = sess.ID(t.ID).Cols("name", "lower_name", "description",
|
||||
"can_create_org_repo", "authorize", "includes_all_repositories").Update(t); err != nil {
|
||||
return fmt.Errorf("update: %w", err)
|
||||
|
@ -16,6 +16,8 @@ import (
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ___________
|
||||
@ -203,14 +205,10 @@ func IsUsableTeamName(name string) error {
|
||||
|
||||
// GetTeam returns team by given team name and organization.
|
||||
func GetTeam(ctx context.Context, orgID int64, name string) (*Team, error) {
|
||||
t := &Team{
|
||||
OrgID: orgID,
|
||||
LowerName: strings.ToLower(name),
|
||||
}
|
||||
has, err := db.GetByBean(ctx, t)
|
||||
t, exist, err := db.Get[Team](ctx, builder.Eq{"org_id": orgID, "lower_name": strings.ToLower(name)})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
} else if !exist {
|
||||
return nil, ErrTeamNotExist{orgID, 0, name}
|
||||
}
|
||||
return t, nil
|
||||
|
@ -13,6 +13,8 @@ import (
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// Access represents the highest access level of a user to the repository. The only access type
|
||||
@ -51,9 +53,11 @@ func accessLevel(ctx context.Context, user *user_model.User, repo *repo_model.Re
|
||||
return perm.AccessModeOwner, nil
|
||||
}
|
||||
|
||||
a := &Access{UserID: userID, RepoID: repo.ID}
|
||||
if has, err := db.GetByBean(ctx, a); !has || err != nil {
|
||||
a, exist, err := db.Get[Access](ctx, builder.Eq{"user_id": userID, "repo_id": repo.ID})
|
||||
if err != nil {
|
||||
return mode, err
|
||||
} else if !exist {
|
||||
return mode, nil
|
||||
}
|
||||
return a.Mode, nil
|
||||
}
|
||||
|
@ -294,6 +294,18 @@ func GetProjectByID(ctx context.Context, id int64) (*Project, error) {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// GetProjectForRepoByID returns the projects in a repository
|
||||
func GetProjectForRepoByID(ctx context.Context, repoID, id int64) (*Project, error) {
|
||||
p := new(Project)
|
||||
has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", id, repoID).Get(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrProjectNotExist{ID: id}
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// UpdateProject updates project properties
|
||||
func UpdateProject(ctx context.Context, p *Project) error {
|
||||
if !IsCardTypeValid(p.CardType) {
|
||||
|
@ -207,6 +207,21 @@ func GetReleaseByID(ctx context.Context, id int64) (*Release, error) {
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// GetReleaseForRepoByID returns release with given ID.
|
||||
func GetReleaseForRepoByID(ctx context.Context, repoID, id int64) (*Release, error) {
|
||||
rel := new(Release)
|
||||
has, err := db.GetEngine(ctx).
|
||||
Where("id=? AND repo_id=?", id, repoID).
|
||||
Get(rel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrReleaseNotExist{id, ""}
|
||||
}
|
||||
|
||||
return rel, nil
|
||||
}
|
||||
|
||||
// FindReleasesOptions describes the conditions to Find releases
|
||||
type FindReleasesOptions struct {
|
||||
db.ListOptions
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
@ -179,6 +180,7 @@ type Repository struct {
|
||||
IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"`
|
||||
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
|
||||
Topics []string `xorm:"TEXT JSON"`
|
||||
ObjectFormat git.ObjectFormat `xorm:"-"`
|
||||
|
||||
TrustModel TrustModelType
|
||||
|
||||
@ -274,6 +276,8 @@ func (repo *Repository) AfterLoad() {
|
||||
repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
|
||||
repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects
|
||||
repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns
|
||||
|
||||
repo.ObjectFormat = git.ObjectFormatFromID(git.Sha1)
|
||||
}
|
||||
|
||||
// LoadAttributes loads attributes of the repository.
|
||||
@ -313,7 +317,7 @@ func (repo *Repository) HTMLURL() string {
|
||||
// CommitLink make link to by commit full ID
|
||||
// note: won't check whether it's an right id
|
||||
func (repo *Repository) CommitLink(commitID string) (result string) {
|
||||
if commitID == "" || commitID == "0000000000000000000000000000000000000000" {
|
||||
if git.IsEmptyCommitID(commitID) {
|
||||
result = ""
|
||||
} else {
|
||||
result = repo.Link() + "/commit/" + url.PathEscape(commitID)
|
||||
|
@ -13,6 +13,8 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting/config"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type Setting struct {
|
||||
@ -36,16 +38,17 @@ func init() {
|
||||
const keyRevision = "revision"
|
||||
|
||||
func GetRevision(ctx context.Context) int {
|
||||
revision := &Setting{SettingKey: keyRevision}
|
||||
if has, err := db.GetByBean(ctx, revision); err != nil {
|
||||
revision, exist, err := db.Get[Setting](ctx, builder.Eq{"setting_key": keyRevision})
|
||||
if err != nil {
|
||||
return 0
|
||||
} else if !has {
|
||||
} else if !exist {
|
||||
err = db.Insert(ctx, &Setting{SettingKey: keyRevision, Version: 1})
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return 1
|
||||
} else if revision.Version <= 0 || revision.Version >= math.MaxInt-1 {
|
||||
}
|
||||
if revision.Version <= 0 || revision.Version >= math.MaxInt-1 {
|
||||
_, err = db.Exec(ctx, "UPDATE system_setting SET version=1 WHERE setting_key=?", keyRevision)
|
||||
if err != nil {
|
||||
return 0
|
||||
@ -81,7 +84,7 @@ func SetSettings(ctx context.Context, settings map[string]string) error {
|
||||
return err
|
||||
}
|
||||
for k, v := range settings {
|
||||
res, err := e.Exec("UPDATE system_setting SET setting_value=? WHERE setting_key=?", v, k)
|
||||
res, err := e.Exec("UPDATE system_setting SET version=version+1, setting_value=? WHERE setting_key=?", v, k)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -39,4 +39,13 @@ func TestSettings(t *testing.T) {
|
||||
assert.EqualValues(t, 3, rev)
|
||||
assert.Len(t, settings, 2)
|
||||
assert.EqualValues(t, "false", settings[keyName])
|
||||
|
||||
// setting the same value should not trigger DuplicateKey error, and the "version" should be increased
|
||||
err = system.SetSettings(db.DefaultContext, map[string]string{keyName: "false"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
rev, settings, err = system.GetAllSettings(db.DefaultContext)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, settings, 2)
|
||||
assert.EqualValues(t, 4, rev)
|
||||
}
|
||||
|
@ -527,12 +527,13 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
|
||||
|
||||
// Activate/deactivate a user's secondary email address
|
||||
// First check if there's another user active with the same address
|
||||
addr := EmailAddress{UID: userID, LowerEmail: strings.ToLower(email)}
|
||||
if has, err := db.GetByBean(ctx, &addr); err != nil {
|
||||
addr, exist, err := db.Get[EmailAddress](ctx, builder.Eq{"uid": userID, "lower_email": strings.ToLower(email)})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
} else if !exist {
|
||||
return fmt.Errorf("no such email: %d (%s)", userID, email)
|
||||
}
|
||||
|
||||
if addr.IsActivated == activate {
|
||||
// Already in the desired state; no action
|
||||
return nil
|
||||
@ -544,25 +545,26 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
|
||||
return ErrEmailAlreadyUsed{Email: email}
|
||||
}
|
||||
}
|
||||
if err = updateActivation(ctx, &addr, activate); err != nil {
|
||||
if err = updateActivation(ctx, addr, activate); err != nil {
|
||||
return fmt.Errorf("unable to updateActivation() for %d:%s: %w", addr.ID, addr.Email, err)
|
||||
}
|
||||
|
||||
// Activate/deactivate a user's primary email address and account
|
||||
if addr.IsPrimary {
|
||||
user := User{ID: userID, Email: email}
|
||||
if has, err := db.GetByBean(ctx, &user); err != nil {
|
||||
user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID, "email": email})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
} else if !exist {
|
||||
return fmt.Errorf("no user with ID: %d and Email: %s", userID, email)
|
||||
}
|
||||
|
||||
// The user's activation state should be synchronized with the primary email
|
||||
if user.IsActive != activate {
|
||||
user.IsActive = activate
|
||||
if user.Rands, err = GetUserSalt(); err != nil {
|
||||
return fmt.Errorf("unable to generate salt: %w", err)
|
||||
}
|
||||
if err = UpdateUserCols(ctx, &user, "is_active", "rands"); err != nil {
|
||||
if err = UpdateUserCols(ctx, user, "is_active", "rands"); err != nil {
|
||||
return fmt.Errorf("unable to updateUserCols() for user ID: %d: %w", userID, err)
|
||||
}
|
||||
}
|
||||
|
@ -98,9 +98,10 @@ func GetExternalLogin(ctx context.Context, externalLoginUser *ExternalLoginUser)
|
||||
|
||||
// LinkExternalToUser link the external user to the user
|
||||
func LinkExternalToUser(ctx context.Context, user *User, externalLoginUser *ExternalLoginUser) error {
|
||||
has, err := db.GetEngine(ctx).Where("external_id=? AND login_source_id=?", externalLoginUser.ExternalID, externalLoginUser.LoginSourceID).
|
||||
NoAutoCondition().
|
||||
Exist(externalLoginUser)
|
||||
has, err := db.Exist[ExternalLoginUser](ctx, builder.Eq{
|
||||
"external_id": externalLoginUser.ExternalID,
|
||||
"login_source_id": externalLoginUser.LoginSourceID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if has {
|
||||
@ -145,9 +146,10 @@ func GetUserIDByExternalUserID(ctx context.Context, provider, userID string) (in
|
||||
|
||||
// UpdateExternalUserByExternalID updates an external user's information
|
||||
func UpdateExternalUserByExternalID(ctx context.Context, external *ExternalLoginUser) error {
|
||||
has, err := db.GetEngine(ctx).Where("external_id=? AND login_source_id=?", external.ExternalID, external.LoginSourceID).
|
||||
NoAutoCondition().
|
||||
Exist(external)
|
||||
has, err := db.Exist[ExternalLoginUser](ctx, builder.Eq{
|
||||
"external_id": external.ExternalID,
|
||||
"login_source_id": external.LoginSourceID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||
|
||||
gouuid "github.com/google/uuid"
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
// ___ ___ __ ___________ __
|
||||
@ -150,14 +151,10 @@ func UpdateHookTask(ctx context.Context, t *HookTask) error {
|
||||
|
||||
// ReplayHookTask copies a hook task to get re-delivered
|
||||
func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask, error) {
|
||||
task := &HookTask{
|
||||
HookID: hookID,
|
||||
UUID: uuid,
|
||||
}
|
||||
has, err := db.GetByBean(ctx, task)
|
||||
task, exist, err := db.Get[HookTask](ctx, builder.Eq{"hook_id": hookID, "uuid": uuid})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
} else if !exist {
|
||||
return nil, ErrHookTaskNotExist{
|
||||
HookID: hookID,
|
||||
UUID: uuid,
|
||||
|
@ -392,39 +392,40 @@ func CreateWebhooks(ctx context.Context, ws []*Webhook) error {
|
||||
return db.Insert(ctx, ws)
|
||||
}
|
||||
|
||||
// getWebhook uses argument bean as query condition,
|
||||
// ID must be specified and do not assign unnecessary fields.
|
||||
func getWebhook(ctx context.Context, bean *Webhook) (*Webhook, error) {
|
||||
has, err := db.GetEngine(ctx).Get(bean)
|
||||
// GetWebhookByID returns webhook of repository by given ID.
|
||||
func GetWebhookByID(ctx context.Context, id int64) (*Webhook, error) {
|
||||
bean := new(Webhook)
|
||||
has, err := db.GetEngine(ctx).ID(id).Get(bean)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrWebhookNotExist{ID: bean.ID}
|
||||
return nil, ErrWebhookNotExist{ID: id}
|
||||
}
|
||||
return bean, nil
|
||||
}
|
||||
|
||||
// GetWebhookByID returns webhook of repository by given ID.
|
||||
func GetWebhookByID(ctx context.Context, id int64) (*Webhook, error) {
|
||||
return getWebhook(ctx, &Webhook{
|
||||
ID: id,
|
||||
})
|
||||
}
|
||||
|
||||
// GetWebhookByRepoID returns webhook of repository by given ID.
|
||||
func GetWebhookByRepoID(ctx context.Context, repoID, id int64) (*Webhook, error) {
|
||||
return getWebhook(ctx, &Webhook{
|
||||
ID: id,
|
||||
RepoID: repoID,
|
||||
})
|
||||
webhook := new(Webhook)
|
||||
has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", id, repoID).Get(webhook)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrWebhookNotExist{ID: id}
|
||||
}
|
||||
return webhook, nil
|
||||
}
|
||||
|
||||
// GetWebhookByOwnerID returns webhook of a user or organization by given ID.
|
||||
func GetWebhookByOwnerID(ctx context.Context, ownerID, id int64) (*Webhook, error) {
|
||||
return getWebhook(ctx, &Webhook{
|
||||
ID: id,
|
||||
OwnerID: ownerID,
|
||||
})
|
||||
webhook := new(Webhook)
|
||||
has, err := db.GetEngine(ctx).Where("id=? AND owner_id=?", id, ownerID).Get(webhook)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
return nil, ErrWebhookNotExist{ID: id}
|
||||
}
|
||||
return webhook, nil
|
||||
}
|
||||
|
||||
// ListWebhookOptions are options to filter webhooks on ListWebhooksByOpts
|
||||
@ -461,20 +462,20 @@ func UpdateWebhookLastStatus(ctx context.Context, w *Webhook) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// deleteWebhook uses argument bean as query condition,
|
||||
// DeleteWebhookByID uses argument bean as query condition,
|
||||
// ID must be specified and do not assign unnecessary fields.
|
||||
func deleteWebhook(ctx context.Context, bean *Webhook) (err error) {
|
||||
func DeleteWebhookByID(ctx context.Context, id int64) (err error) {
|
||||
ctx, committer, err := db.TxContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer committer.Close()
|
||||
|
||||
if count, err := db.DeleteByBean(ctx, bean); err != nil {
|
||||
if count, err := db.DeleteByID(ctx, id, new(Webhook)); err != nil {
|
||||
return err
|
||||
} else if count == 0 {
|
||||
return ErrWebhookNotExist{ID: bean.ID}
|
||||
} else if _, err = db.DeleteByBean(ctx, &HookTask{HookID: bean.ID}); err != nil {
|
||||
return ErrWebhookNotExist{ID: id}
|
||||
} else if _, err = db.DeleteByBean(ctx, &HookTask{HookID: id}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -483,16 +484,16 @@ func deleteWebhook(ctx context.Context, bean *Webhook) (err error) {
|
||||
|
||||
// DeleteWebhookByRepoID deletes webhook of repository by given ID.
|
||||
func DeleteWebhookByRepoID(ctx context.Context, repoID, id int64) error {
|
||||
return deleteWebhook(ctx, &Webhook{
|
||||
ID: id,
|
||||
RepoID: repoID,
|
||||
})
|
||||
if _, err := GetWebhookByRepoID(ctx, repoID, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return DeleteWebhookByID(ctx, id)
|
||||
}
|
||||
|
||||
// DeleteWebhookByOwnerID deletes webhook of a user or organization by given ID.
|
||||
func DeleteWebhookByOwnerID(ctx context.Context, ownerID, id int64) error {
|
||||
return deleteWebhook(ctx, &Webhook{
|
||||
ID: id,
|
||||
OwnerID: ownerID,
|
||||
})
|
||||
if _, err := GetWebhookByOwnerID(ctx, ownerID, id); err != nil {
|
||||
return err
|
||||
}
|
||||
return DeleteWebhookByID(ctx, id)
|
||||
}
|
||||
|
@ -308,6 +308,12 @@ func RepoRefForAPI(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat()
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetCommit", err)
|
||||
return
|
||||
}
|
||||
|
||||
if ref := ctx.FormTrim("ref"); len(ref) > 0 {
|
||||
commit, err := ctx.Repo.GitRepo.GetCommit(ref)
|
||||
if err != nil {
|
||||
@ -325,7 +331,6 @@ func RepoRefForAPI(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny)
|
||||
|
||||
if ctx.Repo.GitRepo.IsBranchExist(refName) {
|
||||
@ -342,7 +347,7 @@ func RepoRefForAPI(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
|
||||
} else if len(refName) == git.SHAFullLength {
|
||||
} else if len(refName) == objectFormat.FullLength() {
|
||||
ctx.Repo.CommitID = refName
|
||||
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
|
||||
if err != nil {
|
||||
|
@ -668,11 +668,9 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
|
||||
branchOpts := git_model.FindBranchOptions{
|
||||
RepoID: ctx.Repo.Repository.ID,
|
||||
IsDeletedBranch: util.OptionalBoolFalse,
|
||||
ListOptions: db.ListOptions{
|
||||
ListAll: true,
|
||||
},
|
||||
ListOptions: db.ListOptionsAll,
|
||||
}
|
||||
branchesTotal, err := git_model.CountBranches(ctx, branchOpts)
|
||||
branchesTotal, err := db.Count[git_model.Branch](ctx, branchOpts)
|
||||
if err != nil {
|
||||
ctx.ServerError("CountBranches", err)
|
||||
return cancel
|
||||
@ -827,7 +825,9 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
|
||||
}
|
||||
// For legacy and API support only full commit sha
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) > 0 && len(parts[0]) == git.SHAFullLength {
|
||||
objectFormat, _ := repo.GitRepo.GetObjectFormat()
|
||||
|
||||
if len(parts) > 0 && len(parts[0]) == objectFormat.FullLength() {
|
||||
repo.TreePath = strings.Join(parts[1:], "/")
|
||||
return parts[0]
|
||||
}
|
||||
@ -871,7 +871,9 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
|
||||
return getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsTagExist)
|
||||
case RepoRefCommit:
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= git.SHAFullLength {
|
||||
objectFormat, _ := repo.GitRepo.GetObjectFormat()
|
||||
|
||||
if len(parts) > 0 && len(parts[0]) >= 7 && len(parts[0]) <= objectFormat.FullLength() {
|
||||
repo.TreePath = strings.Join(parts[1:], "/")
|
||||
return parts[0]
|
||||
}
|
||||
@ -931,6 +933,12 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
|
||||
}
|
||||
}
|
||||
|
||||
objectFormat, err := ctx.Repo.GitRepo.GetObjectFormat()
|
||||
if err != nil {
|
||||
log.Error("Cannot determine objectFormat for repository: %w", err)
|
||||
ctx.Repo.Repository.MarkAsBrokenEmpty()
|
||||
}
|
||||
|
||||
// Get default branch.
|
||||
if len(ctx.Params("*")) == 0 {
|
||||
refName = ctx.Repo.Repository.DefaultBranch
|
||||
@ -997,7 +1005,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
|
||||
return cancel
|
||||
}
|
||||
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
|
||||
} else if len(refName) >= 7 && len(refName) <= git.SHAFullLength {
|
||||
} else if len(refName) >= 7 && len(refName) <= objectFormat.FullLength() {
|
||||
ctx.Repo.IsViewCommit = true
|
||||
ctx.Repo.CommitID = refName
|
||||
|
||||
@ -1007,7 +1015,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
|
||||
return cancel
|
||||
}
|
||||
// If short commit ID add canonical link header
|
||||
if len(refName) < git.SHAFullLength {
|
||||
if len(refName) < objectFormat.FullLength() {
|
||||
ctx.RespHeader().Set("Link", fmt.Sprintf("<%s>; rel=\"canonical\"",
|
||||
util.URLJoin(setting.AppURL, strings.Replace(ctx.Req.URL.RequestURI(), util.PathEscapeSegments(refName), url.PathEscape(ctx.Repo.Commit.ID.String()), 1))))
|
||||
}
|
||||
|
@ -79,6 +79,7 @@ var Checks []*Check
|
||||
|
||||
// RunChecks runs the doctor checks for the provided list
|
||||
func RunChecks(ctx context.Context, colorize, autofix bool, checks []*Check) error {
|
||||
SortChecks(checks)
|
||||
// the checks output logs by a special logger, they do not use the default logger
|
||||
logger := log.BaseLoggerToGeneralLogger(&doctorCheckLogger{colorize: colorize})
|
||||
loggerStep := log.BaseLoggerToGeneralLogger(&doctorCheckStepLogger{colorize: colorize})
|
||||
@ -104,20 +105,23 @@ func RunChecks(ctx context.Context, colorize, autofix bool, checks []*Check) err
|
||||
logger.Info("OK")
|
||||
}
|
||||
}
|
||||
logger.Info("\nAll done.")
|
||||
logger.Info("\nAll done (checks: %d).", len(checks))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Register registers a command with the list
|
||||
func Register(command *Check) {
|
||||
Checks = append(Checks, command)
|
||||
sort.SliceStable(Checks, func(i, j int) bool {
|
||||
if Checks[i].Priority == Checks[j].Priority {
|
||||
return Checks[i].Name < Checks[j].Name
|
||||
}
|
||||
|
||||
func SortChecks(checks []*Check) {
|
||||
sort.SliceStable(checks, func(i, j int) bool {
|
||||
if checks[i].Priority == checks[j].Priority {
|
||||
return checks[i].Name < checks[j].Name
|
||||
}
|
||||
if Checks[i].Priority == 0 {
|
||||
if checks[i].Priority == 0 {
|
||||
return false
|
||||
}
|
||||
return Checks[i].Priority < Checks[j].Priority
|
||||
return checks[i].Priority < checks[j].Priority
|
||||
})
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ func handleDeleteOrphanedRepos(ctx context.Context, logger log.Logger, autofix b
|
||||
|
||||
// countOrphanedRepos count repository where user of owner_id do not exist
|
||||
func countOrphanedRepos(ctx context.Context) (int64, error) {
|
||||
return db.CountOrphanedObjects(ctx, "repository", "user", "repository.owner_id=user.id")
|
||||
return db.CountOrphanedObjects(ctx, "repository", "user", "repository.owner_id=`user`.id")
|
||||
}
|
||||
|
||||
// deleteOrphanedRepos delete repository where user of owner_id do not exist
|
||||
@ -43,7 +43,7 @@ func deleteOrphanedRepos(ctx context.Context) (int64, error) {
|
||||
default:
|
||||
var ids []int64
|
||||
if err := e.Table("`repository`").
|
||||
Join("LEFT", "`user`", "repository.owner_id=user.id").
|
||||
Join("LEFT", "`user`", "repository.owner_id=`user`.id").
|
||||
Where(builder.IsNull{"`user`.id"}).
|
||||
Select("`repository`.id").Limit(batchSize).Find(&ids); err != nil {
|
||||
return deleted, err
|
||||
|
@ -148,7 +148,7 @@ func CatFileBatch(ctx context.Context, repoPath string) (WriteCloserError, *bufi
|
||||
// ReadBatchLine reads the header line from cat-file --batch
|
||||
// We expect:
|
||||
// <sha> SP <type> SP <size> LF
|
||||
// sha is a 40byte not 20byte here
|
||||
// sha is a hex encoded here
|
||||
func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err error) {
|
||||
typ, err = rd.ReadString('\n')
|
||||
if err != nil {
|
||||
@ -251,20 +251,19 @@ headerLoop:
|
||||
}
|
||||
|
||||
// git tree files are a list:
|
||||
// <mode-in-ascii> SP <fname> NUL <20-byte SHA>
|
||||
// <mode-in-ascii> SP <fname> NUL <binary Hash>
|
||||
//
|
||||
// Unfortunately this 20-byte notation is somewhat in conflict to all other git tools
|
||||
// Therefore we need some method to convert these 20-byte SHAs to a 40-byte SHA
|
||||
// Therefore we need some method to convert these binary hashes to hex hashes
|
||||
|
||||
// constant hextable to help quickly convert between 20byte and 40byte hashes
|
||||
// constant hextable to help quickly convert between binary and hex representation
|
||||
const hextable = "0123456789abcdef"
|
||||
|
||||
// To40ByteSHA converts a 20-byte SHA into a 40-byte sha. Input and output can be the
|
||||
// same 40 byte slice to support in place conversion without allocations.
|
||||
// BinToHexHeash converts a binary Hash into a hex encoded one. Input and output can be the
|
||||
// same byte slice to support in place conversion without allocations.
|
||||
// This is at least 100x quicker that hex.EncodeToString
|
||||
// NB This requires that out is a 40-byte slice
|
||||
func To40ByteSHA(sha, out []byte) []byte {
|
||||
for i := 19; i >= 0; i-- {
|
||||
func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte {
|
||||
for i := objectFormat.FullLength()/2 - 1; i >= 0; i-- {
|
||||
v := sha[i]
|
||||
vhi, vlo := v>>4, v&0x0f
|
||||
shi, slo := hextable[vhi], hextable[vlo]
|
||||
@ -278,10 +277,10 @@ func To40ByteSHA(sha, out []byte) []byte {
|
||||
// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations
|
||||
//
|
||||
// Each line is composed of:
|
||||
// <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <20-byte SHA>
|
||||
// <mode-in-ascii-dropping-initial-zeros> SP <fname> NUL <binary HASH>
|
||||
//
|
||||
// We don't attempt to convert the 20-byte SHA to 40-byte SHA to save a lot of time
|
||||
func ParseTreeLine(rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
|
||||
// We don't attempt to convert the raw HASH to save a lot of time
|
||||
func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
|
||||
var readBytes []byte
|
||||
|
||||
// Read the Mode & fname
|
||||
@ -324,11 +323,12 @@ func ParseTreeLine(rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fn
|
||||
fnameBuf = fnameBuf[:len(fnameBuf)-1]
|
||||
fname = fnameBuf
|
||||
|
||||
// Deal with the 20-byte SHA
|
||||
// Deal with the binary hash
|
||||
idx = 0
|
||||
for idx < 20 {
|
||||
len := objectFormat.FullLength() / 2
|
||||
for idx < len {
|
||||
var read int
|
||||
read, err = rd.Read(shaBuf[idx:20])
|
||||
read, err = rd.Read(shaBuf[idx:len])
|
||||
n += read
|
||||
if err != nil {
|
||||
return mode, fname, sha, n, err
|
||||
|
@ -10,7 +10,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
@ -18,8 +17,10 @@ import (
|
||||
|
||||
// BlamePart represents block of blame - continuous lines with one sha
|
||||
type BlamePart struct {
|
||||
Sha string
|
||||
Lines []string
|
||||
Sha string
|
||||
Lines []string
|
||||
PreviousSha string
|
||||
PreviousPath string
|
||||
}
|
||||
|
||||
// BlameReader returns part of file blame one by one
|
||||
@ -30,47 +31,56 @@ type BlameReader struct {
|
||||
done chan error
|
||||
lastSha *string
|
||||
ignoreRevsFile *string
|
||||
objectFormat ObjectFormat
|
||||
}
|
||||
|
||||
func (r *BlameReader) UsesIgnoreRevs() bool {
|
||||
return r.ignoreRevsFile != nil
|
||||
}
|
||||
|
||||
var shaLineRegex = regexp.MustCompile("^([a-z0-9]{40})")
|
||||
|
||||
// NextPart returns next part of blame (sequential code lines with the same commit)
|
||||
func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
var blamePart *BlamePart
|
||||
|
||||
if r.lastSha != nil {
|
||||
blamePart = &BlamePart{*r.lastSha, make([]string, 0)}
|
||||
blamePart = &BlamePart{
|
||||
Sha: *r.lastSha,
|
||||
Lines: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
var line []byte
|
||||
const previousHeader = "previous "
|
||||
var lineBytes []byte
|
||||
var isPrefix bool
|
||||
var err error
|
||||
|
||||
for err != io.EOF {
|
||||
line, isPrefix, err = r.bufferedReader.ReadLine()
|
||||
lineBytes, isPrefix, err = r.bufferedReader.ReadLine()
|
||||
if err != nil && err != io.EOF {
|
||||
return blamePart, err
|
||||
}
|
||||
|
||||
if len(line) == 0 {
|
||||
if len(lineBytes) == 0 {
|
||||
// isPrefix will be false
|
||||
continue
|
||||
}
|
||||
|
||||
lines := shaLineRegex.FindSubmatch(line)
|
||||
if lines != nil {
|
||||
sha1 := string(lines[1])
|
||||
var objectID string
|
||||
objectFormatLength := r.objectFormat.FullLength()
|
||||
|
||||
if len(lineBytes) > objectFormatLength && lineBytes[objectFormatLength] == ' ' && r.objectFormat.IsValid(string(lineBytes[0:objectFormatLength])) {
|
||||
objectID = string(lineBytes[0:objectFormatLength])
|
||||
}
|
||||
if len(objectID) > 0 {
|
||||
if blamePart == nil {
|
||||
blamePart = &BlamePart{sha1, make([]string, 0)}
|
||||
blamePart = &BlamePart{
|
||||
Sha: objectID,
|
||||
Lines: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
if blamePart.Sha != sha1 {
|
||||
r.lastSha = &sha1
|
||||
if blamePart.Sha != objectID {
|
||||
r.lastSha = &objectID
|
||||
// need to munch to end of line...
|
||||
for isPrefix {
|
||||
_, isPrefix, err = r.bufferedReader.ReadLine()
|
||||
@ -80,10 +90,13 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||
}
|
||||
return blamePart, nil
|
||||
}
|
||||
} else if line[0] == '\t' {
|
||||
code := line[1:]
|
||||
|
||||
blamePart.Lines = append(blamePart.Lines, string(code))
|
||||
} else if lineBytes[0] == '\t' {
|
||||
blamePart.Lines = append(blamePart.Lines, string(lineBytes[1:]))
|
||||
} else if bytes.HasPrefix(lineBytes, []byte(previousHeader)) {
|
||||
offset := len(previousHeader) // already includes a space
|
||||
blamePart.PreviousSha = string(lineBytes[offset : offset+objectFormatLength])
|
||||
offset += objectFormatLength + 1 // +1 for space
|
||||
blamePart.PreviousPath = string(lineBytes[offset:])
|
||||
}
|
||||
|
||||
// need to munch to end of line...
|
||||
@ -113,7 +126,7 @@ func (r *BlameReader) Close() error {
|
||||
}
|
||||
|
||||
// CreateBlameReader creates reader for given repository, commit and file
|
||||
func CreateBlameReader(ctx context.Context, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) {
|
||||
func CreateBlameReader(ctx context.Context, objectFormat ObjectFormat, repoPath string, commit *Commit, file string, bypassBlameIgnore bool) (*BlameReader, error) {
|
||||
var ignoreRevsFile *string
|
||||
if CheckGitVersionAtLeast("2.23") == nil && !bypassBlameIgnore {
|
||||
ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit)
|
||||
@ -162,6 +175,7 @@ func CreateBlameReader(ctx context.Context, repoPath string, commit *Commit, fil
|
||||
bufferedReader: bufferedReader,
|
||||
done: done,
|
||||
ignoreRevsFile: ignoreRevsFile,
|
||||
objectFormat: objectFormat,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -24,20 +24,22 @@ func TestReadingBlameOutput(t *testing.T) {
|
||||
|
||||
parts := []*BlamePart{
|
||||
{
|
||||
"72866af952e98d02a73003501836074b286a78f6",
|
||||
[]string{
|
||||
Sha: "72866af952e98d02a73003501836074b286a78f6",
|
||||
Lines: []string{
|
||||
"# test_repo",
|
||||
"Test repository for testing migration from github to gitea",
|
||||
},
|
||||
},
|
||||
{
|
||||
"f32b0a9dfd09a60f616f29158f772cedd89942d2",
|
||||
[]string{"", "Do not make any changes to this repo it is used for unit testing"},
|
||||
Sha: "f32b0a9dfd09a60f616f29158f772cedd89942d2",
|
||||
Lines: []string{"", "Do not make any changes to this repo it is used for unit testing"},
|
||||
PreviousSha: "72866af952e98d02a73003501836074b286a78f6",
|
||||
PreviousPath: "README.md",
|
||||
},
|
||||
}
|
||||
|
||||
for _, bypass := range []bool{false, true} {
|
||||
blameReader, err := CreateBlameReader(ctx, "./tests/repos/repo5_pulls", commit, "README.md", bypass)
|
||||
blameReader, err := CreateBlameReader(ctx, &Sha1ObjectFormat{}, "./tests/repos/repo5_pulls", commit, "README.md", bypass)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, blameReader)
|
||||
defer blameReader.Close()
|
||||
@ -64,16 +66,18 @@ func TestReadingBlameOutput(t *testing.T) {
|
||||
|
||||
full := []*BlamePart{
|
||||
{
|
||||
"af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||
[]string{"line", "line"},
|
||||
Sha: "af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||
Lines: []string{"line", "line"},
|
||||
},
|
||||
{
|
||||
"45fb6cbc12f970b04eacd5cd4165edd11c8d7376",
|
||||
[]string{"changed line"},
|
||||
Sha: "45fb6cbc12f970b04eacd5cd4165edd11c8d7376",
|
||||
Lines: []string{"changed line"},
|
||||
PreviousSha: "af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||
PreviousPath: "blame.txt",
|
||||
},
|
||||
{
|
||||
"af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||
[]string{"line", "line", ""},
|
||||
Sha: "af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||
Lines: []string{"line", "line", ""},
|
||||
},
|
||||
}
|
||||
|
||||
@ -89,8 +93,8 @@ func TestReadingBlameOutput(t *testing.T) {
|
||||
Bypass: false,
|
||||
Parts: []*BlamePart{
|
||||
{
|
||||
"af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||
[]string{"line", "line", "changed line", "line", "line", ""},
|
||||
Sha: "af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||
Lines: []string{"line", "line", "changed line", "line", "line", ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -118,7 +122,7 @@ func TestReadingBlameOutput(t *testing.T) {
|
||||
commit, err := repo.GetCommit(c.CommitID)
|
||||
assert.NoError(t, err)
|
||||
|
||||
blameReader, err := CreateBlameReader(ctx, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass)
|
||||
blameReader, err := CreateBlameReader(ctx, repo.objectFormat, "./tests/repos/repo6_blame", commit, "blame.txt", c.Bypass)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, blameReader)
|
||||
defer blameReader.Close()
|
||||
|
@ -14,7 +14,7 @@ import (
|
||||
|
||||
// Blob represents a Git object.
|
||||
type Blob struct {
|
||||
ID SHA1
|
||||
ID ObjectID
|
||||
|
||||
gogitEncodedObj plumbing.EncodedObject
|
||||
name string
|
||||
|
@ -16,7 +16,7 @@ import (
|
||||
|
||||
// Blob represents a Git object.
|
||||
type Blob struct {
|
||||
ID SHA1
|
||||
ID ObjectID
|
||||
|
||||
gotSize bool
|
||||
size int64
|
||||
|
@ -21,13 +21,13 @@ import (
|
||||
// Commit represents a git commit.
|
||||
type Commit struct {
|
||||
Tree
|
||||
ID SHA1 // The ID of this commit object
|
||||
ID ObjectID // The ID of this commit object
|
||||
Author *Signature
|
||||
Committer *Signature
|
||||
CommitMessage string
|
||||
Signature *CommitGPGSignature
|
||||
|
||||
Parents []SHA1 // SHA1 strings
|
||||
Parents []ObjectID // ID strings
|
||||
submoduleCache *ObjectCache
|
||||
}
|
||||
|
||||
@ -43,15 +43,16 @@ func (c *Commit) Message() string {
|
||||
}
|
||||
|
||||
// Summary returns first line of commit message.
|
||||
// The string is forced to be valid UTF8
|
||||
func (c *Commit) Summary() string {
|
||||
return strings.Split(strings.TrimSpace(c.CommitMessage), "\n")[0]
|
||||
return strings.ToValidUTF8(strings.Split(strings.TrimSpace(c.CommitMessage), "\n")[0], "?")
|
||||
}
|
||||
|
||||
// ParentID returns oid of n-th parent (0-based index).
|
||||
// It returns nil if no such parent exists.
|
||||
func (c *Commit) ParentID(n int) (SHA1, error) {
|
||||
func (c *Commit) ParentID(n int) (ObjectID, error) {
|
||||
if n >= len(c.Parents) {
|
||||
return SHA1{}, ErrNotExist{"", ""}
|
||||
return nil, ErrNotExist{"", ""}
|
||||
}
|
||||
return c.Parents[n], nil
|
||||
}
|
||||
@ -208,9 +209,9 @@ func (c *Commit) CommitsBefore() ([]*Commit, error) {
|
||||
}
|
||||
|
||||
// HasPreviousCommit returns true if a given commitHash is contained in commit's parents
|
||||
func (c *Commit) HasPreviousCommit(commitHash SHA1) (bool, error) {
|
||||
func (c *Commit) HasPreviousCommit(objectID ObjectID) (bool, error) {
|
||||
this := c.ID.String()
|
||||
that := commitHash.String()
|
||||
that := objectID.String()
|
||||
|
||||
if this == that {
|
||||
return false, nil
|
||||
@ -231,9 +232,14 @@ func (c *Commit) HasPreviousCommit(commitHash SHA1) (bool, error) {
|
||||
|
||||
// IsForcePush returns true if a push from oldCommitHash to this is a force push
|
||||
func (c *Commit) IsForcePush(oldCommitID string) (bool, error) {
|
||||
if oldCommitID == EmptySHA {
|
||||
objectFormat, err := c.repo.GetObjectFormat()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if oldCommitID == objectFormat.Empty().String() {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
oldCommit, err := c.repo.GetCommit(oldCommitID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@ -59,11 +59,11 @@ func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
|
||||
|
||||
func convertCommit(c *object.Commit) *Commit {
|
||||
return &Commit{
|
||||
ID: c.Hash,
|
||||
ID: ParseGogitHash(c.Hash),
|
||||
CommitMessage: c.Message,
|
||||
Committer: &c.Committer,
|
||||
Author: &c.Author,
|
||||
Signature: convertPGPSignature(c),
|
||||
Parents: c.ParentHashes,
|
||||
Parents: ParseGogitHashArray(c.ParentHashes),
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ func (tes Entries) GetCommitsInfo(ctx context.Context, commit *Commit, treePath
|
||||
defer commitGraphFile.Close()
|
||||
}
|
||||
|
||||
c, err := commitNodeIndex.Get(commit.ID)
|
||||
c, err := commitNodeIndex.Get(plumbing.Hash(commit.ID.RawValue()))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
@ -153,7 +153,7 @@ func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string,
|
||||
if typ != "commit" {
|
||||
return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID)
|
||||
}
|
||||
c, err = CommitFromReader(commit.repo, MustIDFromString(commitID), io.LimitReader(batchReader, size))
|
||||
c, err = CommitFromReader(commit.repo, commit.ID.Type().MustIDFromString(commitID), io.LimitReader(batchReader, size))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -14,9 +14,9 @@ import (
|
||||
// We need this to interpret commits from cat-file or cat-file --batch
|
||||
//
|
||||
// If used as part of a cat-file --batch stream you need to limit the reader to the correct size
|
||||
func CommitFromReader(gitRepo *Repository, sha SHA1, reader io.Reader) (*Commit, error) {
|
||||
func CommitFromReader(gitRepo *Repository, objectID ObjectID, reader io.Reader) (*Commit, error) {
|
||||
commit := &Commit{
|
||||
ID: sha,
|
||||
ID: objectID,
|
||||
Author: &Signature{},
|
||||
Committer: &Signature{},
|
||||
}
|
||||
@ -71,10 +71,10 @@ readLoop:
|
||||
|
||||
switch string(split[0]) {
|
||||
case "tree":
|
||||
commit.Tree = *NewTree(gitRepo, MustIDFromString(string(data)))
|
||||
commit.Tree = *NewTree(gitRepo, objectID.Type().MustIDFromString(string(data)))
|
||||
_, _ = payloadSB.Write(line)
|
||||
case "parent":
|
||||
commit.Parents = append(commit.Parents, MustIDFromString(string(data)))
|
||||
commit.Parents = append(commit.Parents, objectID.Type().MustIDFromString(string(data)))
|
||||
_, _ = payloadSB.Write(line)
|
||||
case "author":
|
||||
commit.Author = &Signature{}
|
||||
|
@ -81,7 +81,7 @@ gpgsig -----BEGIN PGP SIGNATURE-----
|
||||
|
||||
empty commit`
|
||||
|
||||
sha := SHA1{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2}
|
||||
sha := &Sha1Hash{0xfe, 0xaf, 0x4b, 0xa6, 0xbc, 0x63, 0x5f, 0xec, 0x44, 0x2f, 0x46, 0xdd, 0xd4, 0x51, 0x24, 0x16, 0xec, 0x43, 0xc2, 0xc2}
|
||||
gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare"))
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, gitRepo)
|
||||
@ -135,8 +135,8 @@ func TestHasPreviousCommit(t *testing.T) {
|
||||
commit, err := repo.GetCommit("8006ff9adbf0cb94da7dad9e537e53817f9fa5c0")
|
||||
assert.NoError(t, err)
|
||||
|
||||
parentSHA := MustIDFromString("8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2")
|
||||
notParentSHA := MustIDFromString("2839944139e0de9737a044f78b0e4b40d989a9e3")
|
||||
parentSHA := repo.objectFormat.MustIDFromString("8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2")
|
||||
notParentSHA := repo.objectFormat.MustIDFromString("2839944139e0de9737a044f78b0e4b40d989a9e3")
|
||||
|
||||
haz, err := commit.HasPreviousCommit(parentSHA)
|
||||
assert.NoError(t, err)
|
||||
|
@ -33,8 +33,8 @@ var (
|
||||
// DefaultContext is the default context to run git commands in, must be initialized by git.InitXxx
|
||||
DefaultContext context.Context
|
||||
|
||||
// SupportProcReceive version >= 2.29.0
|
||||
SupportProcReceive bool
|
||||
SupportProcReceive bool // >= 2.29
|
||||
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
|
||||
|
||||
gitVersion *version.Version
|
||||
)
|
||||
@ -189,7 +189,7 @@ func InitFull(ctx context.Context) (err error) {
|
||||
globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=")
|
||||
}
|
||||
SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
|
||||
|
||||
SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil
|
||||
if setting.LFS.StartServer {
|
||||
if CheckGitVersionAtLeast("2.1.2") != nil {
|
||||
return errors.New("LFS server support requires Git >= 2.1.2")
|
||||
|
@ -92,17 +92,21 @@ func (c *LastCommitCache) Get(ref, entryPath string) (*Commit, error) {
|
||||
|
||||
// GetCommitByPath gets the last commit for the entry in the provided commit
|
||||
func (c *LastCommitCache) GetCommitByPath(commitID, entryPath string) (*Commit, error) {
|
||||
sha1, err := NewIDFromString(commitID)
|
||||
objectFormat, err := c.repo.GetObjectFormat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sha, err := objectFormat.NewIDFromString(commitID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lastCommit, err := c.Get(sha1.String(), entryPath)
|
||||
lastCommit, err := c.Get(sha.String(), entryPath)
|
||||
if err != nil || lastCommit != nil {
|
||||
return lastCommit, err
|
||||
}
|
||||
|
||||
lastCommit, err = c.repo.getCommitByPathWithID(sha1, entryPath)
|
||||
lastCommit, err = c.repo.getCommitByPathWithID(sha, entryPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ package git
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
|
||||
)
|
||||
|
||||
@ -18,7 +19,7 @@ func (c *Commit) CacheCommit(ctx context.Context) error {
|
||||
}
|
||||
commitNodeIndex, _ := c.repo.CommitNodeIndex()
|
||||
|
||||
index, err := commitNodeIndex.Get(c.ID)
|
||||
index, err := commitNodeIndex.Get(plumbing.Hash(c.ID.RawValue()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -143,17 +143,20 @@ func (g *LogNameStatusRepoParser) Next(treepath string, paths2ids map[string]int
|
||||
}
|
||||
|
||||
// Our "line" must look like: <commitid> SP (<parent> SP) * NUL
|
||||
ret.CommitID = string(g.next[0:40])
|
||||
parents := string(g.next[41:])
|
||||
commitIds := string(g.next)
|
||||
if g.buffull {
|
||||
more, err := g.rd.ReadString('\x00')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
parents += more
|
||||
commitIds += more
|
||||
}
|
||||
commitIds = commitIds[:len(commitIds)-1]
|
||||
splitIds := strings.Split(commitIds, " ")
|
||||
ret.CommitID = splitIds[0]
|
||||
if len(splitIds) > 1 {
|
||||
ret.ParentIDs = splitIds[1:]
|
||||
}
|
||||
parents = parents[:len(parents)-1]
|
||||
ret.ParentIDs = strings.Split(parents, " ")
|
||||
|
||||
// now read the next "line"
|
||||
g.buffull = false
|
||||
|
@ -11,6 +11,7 @@ import (
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
@ -72,7 +73,7 @@ func GetNote(ctx context.Context, repo *Repository, commitID string, note *Note)
|
||||
defer commitGraphFile.Close()
|
||||
}
|
||||
|
||||
commitNode, err := commitNodeIndex.Get(notes.ID)
|
||||
commitNode, err := commitNodeIndex.Get(plumbing.Hash(notes.ID.RawValue()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
101
modules/git/object_format.go
Normal file
101
modules/git/object_format.go
Normal file
@ -0,0 +1,101 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ObjectFormatID int
|
||||
|
||||
const (
|
||||
Sha1 ObjectFormatID = iota
|
||||
)
|
||||
|
||||
// sha1Pattern can be used to determine if a string is an valid sha
|
||||
var sha1Pattern = regexp.MustCompile(`^[0-9a-f]{4,40}$`)
|
||||
|
||||
type ObjectFormat interface {
|
||||
ID() ObjectFormatID
|
||||
String() string
|
||||
|
||||
// Empty is the hash of empty git
|
||||
Empty() ObjectID
|
||||
// EmptyTree is the hash of an empty tree
|
||||
EmptyTree() ObjectID
|
||||
// FullLength is the length of the hash's hex string
|
||||
FullLength() int
|
||||
|
||||
IsValid(input string) bool
|
||||
MustID(b []byte) ObjectID
|
||||
MustIDFromString(s string) ObjectID
|
||||
NewID(b []byte) (ObjectID, error)
|
||||
NewIDFromString(s string) (ObjectID, error)
|
||||
NewEmptyID() ObjectID
|
||||
|
||||
NewHasher() HasherInterface
|
||||
}
|
||||
|
||||
type Sha1ObjectFormat struct{}
|
||||
|
||||
func (*Sha1ObjectFormat) ID() ObjectFormatID { return Sha1 }
|
||||
func (*Sha1ObjectFormat) String() string { return "sha1" }
|
||||
func (*Sha1ObjectFormat) Empty() ObjectID { return &Sha1Hash{} }
|
||||
func (*Sha1ObjectFormat) EmptyTree() ObjectID {
|
||||
return &Sha1Hash{
|
||||
0x4b, 0x82, 0x5d, 0xc6, 0x42, 0xcb, 0x6e, 0xb9, 0xa0, 0x60,
|
||||
0xe5, 0x4b, 0xf8, 0xd6, 0x92, 0x88, 0xfb, 0xee, 0x49, 0x04,
|
||||
}
|
||||
}
|
||||
func (*Sha1ObjectFormat) FullLength() int { return 40 }
|
||||
func (*Sha1ObjectFormat) IsValid(input string) bool {
|
||||
return sha1Pattern.MatchString(input)
|
||||
}
|
||||
|
||||
func (*Sha1ObjectFormat) MustID(b []byte) ObjectID {
|
||||
var id Sha1Hash
|
||||
copy(id[0:20], b)
|
||||
return &id
|
||||
}
|
||||
|
||||
func (h *Sha1ObjectFormat) MustIDFromString(s string) ObjectID {
|
||||
return MustIDFromString(h, s)
|
||||
}
|
||||
|
||||
func (h *Sha1ObjectFormat) NewID(b []byte) (ObjectID, error) {
|
||||
return IDFromRaw(h, b)
|
||||
}
|
||||
|
||||
func (h *Sha1ObjectFormat) NewIDFromString(s string) (ObjectID, error) {
|
||||
return genericIDFromString(h, s)
|
||||
}
|
||||
|
||||
func (*Sha1ObjectFormat) NewEmptyID() ObjectID {
|
||||
return NewSha1()
|
||||
}
|
||||
|
||||
func (h *Sha1ObjectFormat) NewHasher() HasherInterface {
|
||||
return &Sha1Hasher{sha1.New()}
|
||||
}
|
||||
|
||||
func ObjectFormatFromID(id ObjectFormatID) ObjectFormat {
|
||||
switch id {
|
||||
case Sha1:
|
||||
return &Sha1ObjectFormat{}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ObjectFormatFromString(hash string) (ObjectFormat, error) {
|
||||
switch strings.ToLower(hash) {
|
||||
case "sha1":
|
||||
return &Sha1ObjectFormat{}, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unknown hash type: %s", hash)
|
||||
}
|
141
modules/git/object_id.go
Normal file
141
modules/git/object_id.go
Normal file
@ -0,0 +1,141 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ObjectID interface {
|
||||
String() string
|
||||
IsZero() bool
|
||||
RawValue() []byte
|
||||
Type() ObjectFormat
|
||||
}
|
||||
|
||||
type Sha1Hash [20]byte
|
||||
|
||||
func (h *Sha1Hash) String() string {
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
func (h *Sha1Hash) IsZero() bool {
|
||||
empty := Sha1Hash{}
|
||||
return bytes.Equal(empty[:], h[:])
|
||||
}
|
||||
func (h *Sha1Hash) RawValue() []byte { return h[:] }
|
||||
func (*Sha1Hash) Type() ObjectFormat { return &Sha1ObjectFormat{} }
|
||||
|
||||
func NewSha1() *Sha1Hash {
|
||||
return &Sha1Hash{}
|
||||
}
|
||||
|
||||
// NewHash is for generic implementations
|
||||
func NewHash(hash string) (ObjectID, error) {
|
||||
hash = strings.ToLower(hash)
|
||||
switch hash {
|
||||
case "sha1":
|
||||
return &Sha1Hash{}, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("unsupported hash type")
|
||||
}
|
||||
|
||||
func IDFromRaw(h ObjectFormat, b []byte) (ObjectID, error) {
|
||||
if len(b) != h.FullLength()/2 {
|
||||
return h.Empty(), fmt.Errorf("length must be %d: %v", h.FullLength(), b)
|
||||
}
|
||||
return h.MustID(b), nil
|
||||
}
|
||||
|
||||
func MustIDFromString(h ObjectFormat, s string) ObjectID {
|
||||
b, _ := hex.DecodeString(s)
|
||||
return h.MustID(b)
|
||||
}
|
||||
|
||||
func genericIDFromString(h ObjectFormat, s string) (ObjectID, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) != h.FullLength() {
|
||||
return h.Empty(), fmt.Errorf("length must be %d: %s", h.FullLength(), s)
|
||||
}
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
return h.Empty(), err
|
||||
}
|
||||
return h.NewID(b)
|
||||
}
|
||||
|
||||
func IDFromString(hexHash string) (ObjectID, error) {
|
||||
switch len(hexHash) {
|
||||
case 40:
|
||||
hashType := Sha1ObjectFormat{}
|
||||
h, err := hashType.NewIDFromString(hexHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid hash hex string: '%s' len: %d", hexHash, len(hexHash))
|
||||
}
|
||||
|
||||
func IsEmptyCommitID(commitID string) bool {
|
||||
if commitID == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
id, err := IDFromString(commitID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return id.IsZero()
|
||||
}
|
||||
|
||||
// HasherInterface is a struct that will generate a Hash
|
||||
type HasherInterface interface {
|
||||
hash.Hash
|
||||
|
||||
HashSum() ObjectID
|
||||
}
|
||||
|
||||
type Sha1Hasher struct {
|
||||
hash.Hash
|
||||
}
|
||||
|
||||
// ComputeBlobHash compute the hash for a given blob content
|
||||
func ComputeBlobHash(hashType ObjectFormat, content []byte) ObjectID {
|
||||
return ComputeHash(hashType, ObjectBlob, content)
|
||||
}
|
||||
|
||||
// ComputeHash compute the hash for a given ObjectType and content
|
||||
func ComputeHash(hashType ObjectFormat, t ObjectType, content []byte) ObjectID {
|
||||
h := hashType.NewHasher()
|
||||
_, _ = h.Write(t.Bytes())
|
||||
_, _ = h.Write([]byte(" "))
|
||||
_, _ = h.Write([]byte(strconv.FormatInt(int64(len(content)), 10)))
|
||||
_, _ = h.Write([]byte{0})
|
||||
return h.HashSum()
|
||||
}
|
||||
|
||||
// HashSum generates a SHA1 for the provided hash
|
||||
func (h *Sha1Hasher) HashSum() ObjectID {
|
||||
var sha1 Sha1Hash
|
||||
copy(sha1[:], h.Hash.Sum(nil))
|
||||
return &sha1
|
||||
}
|
||||
|
||||
type ErrInvalidSHA struct {
|
||||
SHA string
|
||||
}
|
||||
|
||||
func (err ErrInvalidSHA) Error() string {
|
||||
return fmt.Sprintf("invalid sha: %s", err.SHA)
|
||||
}
|
28
modules/git/object_id_gogit.go
Normal file
28
modules/git/object_id_gogit.go
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
//go:build gogit
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/hash"
|
||||
)
|
||||
|
||||
func ParseGogitHash(h plumbing.Hash) ObjectID {
|
||||
switch hash.Size {
|
||||
case 20:
|
||||
return ObjectFormatFromID(Sha1).MustID(h[:])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseGogitHashArray(objectIDs []plumbing.Hash) []ObjectID {
|
||||
ret := make([]ObjectID, len(objectIDs))
|
||||
for i, h := range objectIDs {
|
||||
ret[i] = ParseGogitHash(h)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
21
modules/git/object_id_test.go
Normal file
21
modules/git/object_id_test.go
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright 2022 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package git
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIsValidSHAPattern(t *testing.T) {
|
||||
h := NewSha1().Type()
|
||||
assert.True(t, h.IsValid("fee1"))
|
||||
assert.True(t, h.IsValid("abc000"))
|
||||
assert.True(t, h.IsValid("9023902390239023902390239023902390239023"))
|
||||
assert.False(t, h.IsValid("90239023902390239023902390239023902390239023"))
|
||||
assert.False(t, h.IsValid("abc"))
|
||||
assert.False(t, h.IsValid("123g"))
|
||||
assert.False(t, h.IsValid("some random text"))
|
||||
}
|
@ -11,12 +11,14 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/go-git/go-git/v5/plumbing/hash"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
// ParseTreeEntries parses the output of a `git ls-tree -l` command.
|
||||
func ParseTreeEntries(data []byte) ([]*TreeEntry, error) {
|
||||
func ParseTreeEntries(h ObjectFormat, data []byte) ([]*TreeEntry, error) {
|
||||
return parseTreeEntries(data, nil)
|
||||
}
|
||||
|
||||
@ -50,15 +52,16 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
||||
return nil, fmt.Errorf("unknown type: %v", string(data[pos:pos+6]))
|
||||
}
|
||||
|
||||
if pos+40 > len(data) {
|
||||
// in hex format, not byte format ....
|
||||
if pos+hash.Size*2 > len(data) {
|
||||
return nil, fmt.Errorf("Invalid ls-tree output: %s", string(data))
|
||||
}
|
||||
id, err := NewIDFromString(string(data[pos : pos+40]))
|
||||
var err error
|
||||
entry.ID, err = IDFromString(string(data[pos : pos+hash.Size*2]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Invalid ls-tree output: %w", err)
|
||||
return nil, fmt.Errorf("invalid ls-tree output: %w", err)
|
||||
}
|
||||
entry.ID = id
|
||||
entry.gogitTreeEntry.Hash = id
|
||||
entry.gogitTreeEntry.Hash = plumbing.Hash(entry.ID.RawValue())
|
||||
pos += 41 // skip over sha and trailing space
|
||||
|
||||
end := pos + bytes.IndexByte(data[pos:], '\t')
|
||||
@ -77,6 +80,7 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
||||
|
||||
// In case entry name is surrounded by double quotes(it happens only in git-shell).
|
||||
if data[pos] == '"' {
|
||||
var err error
|
||||
entry.gogitTreeEntry.Name, err = strconv.Unquote(string(data[pos:end]))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Invalid ls-tree output: %w", err)
|
||||
|
@ -6,8 +6,10 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/filemode"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@ -26,9 +28,9 @@ func TestParseTreeEntries(t *testing.T) {
|
||||
Input: "100644 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c 1022\texample/file2.txt\n",
|
||||
Expected: []*TreeEntry{
|
||||
{
|
||||
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
||||
ID: ObjectFormatFromID(Sha1).MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
||||
gogitTreeEntry: &object.TreeEntry{
|
||||
Hash: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
||||
Hash: plumbing.Hash(ObjectFormatFromID(Sha1).MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()),
|
||||
Name: "example/file2.txt",
|
||||
Mode: filemode.Regular,
|
||||
},
|
||||
@ -42,9 +44,9 @@ func TestParseTreeEntries(t *testing.T) {
|
||||
"040000 tree 1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8 -\texample\n",
|
||||
Expected: []*TreeEntry{
|
||||
{
|
||||
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
||||
ID: ObjectFormatFromID(Sha1).MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
||||
gogitTreeEntry: &object.TreeEntry{
|
||||
Hash: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
||||
Hash: plumbing.Hash(ObjectFormatFromID(Sha1).MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()),
|
||||
Name: "example/\n.txt",
|
||||
Mode: filemode.Symlink,
|
||||
},
|
||||
@ -52,10 +54,10 @@ func TestParseTreeEntries(t *testing.T) {
|
||||
sized: true,
|
||||
},
|
||||
{
|
||||
ID: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
|
||||
ID: ObjectFormatFromID(Sha1).MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
|
||||
sized: true,
|
||||
gogitTreeEntry: &object.TreeEntry{
|
||||
Hash: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
|
||||
Hash: plumbing.Hash(ObjectFormatFromID(Sha1).MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8").RawValue()),
|
||||
Name: "example",
|
||||
Mode: filemode.Dir,
|
||||
},
|
||||
@ -65,8 +67,12 @@ func TestParseTreeEntries(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
entries, err := ParseTreeEntries([]byte(testCase.Input))
|
||||
entries, err := ParseTreeEntries(ObjectFormatFromID(Sha1), []byte(testCase.Input))
|
||||
assert.NoError(t, err)
|
||||
if len(entries) > 1 {
|
||||
fmt.Println(testCase.Expected[0].ID)
|
||||
fmt.Println(entries[0].ID)
|
||||
}
|
||||
assert.EqualValues(t, testCase.Expected, entries)
|
||||
}
|
||||
}
|
||||
|
@ -17,13 +17,13 @@ import (
|
||||
)
|
||||
|
||||
// ParseTreeEntries parses the output of a `git ls-tree -l` command.
|
||||
func ParseTreeEntries(data []byte) ([]*TreeEntry, error) {
|
||||
return parseTreeEntries(data, nil)
|
||||
func ParseTreeEntries(objectFormat ObjectFormat, data []byte) ([]*TreeEntry, error) {
|
||||
return parseTreeEntries(objectFormat, data, nil)
|
||||
}
|
||||
|
||||
var sepSpace = []byte{' '}
|
||||
|
||||
func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
||||
func parseTreeEntries(objectFormat ObjectFormat, data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
||||
var err error
|
||||
entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1)
|
||||
for pos := 0; pos < len(data); {
|
||||
@ -72,7 +72,7 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
||||
return nil, fmt.Errorf("unknown type: %v", string(entryMode))
|
||||
}
|
||||
|
||||
entry.ID, err = NewIDFromString(string(entryObjectID))
|
||||
entry.ID, err = objectFormat.NewIDFromString(string(entryObjectID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err)
|
||||
}
|
||||
@ -92,15 +92,15 @@ func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func catBatchParseTreeEntries(ptree *Tree, rd *bufio.Reader, sz int64) ([]*TreeEntry, error) {
|
||||
func catBatchParseTreeEntries(objectFormat ObjectFormat, ptree *Tree, rd *bufio.Reader, sz int64) ([]*TreeEntry, error) {
|
||||
fnameBuf := make([]byte, 4096)
|
||||
modeBuf := make([]byte, 40)
|
||||
shaBuf := make([]byte, 40)
|
||||
shaBuf := make([]byte, objectFormat.FullLength())
|
||||
entries := make([]*TreeEntry, 0, 10)
|
||||
|
||||
loop:
|
||||
for sz > 0 {
|
||||
mode, fname, sha, count, err := ParseTreeLine(rd, modeBuf, fnameBuf, shaBuf)
|
||||
mode, fname, sha, count, err := ParseTreeLine(objectFormat, rd, modeBuf, fnameBuf, shaBuf)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break loop
|
||||
@ -127,7 +127,7 @@ loop:
|
||||
return nil, fmt.Errorf("unknown mode: %v", string(mode))
|
||||
}
|
||||
|
||||
entry.ID = MustID(sha)
|
||||
entry.ID = objectFormat.MustID(sha)
|
||||
entry.name = string(fname)
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
|
@ -12,6 +12,8 @@ import (
|
||||
)
|
||||
|
||||
func TestParseTreeEntriesLong(t *testing.T) {
|
||||
objectFormat := ObjectFormatFromID(Sha1)
|
||||
|
||||
testCases := []struct {
|
||||
Input string
|
||||
Expected []*TreeEntry
|
||||
@ -24,28 +26,28 @@ func TestParseTreeEntriesLong(t *testing.T) {
|
||||
`,
|
||||
Expected: []*TreeEntry{
|
||||
{
|
||||
ID: MustIDFromString("ea0d83c9081af9500ac9f804101b3fd0a5c293af"),
|
||||
ID: objectFormat.MustIDFromString("ea0d83c9081af9500ac9f804101b3fd0a5c293af"),
|
||||
name: "README.md",
|
||||
entryMode: EntryModeBlob,
|
||||
size: 8218,
|
||||
sized: true,
|
||||
},
|
||||
{
|
||||
ID: MustIDFromString("037f27dc9d353ae4fd50f0474b2194c593914e35"),
|
||||
ID: objectFormat.MustIDFromString("037f27dc9d353ae4fd50f0474b2194c593914e35"),
|
||||
name: "README_ZH.md",
|
||||
entryMode: EntryModeBlob,
|
||||
size: 4681,
|
||||
sized: true,
|
||||
},
|
||||
{
|
||||
ID: MustIDFromString("9846a94f7e8350a916632929d0fda38c90dd2ca8"),
|
||||
ID: objectFormat.MustIDFromString("9846a94f7e8350a916632929d0fda38c90dd2ca8"),
|
||||
name: "SECURITY.md",
|
||||
entryMode: EntryModeBlob,
|
||||
size: 429,
|
||||
sized: true,
|
||||
},
|
||||
{
|
||||
ID: MustIDFromString("84b90550547016f73c5dd3f50dea662389e67b6d"),
|
||||
ID: objectFormat.MustIDFromString("84b90550547016f73c5dd3f50dea662389e67b6d"),
|
||||
name: "assets",
|
||||
entryMode: EntryModeTree,
|
||||
sized: true,
|
||||
@ -54,7 +56,7 @@ func TestParseTreeEntriesLong(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
entries, err := ParseTreeEntries([]byte(testCase.Input))
|
||||
entries, err := ParseTreeEntries(objectFormat, []byte(testCase.Input))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, entries, len(testCase.Expected))
|
||||
for i, entry := range entries {
|
||||
@ -64,6 +66,8 @@ func TestParseTreeEntriesLong(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseTreeEntriesShort(t *testing.T) {
|
||||
objectFormat := ObjectFormatFromID(Sha1)
|
||||
|
||||
testCases := []struct {
|
||||
Input string
|
||||
Expected []*TreeEntry
|
||||
@ -74,12 +78,12 @@ func TestParseTreeEntriesShort(t *testing.T) {
|
||||
`,
|
||||
Expected: []*TreeEntry{
|
||||
{
|
||||
ID: MustIDFromString("ea0d83c9081af9500ac9f804101b3fd0a5c293af"),
|
||||
ID: objectFormat.MustIDFromString("ea0d83c9081af9500ac9f804101b3fd0a5c293af"),
|
||||
name: "README.md",
|
||||
entryMode: EntryModeBlob,
|
||||
},
|
||||
{
|
||||
ID: MustIDFromString("84b90550547016f73c5dd3f50dea662389e67b6d"),
|
||||
ID: objectFormat.MustIDFromString("84b90550547016f73c5dd3f50dea662389e67b6d"),
|
||||
name: "assets",
|
||||
entryMode: EntryModeTree,
|
||||
},
|
||||
@ -87,7 +91,7 @@ func TestParseTreeEntriesShort(t *testing.T) {
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
entries, err := ParseTreeEntries([]byte(testCase.Input))
|
||||
entries, err := ParseTreeEntries(objectFormat, []byte(testCase.Input))
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, entries, len(testCase.Expected))
|
||||
for i, entry := range entries {
|
||||
@ -98,7 +102,7 @@ func TestParseTreeEntriesShort(t *testing.T) {
|
||||
|
||||
func TestParseTreeEntriesInvalid(t *testing.T) {
|
||||
// there was a panic: "runtime error: slice bounds out of range" when the input was invalid: #20315
|
||||
entries, err := ParseTreeEntries([]byte("100644 blob ea0d83c9081af9500ac9f804101b3fd0a5c293af"))
|
||||
entries, err := ParseTreeEntries(ObjectFormatFromID(Sha1), []byte("100644 blob ea0d83c9081af9500ac9f804101b3fd0a5c293af"))
|
||||
assert.Error(t, err)
|
||||
assert.Len(t, entries, 0)
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
@ -26,7 +27,7 @@ type LFSResult struct {
|
||||
SHA string
|
||||
Summary string
|
||||
When time.Time
|
||||
ParentHashes []git.SHA1
|
||||
ParentHashes []git.ObjectID
|
||||
BranchName string
|
||||
FullCommitName string
|
||||
}
|
||||
@ -38,7 +39,7 @@ func (a lfsResultSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a lfsResultSlice) Less(i, j int) bool { return a[j].When.After(a[i].When) }
|
||||
|
||||
// FindLFSFile finds commits that contain a provided pointer file hash
|
||||
func FindLFSFile(repo *git.Repository, hash git.SHA1) ([]*LFSResult, error) {
|
||||
func FindLFSFile(repo *git.Repository, objectID git.ObjectID) ([]*LFSResult, error) {
|
||||
resultsMap := map[string]*LFSResult{}
|
||||
results := make([]*LFSResult, 0)
|
||||
|
||||
@ -65,13 +66,18 @@ func FindLFSFile(repo *git.Repository, hash git.SHA1) ([]*LFSResult, error) {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if entry.Hash == hash {
|
||||
if entry.Hash == plumbing.Hash(objectID.RawValue()) {
|
||||
parents := make([]git.ObjectID, len(gitCommit.ParentHashes))
|
||||
for i, parentCommitID := range gitCommit.ParentHashes {
|
||||
parents[i] = git.ParseGogitHash(parentCommitID)
|
||||
}
|
||||
|
||||
result := LFSResult{
|
||||
Name: name,
|
||||
SHA: gitCommit.Hash.String(),
|
||||
Summary: strings.Split(strings.TrimSpace(gitCommit.Message), "\n")[0],
|
||||
When: gitCommit.Author.When,
|
||||
ParentHashes: gitCommit.ParentHashes,
|
||||
ParentHashes: parents,
|
||||
}
|
||||
resultsMap[gitCommit.Hash.String()+":"+name] = &result
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user