mirror of
https://github.com/go-gitea/gitea
synced 2025-11-06 22:38:12 +00:00
Merge branch 'main' into allow-force-push-protected-branches
This commit is contained in:
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
|
- name: Get cleaned branch name
|
||||||
id: clean_name
|
id: clean_name
|
||||||
run: |
|
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 "Cleaned name is ${REF_NAME}"
|
||||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
- name: configure aws
|
- name: configure aws
|
||||||
@@ -56,6 +56,10 @@ jobs:
|
|||||||
- name: upload binaries to s3
|
- name: upload binaries to s3
|
||||||
run: |
|
run: |
|
||||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
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
|
- name: create github release
|
||||||
run: |
|
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 }} --draft --notes-from-tag dist/release/*
|
||||||
@@ -74,6 +78,8 @@ jobs:
|
|||||||
id: meta
|
id: meta
|
||||||
with:
|
with:
|
||||||
images: gitea/gitea
|
images: gitea/gitea
|
||||||
|
flavor: |
|
||||||
|
latest=false
|
||||||
# 1.2.3-rc0
|
# 1.2.3-rc0
|
||||||
tags: |
|
tags: |
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
@@ -105,6 +111,7 @@ jobs:
|
|||||||
images: gitea/gitea
|
images: gitea/gitea
|
||||||
# each tag below will have the suffix of -rootless
|
# each tag below will have the suffix of -rootless
|
||||||
flavor: |
|
flavor: |
|
||||||
|
latest=false
|
||||||
suffix=-rootless
|
suffix=-rootless
|
||||||
# 1.2.3-rc0
|
# 1.2.3-rc0
|
||||||
tags: |
|
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
|
- name: Get cleaned branch name
|
||||||
id: clean_name
|
id: clean_name
|
||||||
run: |
|
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 "Cleaned name is ${REF_NAME}"
|
||||||
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
echo "branch=${REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||||
- name: configure aws
|
- name: configure aws
|
||||||
@@ -58,9 +58,13 @@ jobs:
|
|||||||
- name: upload binaries to s3
|
- name: upload binaries to s3
|
||||||
run: |
|
run: |
|
||||||
aws s3 sync dist/release s3://${{ secrets.AWS_S3_BUCKET }}/gitea/${{ steps.clean_name.outputs.branch }} --no-progress
|
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
|
- name: create github release
|
||||||
run: |
|
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:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
docker-rootful:
|
docker-rootful:
|
||||||
@@ -82,7 +86,6 @@ jobs:
|
|||||||
# 1.2
|
# 1.2
|
||||||
# 1.2.3
|
# 1.2.3
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest
|
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
@@ -114,14 +117,13 @@ jobs:
|
|||||||
images: gitea/gitea
|
images: gitea/gitea
|
||||||
# each tag below will have the suffix of -rootless
|
# each tag below will have the suffix of -rootless
|
||||||
flavor: |
|
flavor: |
|
||||||
suffix=-rootless
|
suffix=-rootless,onlatest=true
|
||||||
# this will generate tags in the following format (with -rootless suffix added):
|
# this will generate tags in the following format (with -rootless suffix added):
|
||||||
# latest
|
# latest
|
||||||
# 1
|
# 1
|
||||||
# 1.2
|
# 1.2
|
||||||
# 1.2.3
|
# 1.2.3
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=latest
|
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/models/migrations"
|
"code.gitea.io/gitea/models/migrations"
|
||||||
migrate_base "code.gitea.io/gitea/models/migrations/base"
|
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/doctor"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
@@ -22,6 +23,19 @@ import (
|
|||||||
"xorm.io/xorm"
|
"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{
|
var cmdDoctorCheck = &cli.Command{
|
||||||
Name: "check",
|
Name: "check",
|
||||||
Usage: "Diagnose and optionally fix problems",
|
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{
|
var cmdRecreateTable = &cli.Command{
|
||||||
Name: "recreate-table",
|
Name: "recreate-table",
|
||||||
Usage: "Recreate tables from XORM definitions and copy the data.",
|
Usage: "Recreate tables from XORM definitions and copy the data.",
|
||||||
@@ -177,6 +178,7 @@ func runDoctorCheck(ctx *cli.Context) error {
|
|||||||
if ctx.IsSet("list") {
|
if ctx.IsSet("list") {
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, '\t', 0)
|
||||||
_, _ = w.Write([]byte("Default\tName\tTitle\n"))
|
_, _ = w.Write([]byte("Default\tName\tTitle\n"))
|
||||||
|
doctor.SortChecks(doctor.Checks)
|
||||||
for _, check := range doctor.Checks {
|
for _, check := range doctor.Checks {
|
||||||
if check.IsDefault {
|
if check.IsDefault {
|
||||||
_, _ = w.Write([]byte{'*'})
|
_, _ = w.Write([]byte{'*'})
|
||||||
@@ -192,25 +194,19 @@ func runDoctorCheck(ctx *cli.Context) error {
|
|||||||
|
|
||||||
var checks []*doctor.Check
|
var checks []*doctor.Check
|
||||||
if ctx.Bool("all") {
|
if ctx.Bool("all") {
|
||||||
checks = doctor.Checks
|
checks = make([]*doctor.Check, len(doctor.Checks))
|
||||||
|
copy(checks, doctor.Checks)
|
||||||
} else if ctx.IsSet("run") {
|
} else if ctx.IsSet("run") {
|
||||||
addDefault := ctx.Bool("default")
|
addDefault := ctx.Bool("default")
|
||||||
names := ctx.StringSlice("run")
|
runNamesSet := container.SetOf(ctx.StringSlice("run")...)
|
||||||
for i, name := range names {
|
|
||||||
names[i] = strings.ToLower(strings.TrimSpace(name))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, check := range doctor.Checks {
|
for _, check := range doctor.Checks {
|
||||||
if addDefault && check.IsDefault {
|
if (addDefault && check.IsDefault) || runNamesSet.Contains(check.Name) {
|
||||||
checks = append(checks, check)
|
checks = append(checks, check)
|
||||||
continue
|
runNamesSet.Remove(check.Name)
|
||||||
}
|
|
||||||
for _, name := range names {
|
|
||||||
if name == check.Name {
|
|
||||||
checks = append(checks, check)
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(runNamesSet) > 0 {
|
||||||
|
return fmt.Errorf("unknown checks: %q", strings.Join(runNamesSet.Values(), ","))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, check := range doctor.Checks {
|
for _, check := range doctor.Checks {
|
||||||
@@ -219,6 +215,5 @@ func runDoctorCheck(ctx *cli.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return doctor.RunChecks(stdCtx, colorize, ctx.Bool("fix"), checks)
|
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])
|
oldCommitIDs[count] = string(fields[0])
|
||||||
newCommitIDs[count] = string(fields[1])
|
newCommitIDs[count] = string(fields[1])
|
||||||
refFullNames[count] = git.RefName(fields[2])
|
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
|
masterPushed = true
|
||||||
}
|
}
|
||||||
count++
|
count++
|
||||||
@@ -669,7 +671,8 @@ Gitea or set your environment appropriately.`, "")
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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))
|
err = writeDataPktLine(ctx, os.Stdout, []byte("option old-oid "+rs.OldOID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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.
|
;; 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.
|
;; This cache will store the successfully hashed tokens in a LRU cache as a balance between performance and security.
|
||||||
;SUCCESSFUL_TOKENS_CACHE_SIZE = 20
|
;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
|
## 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.
|
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" 相关的引用,以匹配你的情况。
|
现在,你需要编辑该页面以满足你的需求。特别是,你必须更改电子邮件地址、网址以及与 "Your Gitea Instance" 相关的引用,以匹配你的情况。
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ Gitea 引用 `custom` 目录中的自定义配置文件来覆盖配置、模板
|
|||||||
|
|
||||||
将自定义的公共文件(比如页面和图片)作为 webroot 放在 `custom/public/` 中来让 Gitea 提供这些自定义内容(符号链接将被追踪)。
|
将自定义的公共文件(比如页面和图片)作为 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 页面
|
## 自定义 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.
|
- 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.
|
- 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.
|
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.
|
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 邮件配置。
|
要发送测试邮件以验证设置,请转到 Gitea > 站点管理 > 配置 > SMTP 邮件配置。
|
||||||
|
|
||||||
有关所有选项的完整列表,请查看[配置速查表](doc/administration/config-cheat-sheet.zh-cn.md)。
|
有关所有选项的完整列表,请查看[配置速查表](administration/config-cheat-sheet.md)。
|
||||||
|
|
||||||
请注意:只有在使用 TLS 或 `HOST=localhost` 加密 SMTP 服务器通信时才支持身份验证。TLS 加密可以通过以下方式进行:
|
请注意:只有在使用 TLS 或 `HOST=localhost` 加密 SMTP 服务器通信时才支持身份验证。TLS 加密可以通过以下方式进行:
|
||||||
|
|
||||||
- 通过端口 587 的 STARTTLS(也称为 Opportunistic TLS)。初始连接是明文的,但如果服务器支持,则可以升级为 TLS。
|
- 通过端口 587 的 STARTTLS(也称为 Opportunistic TLS)。初始连接是明文的,但如果服务器支持,则可以升级为 TLS。
|
||||||
- 通过默认端口 465 的 SMTPS 连接。连接到服务器从一开始就使用 TLS。
|
- 通过默认端口 465 的 SMTPS 连接。连接到服务器从一开始就使用 TLS。
|
||||||
- 使用 `IS_TLS_ENABLED=true` 进行强制的 SMTPS 连接。(这两种方式都被称为 Implicit TLS)
|
- 使用 `PROTOCOL=smtps` 进行强制的 SMTPS 连接。(这两种方式都被称为 Implicit TLS)
|
||||||
这是由于 Go 内部库对 STRIPTLS 攻击的保护机制。
|
这是由于 Go 内部库对 STRIPTLS 攻击的保护机制。
|
||||||
|
|
||||||
请注意,自2018年起,[RFC8314](https://tools.ietf.org/html/rfc8314#section-3) 推荐使用 Implicit TLS。
|
请注意,自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
|
```html
|
||||||
<link rel="stylesheet" href="{{AppSubUrl}}/assets/css/my-style-XXXXX.css" />
|
<link rel="stylesheet" href="{{AppSubUrl}}/assets/css/my-style-XXXXX.css" />
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ CERT_FILE = cert.pem
|
|||||||
KEY_FILE = key.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”环境变量。它也可以是绝对路径。
|
对于“CERT_FILE”或“KEY_FILE”字段,当文件路径是相对路径时,文件路径相对于“GITEA_CUSTOM”环境变量。它也可以是绝对路径。
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,7 @@ menu:
|
|||||||
|
|
||||||
## Enabling/configuring API access
|
## Enabling/configuring API access
|
||||||
|
|
||||||
By default, `ENABLE_SWAGGER` is true, and
|
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.
|
||||||
`MAX_RESPONSE_ITEMS` is set to 50. See [Config Cheat
|
|
||||||
Sheet](administration/config-cheat-sheet.md) for more
|
|
||||||
information.
|
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,7 @@ menu:
|
|||||||
|
|
||||||
## 开启/配置 API 访问
|
## 开启/配置 API 访问
|
||||||
|
|
||||||
通常情况下, `ENABLE_SWAGGER` 默认开启并且参数 `MAX_RESPONSE_ITEMS` 默认为 50。您可以从 [Config Cheat
|
通常情况下, `ENABLE_SWAGGER` 默认开启并且参数 `MAX_RESPONSE_ITEMS` 默认为 50。您可以从 [Config Cheat Sheet](administration/config-cheat-sheet.md) 中获取更多配置相关信息。
|
||||||
Sheet](administration/config-cheat-sheet.md) 中获取更多配置相关信息。
|
|
||||||
|
|
||||||
## 通过 API 认证
|
## 通过 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:
|
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)
|
- [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.
|
To migrate from GitHub to Gitea, you can use Gitea's built-in migration form.
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ menu:
|
|||||||
|
|
||||||
要从Gogs迁移到Gitea:
|
要从Gogs迁移到Gitea:
|
||||||
|
|
||||||
- [Gogs版本0.9.146或更低](installation/upgrade-from-gogs.md)
|
|
||||||
- [Gogs版本0.11.46.0418](https://github.com/go-gitea/gitea/issues/4286)
|
- [Gogs版本0.11.46.0418](https://github.com/go-gitea/gitea/issues/4286)
|
||||||
|
|
||||||
要从GitHub迁移到Gitea,您可以使用Gitea内置的迁移表单。
|
要从GitHub迁移到Gitea,您可以使用Gitea内置的迁移表单。
|
||||||
@@ -190,7 +189,7 @@ Gitea 目前支持三个官方主题,分别是 `gitea-light`、`gitea-dark`
|
|||||||
|
|
||||||
假设我们的主题是 `arc-blue`(这是一个真实的主题,可以在[此问题](https://github.com/go-gitea/gitea/issues/6011)中找到)
|
假设我们的主题是 `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`列表中,允许用户使用该主题
|
通过将`arc-blue`添加到`app.ini`中的`THEMES`列表中,允许用户使用该主题
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ chmod 770 /etc/gitea
|
|||||||
- 使用 `gitea generate secret` 创建 `SECRET_KEY` 和 `INTERNAL_TOKEN`
|
- 使用 `gitea generate secret` 创建 `SECRET_KEY` 和 `INTERNAL_TOKEN`
|
||||||
- 提供所有必要的密钥
|
- 提供所有必要的密钥
|
||||||
|
|
||||||
详情参考 [命令行文档](/zh-cn/command-line/) 中有关 `gitea generate secret` 的内容。
|
详情参考 [命令行文档](administration/command-line.md) 中有关 `gitea generate secret` 的内容。
|
||||||
|
|
||||||
### 配置 Gitea 工作路径
|
### 配置 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.` 来重新编写授权密钥文件。
|
如果您没有使用 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)
|
如果从本页中没有找到你需要的内容,请访问 [帮助页面](help/support.md)
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ git checkout v@version@ # or git checkout pr-xyz
|
|||||||
|
|
||||||
- `go` @minGoVersion@ 或更高版本,请参阅 [这里](https://golang.org/dl/)
|
- `go` @minGoVersion@ 或更高版本,请参阅 [这里](https://golang.org/dl/)
|
||||||
- `node` @minNodeVersion@ 或更高版本,并且安装 `npm`, 请参阅 [这里](https://nodejs.org/zh-cn/download/)
|
- `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)。
|
为了尽可能简化编译过程,提供了各种 [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`.
|
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
|
gitea --config /etc/gitea/app.ini actions generate-runner-token
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ Runner级别决定了从哪里获取注册令牌。
|
|||||||
|
|
||||||
注册令牌的格式是一个随机字符串 `D0gvfu2iHfUjNqCYVljVyRV14fISpJxxxxxxxxxx`。
|
注册令牌的格式是一个随机字符串 `D0gvfu2iHfUjNqCYVljVyRV14fISpJxxxxxxxxxx`。
|
||||||
|
|
||||||
注册令牌也可以通过 Gitea 的 [命令行](../../administration/command-line.en-us.md#actions-generate-runner-token) 获得:
|
注册令牌也可以通过 Gitea 的 [命令行](administration/command-line.md#actions-generate-runner-token) 获得:
|
||||||
|
|
||||||
### 注册Runner
|
### 注册Runner
|
||||||
|
|
||||||
|
|||||||
@@ -145,25 +145,25 @@ Adds the following fields:
|
|||||||
|
|
||||||
Uses the following fields:
|
Uses the following fields:
|
||||||
|
|
||||||
- Group Search Base (optional)
|
- Group Search Base DN (optional)
|
||||||
|
|
||||||
- The LDAP DN used for groups.
|
- The LDAP DN used for groups.
|
||||||
- Example: `ou=group,dc=mydomain,dc=com`
|
- 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.
|
- User Attribute Listed in Group (optional)
|
||||||
- Example: `(|(cn=gitea_users)(cn=admins))`
|
|
||||||
|
|
||||||
- User Attribute in Group (optional)
|
|
||||||
|
|
||||||
- The user attribute that is used to reference a user in the group object.
|
- 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: `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`.
|
- Example: `dn` if the group object contains a `member: uid=bender,ou=users,dc=planetexpress,dc=com`.
|
||||||
|
|
||||||
- Group Attribute for User (optional)
|
- Verify group membership in LDAP (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))`
|
||||||
|
|
||||||
## PAM (Pluggable Authentication Module)
|
## PAM (Pluggable Authentication Module)
|
||||||
|
|
||||||
@@ -198,7 +198,7 @@ administrative user.
|
|||||||
field is set to `mail.com`, then Gitea will expect the `user email` field
|
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]
|
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
|
and the official binaries provided do not have this enabled. PAM requires that
|
||||||
the necessary libpam dynamic library be available and the necessary PAM
|
the necessary libpam dynamic library be available and the necessary PAM
|
||||||
development headers be accessible to the compiler.
|
development headers be accessible to the compiler.
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ PAM提供了一种机制,通过对用户进行PAM认证来自动将其添加
|
|||||||
- PAM电子邮件域:用户认证时要附加的电子邮件后缀。例如,如果登录系统期望一个名为gituse的用户,
|
- PAM电子邮件域:用户认证时要附加的电子邮件后缀。例如,如果登录系统期望一个名为gituse的用户,
|
||||||
并且将此字段设置为mail.com,那么Gitea在验证一个GIT实例的用户时将期望user emai字段为gituser@mail.com[^2]。
|
并且将此字段设置为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开发头文件。
|
而官方提供的二进制文件通常不会默认启用此功能。PAM需要确保系统上有必要的libpam动态库,并且编译器可以访问必要的PAM开发头文件。
|
||||||
|
|
||||||
[^1]: 例如,在Debian "Bullseye"上使用标准Linux登录,可以使用`common-session-noninteractive`。这个值对于其他版本的Debian,
|
[^1]: 例如,在Debian "Bullseye"上使用标准Linux登录,可以使用`common-session-noninteractive`。这个值对于其他版本的Debian,
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ func CreateTaskForRunner(ctx context.Context, runner *ActionRunner) (*ActionTask
|
|||||||
}
|
}
|
||||||
|
|
||||||
var jobs []*ActionRunJob
|
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
|
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{})
|
return db.GetEngine(ctx).Where("owner_id=? AND primary_key_id=''", userID).Count(&GPGKey{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetGPGKeyByID returns public key by given ID.
|
func GetGPGKeyForUserByID(ctx context.Context, ownerID, keyID int64) (*GPGKey, error) {
|
||||||
func GetGPGKeyByID(ctx context.Context, keyID int64) (*GPGKey, error) {
|
|
||||||
key := new(GPGKey)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
@@ -225,7 +224,7 @@ func deleteGPGKey(ctx context.Context, keyID string) (int64, error) {
|
|||||||
|
|
||||||
// DeleteGPGKey deletes GPG key information in database.
|
// DeleteGPGKey deletes GPG key information in database.
|
||||||
func DeleteGPGKey(ctx context.Context, doer *user_model.User, id int64) (err error) {
|
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 err != nil {
|
||||||
if IsErrGPGKeyNotExist(err) {
|
if IsErrGPGKeyNotExist(err) {
|
||||||
return nil
|
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)
|
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)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -131,24 +131,22 @@ func AddDeployKey(ctx context.Context, repoID int64, name, content string, readO
|
|||||||
}
|
}
|
||||||
defer committer.Close()
|
defer committer.Close()
|
||||||
|
|
||||||
pkey := &PublicKey{
|
pkey, exist, err := db.Get[PublicKey](ctx, builder.Eq{"fingerprint": fingerprint})
|
||||||
Fingerprint: fingerprint,
|
|
||||||
}
|
|
||||||
has, err := db.GetByBean(ctx, pkey)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
} else if exist {
|
||||||
|
|
||||||
if has {
|
|
||||||
if pkey.Type != KeyTypeDeploy {
|
if pkey.Type != KeyTypeDeploy {
|
||||||
return nil, ErrKeyAlreadyExist{0, fingerprint, ""}
|
return nil, ErrKeyAlreadyExist{0, fingerprint, ""}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// First time use this deploy key.
|
// First time use this deploy key.
|
||||||
pkey.Mode = accessMode
|
pkey = &PublicKey{
|
||||||
pkey.Type = KeyTypeDeploy
|
Fingerprint: fingerprint,
|
||||||
pkey.Content = content
|
Mode: accessMode,
|
||||||
pkey.Name = name
|
Type: KeyTypeDeploy,
|
||||||
|
Content: content,
|
||||||
|
Name: name,
|
||||||
|
}
|
||||||
if err = addKey(ctx, pkey); err != nil {
|
if err = addKey(ctx, pkey); err != nil {
|
||||||
return nil, fmt.Errorf("addKey: %w", err)
|
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.
|
// GetDeployKeyByID returns deploy key by given ID.
|
||||||
func GetDeployKeyByID(ctx context.Context, id int64) (*DeployKey, error) {
|
func GetDeployKeyByID(ctx context.Context, id int64) (*DeployKey, error) {
|
||||||
key := new(DeployKey)
|
key, exist, err := db.GetByID[DeployKey](ctx, id)
|
||||||
has, err := db.GetEngine(ctx).ID(id).Get(key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !exist {
|
||||||
return nil, ErrDeployKeyNotExist{id, 0, 0}
|
return nil, ErrDeployKeyNotExist{id, 0, 0}
|
||||||
}
|
}
|
||||||
return key, nil
|
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.
|
// GetDeployKeyByRepo returns deploy key by given public key ID and repository ID.
|
||||||
func GetDeployKeyByRepo(ctx context.Context, keyID, repoID int64) (*DeployKey, error) {
|
func GetDeployKeyByRepo(ctx context.Context, keyID, repoID int64) (*DeployKey, error) {
|
||||||
key := &DeployKey{
|
key, exist, err := db.Get[DeployKey](ctx, builder.Eq{"key_id": keyID, "repo_id": repoID})
|
||||||
KeyID: keyID,
|
|
||||||
RepoID: repoID,
|
|
||||||
}
|
|
||||||
has, err := db.GetByBean(ctx, key)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !exist {
|
||||||
return nil, ErrDeployKeyNotExist{0, keyID, repoID}
|
return nil, ErrDeployKeyNotExist{0, keyID, repoID}
|
||||||
}
|
}
|
||||||
return key, nil
|
return key, nil
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"golang.org/x/crypto/ssh"
|
"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,
|
// 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.
|
// it is OK to use same key as deploy key for multiple repositories/users.
|
||||||
func checkKeyFingerprint(ctx context.Context, fingerprint string) error {
|
func checkKeyFingerprint(ctx context.Context, fingerprint string) error {
|
||||||
has, err := db.GetByBean(ctx, &PublicKey{
|
has, err := db.Exist[PublicKey](ctx, builder.Eq{"fingerprint": fingerprint})
|
||||||
Fingerprint: fingerprint,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has {
|
} else if has {
|
||||||
|
|||||||
@@ -30,12 +30,17 @@ func VerifySSHKey(ctx context.Context, ownerID int64, fingerprint, token, signat
|
|||||||
return "", ErrKeyNotExist{}
|
return "", ErrKeyNotExist{}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := sshsig.Verify(bytes.NewBuffer([]byte(token)), []byte(signature), []byte(key.Content), "gitea"); err != nil {
|
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)
|
log.Error("Unable to validate token signature. Error: %v", err)
|
||||||
return "", ErrSSHInvalidTokenSignature{
|
return "", ErrSSHInvalidTokenSignature{
|
||||||
Fingerprint: key.Fingerprint,
|
Fingerprint: key.Fingerprint,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
key.Verified = true
|
key.Verified = true
|
||||||
if _, err := db.GetEngine(ctx).ID(key.ID).Cols("verified").Update(key); err != nil {
|
if _, err := db.GetEngine(ctx).ID(key.ID).Cols("verified").Update(key); err != nil {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Session represents a session compatible for go-chi session
|
// 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
|
// ReadSession reads the data for the provided session
|
||||||
func ReadSession(ctx context.Context, key string) (*Session, error) {
|
func ReadSession(ctx context.Context, key string) (*Session, error) {
|
||||||
session := Session{
|
|
||||||
Key: key,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer committer.Close()
|
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
|
return nil, err
|
||||||
} else if !has {
|
} else if !exist {
|
||||||
session.Expiry = timeutil.TimeStampNow()
|
session.Expiry = timeutil.TimeStampNow()
|
||||||
if err := db.Insert(ctx, &session); err != nil {
|
if err := db.Insert(ctx, &session); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &session, committer.Commit()
|
return session, committer.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExistSession checks if a session exists
|
// ExistSession checks if a session exists
|
||||||
func ExistSession(ctx context.Context, key string) (bool, error) {
|
func ExistSession(ctx context.Context, key string) (bool, error) {
|
||||||
session := Session{
|
return db.Exist[Session](ctx, builder.Eq{"key": key})
|
||||||
Key: key,
|
|
||||||
}
|
|
||||||
return db.GetEngine(ctx).Get(&session)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DestroySession destroys a session
|
// DestroySession destroys a session
|
||||||
@@ -79,17 +75,13 @@ func RegenerateSession(ctx context.Context, oldKey, newKey string) (*Session, er
|
|||||||
}
|
}
|
||||||
defer committer.Close()
|
defer committer.Close()
|
||||||
|
|
||||||
if has, err := db.GetByBean(ctx, &Session{
|
if has, err := db.Exist[Session](ctx, builder.Eq{"key": newKey}); err != nil {
|
||||||
Key: newKey,
|
|
||||||
}); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if has {
|
} else if has {
|
||||||
return nil, fmt.Errorf("session Key: %s already exists", newKey)
|
return nil, fmt.Errorf("session Key: %s already exists", newKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if has, err := db.GetByBean(ctx, &Session{
|
if has, err := db.Exist[Session](ctx, builder.Eq{"key": oldKey}); err != nil {
|
||||||
Key: oldKey,
|
|
||||||
}); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
if err := db.Insert(ctx, &Session{
|
if err := db.Insert(ctx, &Session{
|
||||||
@@ -104,14 +96,13 @@ func RegenerateSession(ctx context.Context, oldKey, newKey string) (*Session, er
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s := Session{
|
s, _, err := db.Get[Session](ctx, builder.Eq{"key": newKey})
|
||||||
Key: newKey,
|
if err != nil {
|
||||||
}
|
// is not exist, it should be impossible
|
||||||
if _, err := db.GetByBean(ctx, &s); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &s, committer.Commit()
|
return s, committer.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountSessions returns the number of sessions
|
// CountSessions returns the number of sessions
|
||||||
|
|||||||
@@ -265,10 +265,10 @@ func IsSSPIEnabled(ctx context.Context) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
exist, err := db.Exists[Source](ctx, FindSourcesOptions{
|
exist, err := db.Exist[Source](ctx, FindSourcesOptions{
|
||||||
IsActive: util.OptionalBoolTrue,
|
IsActive: util.OptionalBoolTrue,
|
||||||
LoginType: SSPI,
|
LoginType: SSPI,
|
||||||
})
|
}.ToConds())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Active SSPI Sources: %v", err)
|
log.Error("Active SSPI Sources: %v", err)
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -173,9 +173,44 @@ func Exec(ctx context.Context, sqlAndArgs ...any) (sql.Result, error) {
|
|||||||
return GetEngine(ctx).Exec(sqlAndArgs...)
|
return GetEngine(ctx).Exec(sqlAndArgs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetByBean filled empty fields of the bean according non-empty fields to query in database.
|
func Get[T any](ctx context.Context, cond builder.Cond) (object *T, exist bool, err error) {
|
||||||
func GetByBean(ctx context.Context, bean any) (bool, error) {
|
if !cond.IsValid() {
|
||||||
return GetEngine(ctx).Get(bean)
|
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.
|
// 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
|
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 {
|
func (err ErrNotExist) Unwrap() error {
|
||||||
return util.ErrNotExist
|
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)
|
assert.EqualValues(t, cnt, repoUnitCnt)
|
||||||
|
|
||||||
err = db.Iterate(db.DefaultContext, nil, func(ctx context.Context, repoUnit *repo_model.RepoUnit) error {
|
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.ExistByID[repo_model.RepoUnit](ctx, repoUnit.ID)
|
||||||
has, err := db.GetByBean(ctx, &reopUnit2)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !has {
|
}
|
||||||
|
if !has {
|
||||||
return db.ErrNotExist{Resource: "repo_unit", ID: repoUnit.ID}
|
return db.ErrNotExist{Resource: "repo_unit", ID: repoUnit.ID}
|
||||||
}
|
}
|
||||||
assert.EqualValues(t, repoUnit.RepoID, repoUnit.RepoID)
|
assert.EqualValues(t, repoUnit.RepoID, repoUnit.RepoID)
|
||||||
|
|||||||
@@ -66,3 +66,12 @@
|
|||||||
tree_path: "README.md"
|
tree_path: "README.md"
|
||||||
created_unix: 946684812
|
created_unix: 946684812
|
||||||
invalidated: true
|
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
|
priority: 0
|
||||||
is_closed: true
|
is_closed: true
|
||||||
is_pull: false
|
is_pull: false
|
||||||
num_comments: 0
|
num_comments: 1
|
||||||
created_unix: 946684830
|
created_unix: 946684830
|
||||||
updated_unix: 978307200
|
updated_unix: 978307200
|
||||||
is_locked: false
|
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
|
// UpdateBranch updates the branch information in the database.
|
||||||
// If it doest not exist, insert a new record into database
|
func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) (int64, error) {
|
||||||
func UpdateBranch(ctx context.Context, repoID, pusherID int64, branchName string, commit *git.Commit) error {
|
return db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, branchName).
|
||||||
cnt, err := db.GetEngine(ctx).Where("repo_id=? AND name=?", repoID, branchName).
|
|
||||||
Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted, updated_unix").
|
Cols("commit_id, commit_message, pusher_id, commit_time, is_deleted, updated_unix").
|
||||||
Update(&Branch{
|
Update(&Branch{
|
||||||
CommitID: commit.ID.String(),
|
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()),
|
CommitTime: timeutil.TimeStamp(commit.Committer.When.Unix()),
|
||||||
IsDeleted: false,
|
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
|
// 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)
|
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
|
// 1. update branch in database
|
||||||
if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{
|
if n, err := sess.Where("repo_id=? AND name=?", repo.ID, from).Update(&Branch{
|
||||||
Name: to,
|
Name: to,
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
"xorm.io/xorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type BranchList []*Branch
|
type BranchList []*Branch
|
||||||
@@ -73,7 +72,7 @@ type FindBranchOptions struct {
|
|||||||
Keyword string
|
Keyword string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts *FindBranchOptions) Cond() builder.Cond {
|
func (opts FindBranchOptions) ToConds() builder.Cond {
|
||||||
cond := builder.NewCond()
|
cond := builder.NewCond()
|
||||||
if opts.RepoID > 0 {
|
if opts.RepoID > 0 {
|
||||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||||
@@ -91,41 +90,30 @@ func (opts *FindBranchOptions) Cond() builder.Cond {
|
|||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
func CountBranches(ctx context.Context, opts FindBranchOptions) (int64, error) {
|
func (opts FindBranchOptions) ToOrders() string {
|
||||||
return db.GetEngine(ctx).Where(opts.Cond()).Count(&Branch{})
|
orderBy := opts.OrderBy
|
||||||
}
|
|
||||||
|
|
||||||
func orderByBranches(sess *xorm.Session, opts FindBranchOptions) *xorm.Session {
|
|
||||||
if !opts.IsDeletedBranch.IsFalse() { // if deleted branch included, put them at the end
|
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
|
// 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) {
|
return orderBy
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindBranchNames(ctx context.Context, opts FindBranchOptions) ([]string, error) {
|
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() {
|
if opts.PageSize > 0 && !opts.IsListAll() {
|
||||||
sess = db.SetSessionPagination(sess, &opts.ListOptions)
|
sess = db.SetSessionPagination(sess, &opts.ListOptions)
|
||||||
}
|
}
|
||||||
sess = orderByBranches(sess, opts)
|
|
||||||
var branches []string
|
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 nil, err
|
||||||
}
|
}
|
||||||
return branches, nil
|
return branches, nil
|
||||||
|
|||||||
@@ -30,14 +30,14 @@ func TestAddDeletedBranch(t *testing.T) {
|
|||||||
assert.True(t, secondBranch.IsDeleted)
|
assert.True(t, secondBranch.IsDeleted)
|
||||||
|
|
||||||
commit := &git.Commit{
|
commit := &git.Commit{
|
||||||
ID: git.MustIDFromString(secondBranch.CommitID),
|
ID: repo.ObjectFormat.MustIDFromString(secondBranch.CommitID),
|
||||||
CommitMessage: secondBranch.CommitMessage,
|
CommitMessage: secondBranch.CommitMessage,
|
||||||
Committer: &git.Signature{
|
Committer: &git.Signature{
|
||||||
When: secondBranch.CommitTime.AsLocalTime(),
|
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)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,10 +45,8 @@ func TestGetDeletedBranches(t *testing.T) {
|
|||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
|
|
||||||
branches, err := git_model.FindBranches(db.DefaultContext, git_model.FindBranchOptions{
|
branches, err := db.Find[git_model.Branch](db.DefaultContext, git_model.FindBranchOptions{
|
||||||
ListOptions: db.ListOptions{
|
ListOptions: db.ListOptionsAll,
|
||||||
ListAll: true,
|
|
||||||
},
|
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
IsDeletedBranch: util.OptionalBoolTrue,
|
IsDeletedBranch: util.OptionalBoolTrue,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -114,7 +114,8 @@ WHEN NOT MATCHED
|
|||||||
|
|
||||||
// GetNextCommitStatusIndex retried 3 times to generate a resource index
|
// GetNextCommitStatusIndex retried 3 times to generate a resource index
|
||||||
func GetNextCommitStatusIndex(ctx context.Context, repoID int64, sha string) (int64, error) {
|
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}
|
return 0, git.ErrInvalidSHA{SHA: sha}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,7 +324,9 @@ func GetLatestCommitStatusForPairs(ctx context.Context, repoIDsToLatestCommitSHA
|
|||||||
Select("max( id ) as id, repo_id").
|
Select("max( id ) as id, repo_id").
|
||||||
GroupBy("context_hash, repo_id").OrderBy("max( id ) desc")
|
GroupBy("context_hash, repo_id").OrderBy("max( id ) desc")
|
||||||
|
|
||||||
|
if !listOptions.IsListAll() {
|
||||||
sess = db.SetSessionPagination(sess, &listOptions)
|
sess = db.SetSessionPagination(sess, &listOptions)
|
||||||
|
}
|
||||||
|
|
||||||
err := sess.Find(&results)
|
err := sess.Find(&results)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -423,7 +426,7 @@ func FindRepoRecentCommitStatusContexts(ctx context.Context, repoID int64, befor
|
|||||||
type NewCommitStatusOptions struct {
|
type NewCommitStatusOptions struct {
|
||||||
Repo *repo_model.Repository
|
Repo *repo_model.Repository
|
||||||
Creator *user_model.User
|
Creator *user_model.User
|
||||||
SHA string
|
SHA git.ObjectID
|
||||||
CommitStatus *CommitStatus
|
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)
|
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)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
if err != nil {
|
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)
|
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()
|
defer committer.Close()
|
||||||
|
|
||||||
// Get the next Status Index
|
// 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 {
|
if err != nil {
|
||||||
return fmt.Errorf("generate commit status index failed: %w", err)
|
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.Description = strings.TrimSpace(opts.CommitStatus.Description)
|
||||||
opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context)
|
opts.CommitStatus.Context = strings.TrimSpace(opts.CommitStatus.Context)
|
||||||
opts.CommitStatus.TargetURL = strings.TrimSpace(opts.CommitStatus.TargetURL)
|
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.CreatorID = opts.Creator.ID
|
||||||
opts.CommitStatus.RepoID = opts.Repo.ID
|
opts.CommitStatus.RepoID = opts.Repo.ID
|
||||||
opts.CommitStatus.Index = idx
|
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
|
// NewLFSMetaObject stores a given populated LFSMetaObject structure in the database
|
||||||
// if it is not already present.
|
// 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
|
var err error
|
||||||
|
|
||||||
ctx, committer, err := db.TxContext(ctx)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
@@ -144,16 +144,15 @@ func NewLFSMetaObject(ctx context.Context, m *LFSMetaObject) (*LFSMetaObject, er
|
|||||||
}
|
}
|
||||||
defer committer.Close()
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
} else if exist {
|
||||||
|
|
||||||
if has {
|
|
||||||
m.Existing = true
|
m.Existing = true
|
||||||
return m, committer.Commit()
|
return m, committer.Commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
m = &LFSMetaObject{Pointer: p, RepositoryID: repoID}
|
||||||
if err = db.Insert(ctx, m); err != nil {
|
if err = db.Insert(ctx, m); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gobwas/glob"
|
"github.com/gobwas/glob"
|
||||||
"github.com/gobwas/glob/syntax"
|
"github.com/gobwas/glob/syntax"
|
||||||
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
var ErrBranchIsProtected = errors.New("branch is protected")
|
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
|
// GetProtectedBranchRuleByName getting protected branch rule by name
|
||||||
func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName string) (*ProtectedBranch, error) {
|
func GetProtectedBranchRuleByName(ctx context.Context, repoID int64, ruleName string) (*ProtectedBranch, error) {
|
||||||
rel := &ProtectedBranch{RepoID: repoID, RuleName: ruleName}
|
// branch_name is legacy name, it actually is rule name
|
||||||
has, err := db.GetByBean(ctx, rel)
|
rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "branch_name": ruleName})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
} else if !exist {
|
||||||
if !has {
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return rel, 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
|
// GetProtectedBranchRuleByID getting protected branch rule by rule ID
|
||||||
func GetProtectedBranchRuleByID(ctx context.Context, repoID, ruleID int64) (*ProtectedBranch, error) {
|
func GetProtectedBranchRuleByID(ctx context.Context, repoID, ruleID int64) (*ProtectedBranch, error) {
|
||||||
rel := &ProtectedBranch{ID: ruleID, RepoID: repoID}
|
rel, exist, err := db.Get[ProtectedBranch](ctx, builder.Eq{"repo_id": repoID, "id": ruleID})
|
||||||
has, err := db.GetByBean(ctx, rel)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
} else if !exist {
|
||||||
if !has {
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return rel, nil
|
return rel, nil
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IssueAssignees saves all issue assignees
|
// 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
|
// 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) {
|
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.
|
// 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
|
Type CommentType
|
||||||
IssueIDs []int64
|
IssueIDs []int64
|
||||||
Invalidated util.OptionalBool
|
Invalidated util.OptionalBool
|
||||||
|
IsPull util.OptionalBool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToConds implements FindOptions interface
|
// ToConds implements FindOptions interface
|
||||||
@@ -1058,6 +1059,9 @@ func (opts FindCommentsOptions) ToConds() builder.Cond {
|
|||||||
if !opts.Invalidated.IsNone() {
|
if !opts.Invalidated.IsNone() {
|
||||||
cond = cond.And(builder.Eq{"comment.invalidated": opts.Invalidated.IsTrue()})
|
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
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1065,7 +1069,7 @@ func (opts FindCommentsOptions) ToConds() builder.Cond {
|
|||||||
func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) {
|
func FindComments(ctx context.Context, opts *FindCommentsOptions) (CommentList, error) {
|
||||||
comments := make([]*Comment, 0, 10)
|
comments := make([]*Comment, 0, 10)
|
||||||
sess := db.GetEngine(ctx).Where(opts.ToConds())
|
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")
|
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)
|
// 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{}
|
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 {
|
if err != nil {
|
||||||
log.Error("failed to get issue content history %v. err=%v", id, err)
|
log.Error("failed to get issue content history %v. err=%v", id, err)
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ func TestContentHistory(t *testing.T) {
|
|||||||
hasHistory2, _ := issues_model.HasIssueContentHistory(dbCtx, 10, 1)
|
hasHistory2, _ := issues_model.HasIssueContentHistory(dbCtx, 10, 1)
|
||||||
assert.False(t, hasHistory2)
|
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, 6, h6.ID)
|
||||||
assert.EqualValues(t, 5, h6Prev.ID)
|
assert.EqualValues(t, 5, h6Prev.ID)
|
||||||
|
|
||||||
// soft-delete
|
// soft-delete
|
||||||
_ = issues_model.SoftDeleteIssueContentHistory(dbCtx, 5)
|
_ = 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, 6, h6.ID)
|
||||||
assert.EqualValues(t, 4, h6Prev.ID)
|
assert.EqualValues(t, 4, h6Prev.ID)
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
type IssuesOptions struct { //nolint
|
type IssuesOptions struct { //nolint
|
||||||
db.Paginator
|
db.Paginator
|
||||||
RepoIDs []int64 // overwrites RepoCond if the length is not 0
|
RepoIDs []int64 // overwrites RepoCond if the length is not 0
|
||||||
|
AllPublic bool // include also all public repositories
|
||||||
RepoCond builder.Cond
|
RepoCond builder.Cond
|
||||||
AssigneeID int64
|
AssigneeID int64
|
||||||
PosterID int64
|
PosterID int64
|
||||||
@@ -197,6 +198,12 @@ func applyRepoConditions(sess *xorm.Session, opts *IssuesOptions) *xorm.Session
|
|||||||
} else if len(opts.RepoIDs) > 1 {
|
} else if len(opts.RepoIDs) > 1 {
|
||||||
opts.RepoCond = builder.In("issue.repo_id", opts.RepoIDs)
|
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 {
|
if opts.RepoCond != nil {
|
||||||
sess.And(opts.RepoCond)
|
sess.And(opts.RepoCond)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import (
|
|||||||
// IssueUser represents an issue-user relation.
|
// IssueUser represents an issue-user relation.
|
||||||
type IssueUser struct {
|
type IssueUser struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
UID int64 `xorm:"INDEX"` // User ID.
|
UID int64 `xorm:"INDEX unique(uid_to_issue)"` // User ID.
|
||||||
IssueID int64 `xorm:"INDEX"`
|
IssueID int64 `xorm:"INDEX unique(uid_to_issue)"`
|
||||||
IsRead bool
|
IsRead bool
|
||||||
IsMentioned bool
|
IsMentioned bool
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,15 +304,11 @@ func GetLabelInRepoByName(ctx context.Context, repoID int64, labelName string) (
|
|||||||
return nil, ErrRepoLabelNotExist{0, repoID}
|
return nil, ErrRepoLabelNotExist{0, repoID}
|
||||||
}
|
}
|
||||||
|
|
||||||
l := &Label{
|
l, exist, err := db.Get[Label](ctx, builder.Eq{"name": labelName, "repo_id": repoID})
|
||||||
Name: labelName,
|
|
||||||
RepoID: repoID,
|
|
||||||
}
|
|
||||||
has, err := db.GetByBean(ctx, l)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !exist {
|
||||||
return nil, ErrRepoLabelNotExist{0, l.RepoID}
|
return nil, ErrRepoLabelNotExist{0, repoID}
|
||||||
}
|
}
|
||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
@@ -323,15 +319,11 @@ func GetLabelInRepoByID(ctx context.Context, repoID, labelID int64) (*Label, err
|
|||||||
return nil, ErrRepoLabelNotExist{labelID, repoID}
|
return nil, ErrRepoLabelNotExist{labelID, repoID}
|
||||||
}
|
}
|
||||||
|
|
||||||
l := &Label{
|
l, exist, err := db.Get[Label](ctx, builder.Eq{"id": labelID, "repo_id": repoID})
|
||||||
ID: labelID,
|
|
||||||
RepoID: repoID,
|
|
||||||
}
|
|
||||||
has, err := db.GetByBean(ctx, l)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !exist {
|
||||||
return nil, ErrRepoLabelNotExist{l.ID, l.RepoID}
|
return nil, ErrRepoLabelNotExist{labelID, repoID}
|
||||||
}
|
}
|
||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
@@ -408,15 +400,11 @@ func GetLabelInOrgByName(ctx context.Context, orgID int64, labelName string) (*L
|
|||||||
return nil, ErrOrgLabelNotExist{0, orgID}
|
return nil, ErrOrgLabelNotExist{0, orgID}
|
||||||
}
|
}
|
||||||
|
|
||||||
l := &Label{
|
l, exist, err := db.Get[Label](ctx, builder.Eq{"name": labelName, "org_id": orgID})
|
||||||
Name: labelName,
|
|
||||||
OrgID: orgID,
|
|
||||||
}
|
|
||||||
has, err := db.GetByBean(ctx, l)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !exist {
|
||||||
return nil, ErrOrgLabelNotExist{0, l.OrgID}
|
return nil, ErrOrgLabelNotExist{0, orgID}
|
||||||
}
|
}
|
||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
@@ -427,15 +415,11 @@ func GetLabelInOrgByID(ctx context.Context, orgID, labelID int64) (*Label, error
|
|||||||
return nil, ErrOrgLabelNotExist{labelID, orgID}
|
return nil, ErrOrgLabelNotExist{labelID, orgID}
|
||||||
}
|
}
|
||||||
|
|
||||||
l := &Label{
|
l, exist, err := db.Get[Label](ctx, builder.Eq{"id": labelID, "org_id": orgID})
|
||||||
ID: labelID,
|
|
||||||
OrgID: orgID,
|
|
||||||
}
|
|
||||||
has, err := db.GetByBean(ctx, l)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !exist {
|
||||||
return nil, ErrOrgLabelNotExist{l.ID, l.OrgID}
|
return nil, ErrOrgLabelNotExist{labelID, orgID}
|
||||||
}
|
}
|
||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -295,16 +295,15 @@ func DeleteMilestoneByRepoID(ctx context.Context, repoID, id int64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
numMilestones, err := CountMilestones(ctx, GetMilestonesOption{
|
numMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
State: api.StateAll,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
numClosedMilestones, err := CountMilestones(ctx, GetMilestonesOption{
|
numClosedMilestones, err := db.Count[Milestone](ctx, FindMilestoneOptions{
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
State: api.StateClosed,
|
IsClosed: util.OptionalBoolTrue,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/util"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
|
||||||
|
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
@@ -25,31 +24,31 @@ func (milestones MilestoneList) getMilestoneIDs() []int64 {
|
|||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMilestonesOption contain options to get milestones
|
// FindMilestoneOptions contain options to get milestones
|
||||||
type GetMilestonesOption struct {
|
type FindMilestoneOptions struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
RepoID int64
|
RepoID int64
|
||||||
State api.StateType
|
IsClosed util.OptionalBool
|
||||||
Name string
|
Name string
|
||||||
SortType string
|
SortType string
|
||||||
|
RepoCond builder.Cond
|
||||||
|
RepoIDs []int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (opts GetMilestonesOption) toCond() builder.Cond {
|
func (opts FindMilestoneOptions) ToConds() builder.Cond {
|
||||||
cond := builder.NewCond()
|
cond := builder.NewCond()
|
||||||
if opts.RepoID != 0 {
|
if opts.RepoID != 0 {
|
||||||
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
cond = cond.And(builder.Eq{"repo_id": opts.RepoID})
|
||||||
}
|
}
|
||||||
|
if opts.IsClosed != util.OptionalBoolNone {
|
||||||
switch opts.State {
|
cond = cond.And(builder.Eq{"is_closed": opts.IsClosed.IsTrue()})
|
||||||
case api.StateClosed:
|
}
|
||||||
cond = cond.And(builder.Eq{"is_closed": true})
|
if opts.RepoCond != nil && opts.RepoCond.IsValid() {
|
||||||
case api.StateAll:
|
cond = cond.And(builder.In("repo_id", builder.Select("id").From("repository").Where(opts.RepoCond)))
|
||||||
break
|
}
|
||||||
// api.StateOpen:
|
if len(opts.RepoIDs) > 0 {
|
||||||
default:
|
cond = cond.And(builder.In("repo_id", opts.RepoIDs))
|
||||||
cond = cond.And(builder.Eq{"is_closed": false})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(opts.Name) != 0 {
|
if len(opts.Name) != 0 {
|
||||||
cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name))
|
cond = cond.And(db.BuildCaseInsensitiveLike("name", opts.Name))
|
||||||
}
|
}
|
||||||
@@ -57,34 +56,23 @@ func (opts GetMilestonesOption) toCond() builder.Cond {
|
|||||||
return cond
|
return cond
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetMilestones returns milestones filtered by GetMilestonesOption's
|
func (opts FindMilestoneOptions) ToOrders() string {
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch opts.SortType {
|
switch opts.SortType {
|
||||||
case "furthestduedate":
|
case "furthestduedate":
|
||||||
sess.Desc("deadline_unix")
|
return "deadline_unix DESC"
|
||||||
case "leastcomplete":
|
case "leastcomplete":
|
||||||
sess.Asc("completeness")
|
return "completeness ASC"
|
||||||
case "mostcomplete":
|
case "mostcomplete":
|
||||||
sess.Desc("completeness")
|
return "completeness DESC"
|
||||||
case "leastissues":
|
case "leastissues":
|
||||||
sess.Asc("num_issues")
|
return "num_issues ASC"
|
||||||
case "mostissues":
|
case "mostissues":
|
||||||
sess.Desc("num_issues")
|
return "num_issues DESC"
|
||||||
case "id":
|
case "id":
|
||||||
sess.Asc("id")
|
return "id ASC"
|
||||||
default:
|
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.
|
// 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)
|
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
|
// LoadTotalTrackedTimes loads for every milestone in the list the TotalTrackedTime by a batch request
|
||||||
func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error {
|
func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error {
|
||||||
type totalTimesByMilestone struct {
|
type totalTimesByMilestone struct {
|
||||||
@@ -183,47 +128,9 @@ func (milestones MilestoneList) LoadTotalTrackedTimes(ctx context.Context) error
|
|||||||
return nil
|
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`
|
// 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) {
|
func CountMilestonesMap(ctx context.Context, opts FindMilestoneOptions) (map[int64]int64, error) {
|
||||||
sess := db.GetEngine(ctx).Where("is_closed = ?", isClosed)
|
sess := db.GetEngine(ctx).Where(opts.ToConds())
|
||||||
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
countsSlice := make([]*struct {
|
countsSlice := make([]*struct {
|
||||||
RepoID int64
|
RepoID int64
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
api "code.gitea.io/gitea/modules/structs"
|
api "code.gitea.io/gitea/modules/structs"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"xorm.io/builder"
|
"xorm.io/builder"
|
||||||
@@ -39,10 +40,15 @@ func TestGetMilestoneByRepoID(t *testing.T) {
|
|||||||
func TestGetMilestonesByRepoID(t *testing.T) {
|
func TestGetMilestonesByRepoID(t *testing.T) {
|
||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
test := func(repoID int64, state api.StateType) {
|
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})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
||||||
milestones, _, err := issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
|
milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
State: state,
|
IsClosed: isClosed,
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
@@ -77,9 +83,9 @@ func TestGetMilestonesByRepoID(t *testing.T) {
|
|||||||
test(3, api.StateClosed)
|
test(3, api.StateClosed)
|
||||||
test(3, api.StateAll)
|
test(3, api.StateAll)
|
||||||
|
|
||||||
milestones, _, err := issues_model.GetMilestones(db.DefaultContext, issues_model.GetMilestonesOption{
|
milestones, err := db.Find[issues_model.Milestone](db.DefaultContext, issues_model.FindMilestoneOptions{
|
||||||
RepoID: unittest.NonexistentID,
|
RepoID: unittest.NonexistentID,
|
||||||
State: api.StateOpen,
|
IsClosed: util.OptionalBoolFalse,
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, milestones, 0)
|
assert.Len(t, milestones, 0)
|
||||||
@@ -90,13 +96,13 @@ func TestGetMilestones(t *testing.T) {
|
|||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
test := func(sortType string, sortCond func(*issues_model.Milestone) int) {
|
test := func(sortType string, sortCond func(*issues_model.Milestone) int) {
|
||||||
for _, page := range []int{0, 1} {
|
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{
|
ListOptions: db.ListOptions{
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: setting.UI.IssuePagingNum,
|
PageSize: setting.UI.IssuePagingNum,
|
||||||
},
|
},
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
State: api.StateOpen,
|
IsClosed: util.OptionalBoolFalse,
|
||||||
SortType: sortType,
|
SortType: sortType,
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -107,13 +113,13 @@ func TestGetMilestones(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.True(t, sort.IntsAreSorted(values))
|
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{
|
ListOptions: db.ListOptions{
|
||||||
Page: page,
|
Page: page,
|
||||||
PageSize: setting.UI.IssuePagingNum,
|
PageSize: setting.UI.IssuePagingNum,
|
||||||
},
|
},
|
||||||
RepoID: repo.ID,
|
RepoID: repo.ID,
|
||||||
State: api.StateClosed,
|
IsClosed: util.OptionalBoolTrue,
|
||||||
Name: "",
|
Name: "",
|
||||||
SortType: sortType,
|
SortType: sortType,
|
||||||
})
|
})
|
||||||
@@ -150,9 +156,8 @@ func TestCountRepoMilestones(t *testing.T) {
|
|||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
test := func(repoID int64) {
|
test := func(repoID int64) {
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
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,
|
RepoID: repoID,
|
||||||
State: api.StateAll,
|
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, repo.NumMilestones, count)
|
assert.EqualValues(t, repo.NumMilestones, count)
|
||||||
@@ -161,9 +166,8 @@ func TestCountRepoMilestones(t *testing.T) {
|
|||||||
test(2)
|
test(2)
|
||||||
test(3)
|
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,
|
RepoID: unittest.NonexistentID,
|
||||||
State: api.StateAll,
|
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, 0, count)
|
assert.EqualValues(t, 0, count)
|
||||||
@@ -173,9 +177,9 @@ func TestCountRepoClosedMilestones(t *testing.T) {
|
|||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
test := func(repoID int64) {
|
test := func(repoID int64) {
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: repoID})
|
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,
|
RepoID: repoID,
|
||||||
State: api.StateClosed,
|
IsClosed: util.OptionalBoolTrue,
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, repo.NumClosedMilestones, count)
|
assert.EqualValues(t, repo.NumClosedMilestones, count)
|
||||||
@@ -184,9 +188,9 @@ func TestCountRepoClosedMilestones(t *testing.T) {
|
|||||||
test(2)
|
test(2)
|
||||||
test(3)
|
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,
|
RepoID: unittest.NonexistentID,
|
||||||
State: api.StateClosed,
|
IsClosed: util.OptionalBoolTrue,
|
||||||
})
|
})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.EqualValues(t, 0, count)
|
assert.EqualValues(t, 0, count)
|
||||||
@@ -201,12 +205,19 @@ func TestCountMilestonesByRepoIDs(t *testing.T) {
|
|||||||
repo1OpenCount, repo1ClosedCount := milestonesCount(1)
|
repo1OpenCount, repo1ClosedCount := milestonesCount(1)
|
||||||
repo2OpenCount, repo2ClosedCount := milestonesCount(2)
|
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.NoError(t, err)
|
||||||
assert.EqualValues(t, repo1OpenCount, openCounts[1])
|
assert.EqualValues(t, repo1OpenCount, openCounts[1])
|
||||||
assert.EqualValues(t, repo2OpenCount, openCounts[2])
|
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.NoError(t, err)
|
||||||
assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
|
assert.EqualValues(t, repo1ClosedCount, closedCounts[1])
|
||||||
assert.EqualValues(t, repo2ClosedCount, closedCounts[2])
|
assert.EqualValues(t, repo2ClosedCount, closedCounts[2])
|
||||||
@@ -218,7 +229,15 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
|
|||||||
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
repo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||||
test := func(sortType string, sortCond func(*issues_model.Milestone) int) {
|
test := func(sortType string, sortCond func(*issues_model.Milestone) int) {
|
||||||
for _, page := range []int{0, 1} {
|
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.NoError(t, err)
|
||||||
assert.Len(t, openMilestones, repo1.NumOpenMilestones+repo2.NumOpenMilestones)
|
assert.Len(t, openMilestones, repo1.NumOpenMilestones+repo2.NumOpenMilestones)
|
||||||
values := make([]int, len(openMilestones))
|
values := make([]int, len(openMilestones))
|
||||||
@@ -227,7 +246,16 @@ func TestGetMilestonesByRepoIDs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assert.True(t, sort.IntsAreSorted(values))
|
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.NoError(t, err)
|
||||||
assert.Len(t, closedMilestones, repo1.NumClosedMilestones+repo2.NumClosedMilestones)
|
assert.Len(t, closedMilestones, repo1.NumClosedMilestones+repo2.NumClosedMilestones)
|
||||||
values = make([]int, len(closedMilestones))
|
values = make([]int, len(closedMilestones))
|
||||||
|
|||||||
@@ -660,13 +660,10 @@ func GetPullRequestByIssueIDWithNoAttributes(ctx context.Context, issueID int64)
|
|||||||
|
|
||||||
// GetPullRequestByIssueID returns pull request by given issue ID.
|
// GetPullRequestByIssueID returns pull request by given issue ID.
|
||||||
func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest, error) {
|
func GetPullRequestByIssueID(ctx context.Context, issueID int64) (*PullRequest, error) {
|
||||||
pr := &PullRequest{
|
pr, exist, err := db.Get[PullRequest](ctx, builder.Eq{"issue_id": issueID})
|
||||||
IssueID: issueID,
|
|
||||||
}
|
|
||||||
has, err := db.GetByBean(ctx, pr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !exist {
|
||||||
return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""}
|
return nil, ErrPullRequestNotExist{0, issueID, 0, 0, "", ""}
|
||||||
}
|
}
|
||||||
return pr, pr.LoadAttributes(ctx)
|
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),
|
NewMigration("Add auth_token table", v1_22.CreateAuthTokenTable),
|
||||||
// v282 -> v283
|
// v282 -> v283
|
||||||
NewMigration("Add Index to pull_auto_merge.doer_id", v1_22.AddIndexToPullAutoMergeDoerID),
|
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
|
// GetCurrentDBVersion returns the current db version
|
||||||
|
|||||||
@@ -32,7 +32,12 @@ func AddGitSizeAndLFSSizeToRepositoryTable(x *xorm.Engine) error {
|
|||||||
return err
|
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 {
|
if err != nil {
|
||||||
return err
|
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
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -171,10 +171,10 @@ func NewTeam(ctx context.Context, t *organization.Team) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.LowerName = strings.ToLower(t.Name)
|
t.LowerName = strings.ToLower(t.Name)
|
||||||
has, err = db.GetEngine(ctx).
|
has, err = db.Exist[organization.Team](ctx, builder.Eq{
|
||||||
Where("org_id=?", t.OrgID).
|
"org_id": t.OrgID,
|
||||||
And("lower_name=?", t.LowerName).
|
"lower_name": t.LowerName,
|
||||||
Get(new(organization.Team))
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -232,20 +232,20 @@ func UpdateTeam(ctx context.Context, t *organization.Team, authChanged, includeA
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer committer.Close()
|
defer committer.Close()
|
||||||
sess := db.GetEngine(ctx)
|
|
||||||
|
|
||||||
t.LowerName = strings.ToLower(t.Name)
|
t.LowerName = strings.ToLower(t.Name)
|
||||||
has, err := sess.
|
has, err := db.Exist[organization.Team](ctx, builder.Eq{
|
||||||
Where("org_id=?", t.OrgID).
|
"org_id": t.OrgID,
|
||||||
And("lower_name=?", t.LowerName).
|
"lower_name": t.LowerName,
|
||||||
And("id!=?", t.ID).
|
}.And(builder.Neq{"id": t.ID}),
|
||||||
Get(new(organization.Team))
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has {
|
} else if has {
|
||||||
return organization.ErrTeamAlreadyExist{OrgID: t.OrgID, Name: t.LowerName}
|
return organization.ErrTeamAlreadyExist{OrgID: t.OrgID, Name: t.LowerName}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sess := db.GetEngine(ctx)
|
||||||
if _, err = sess.ID(t.ID).Cols("name", "lower_name", "description",
|
if _, err = sess.ID(t.ID).Cols("name", "lower_name", "description",
|
||||||
"can_create_org_repo", "authorize", "includes_all_repositories").Update(t); err != nil {
|
"can_create_org_repo", "authorize", "includes_all_repositories").Update(t); err != nil {
|
||||||
return fmt.Errorf("update: %w", err)
|
return fmt.Errorf("update: %w", err)
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import (
|
|||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"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.
|
// GetTeam returns team by given team name and organization.
|
||||||
func GetTeam(ctx context.Context, orgID int64, name string) (*Team, error) {
|
func GetTeam(ctx context.Context, orgID int64, name string) (*Team, error) {
|
||||||
t := &Team{
|
t, exist, err := db.Get[Team](ctx, builder.Eq{"org_id": orgID, "lower_name": strings.ToLower(name)})
|
||||||
OrgID: orgID,
|
|
||||||
LowerName: strings.ToLower(name),
|
|
||||||
}
|
|
||||||
has, err := db.GetByBean(ctx, t)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !exist {
|
||||||
return nil, ErrTeamNotExist{orgID, 0, name}
|
return nil, ErrTeamNotExist{orgID, 0, name}
|
||||||
}
|
}
|
||||||
return t, nil
|
return t, nil
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
"code.gitea.io/gitea/models/perm"
|
"code.gitea.io/gitea/models/perm"
|
||||||
repo_model "code.gitea.io/gitea/models/repo"
|
repo_model "code.gitea.io/gitea/models/repo"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
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
|
// 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
|
return perm.AccessModeOwner, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
a := &Access{UserID: userID, RepoID: repo.ID}
|
a, exist, err := db.Get[Access](ctx, builder.Eq{"user_id": userID, "repo_id": repo.ID})
|
||||||
if has, err := db.GetByBean(ctx, a); !has || err != nil {
|
if err != nil {
|
||||||
return mode, err
|
return mode, err
|
||||||
|
} else if !exist {
|
||||||
|
return mode, nil
|
||||||
}
|
}
|
||||||
return a.Mode, nil
|
return a.Mode, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,6 +294,18 @@ func GetProjectByID(ctx context.Context, id int64) (*Project, error) {
|
|||||||
return p, nil
|
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
|
// UpdateProject updates project properties
|
||||||
func UpdateProject(ctx context.Context, p *Project) error {
|
func UpdateProject(ctx context.Context, p *Project) error {
|
||||||
if !IsCardTypeValid(p.CardType) {
|
if !IsCardTypeValid(p.CardType) {
|
||||||
|
|||||||
@@ -207,6 +207,21 @@ func GetReleaseByID(ctx context.Context, id int64) (*Release, error) {
|
|||||||
return rel, nil
|
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
|
// FindReleasesOptions describes the conditions to Find releases
|
||||||
type FindReleasesOptions struct {
|
type FindReleasesOptions struct {
|
||||||
db.ListOptions
|
db.ListOptions
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"code.gitea.io/gitea/models/unit"
|
"code.gitea.io/gitea/models/unit"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/base"
|
"code.gitea.io/gitea/modules/base"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
@@ -179,6 +180,7 @@ type Repository struct {
|
|||||||
IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"`
|
IsFsckEnabled bool `xorm:"NOT NULL DEFAULT true"`
|
||||||
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
|
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
|
||||||
Topics []string `xorm:"TEXT JSON"`
|
Topics []string `xorm:"TEXT JSON"`
|
||||||
|
ObjectFormat git.ObjectFormat `xorm:"-"`
|
||||||
|
|
||||||
TrustModel TrustModelType
|
TrustModel TrustModelType
|
||||||
|
|
||||||
@@ -274,6 +276,8 @@ func (repo *Repository) AfterLoad() {
|
|||||||
repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
|
repo.NumOpenMilestones = repo.NumMilestones - repo.NumClosedMilestones
|
||||||
repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects
|
repo.NumOpenProjects = repo.NumProjects - repo.NumClosedProjects
|
||||||
repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns
|
repo.NumOpenActionRuns = repo.NumActionRuns - repo.NumClosedActionRuns
|
||||||
|
|
||||||
|
repo.ObjectFormat = git.ObjectFormatFromID(git.Sha1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadAttributes loads attributes of the repository.
|
// LoadAttributes loads attributes of the repository.
|
||||||
@@ -313,7 +317,7 @@ func (repo *Repository) HTMLURL() string {
|
|||||||
// CommitLink make link to by commit full ID
|
// CommitLink make link to by commit full ID
|
||||||
// note: won't check whether it's an right id
|
// note: won't check whether it's an right id
|
||||||
func (repo *Repository) CommitLink(commitID string) (result string) {
|
func (repo *Repository) CommitLink(commitID string) (result string) {
|
||||||
if commitID == "" || commitID == "0000000000000000000000000000000000000000" {
|
if git.IsEmptyCommitID(commitID) {
|
||||||
result = ""
|
result = ""
|
||||||
} else {
|
} else {
|
||||||
result = repo.Link() + "/commit/" + url.PathEscape(commitID)
|
result = repo.Link() + "/commit/" + url.PathEscape(commitID)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/setting/config"
|
"code.gitea.io/gitea/modules/setting/config"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
|
|
||||||
|
"xorm.io/builder"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Setting struct {
|
type Setting struct {
|
||||||
@@ -36,16 +38,17 @@ func init() {
|
|||||||
const keyRevision = "revision"
|
const keyRevision = "revision"
|
||||||
|
|
||||||
func GetRevision(ctx context.Context) int {
|
func GetRevision(ctx context.Context) int {
|
||||||
revision := &Setting{SettingKey: keyRevision}
|
revision, exist, err := db.Get[Setting](ctx, builder.Eq{"setting_key": keyRevision})
|
||||||
if has, err := db.GetByBean(ctx, revision); err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
} else if !has {
|
} else if !exist {
|
||||||
err = db.Insert(ctx, &Setting{SettingKey: keyRevision, Version: 1})
|
err = db.Insert(ctx, &Setting{SettingKey: keyRevision, Version: 1})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return 1
|
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)
|
_, err = db.Exec(ctx, "UPDATE system_setting SET version=1 WHERE setting_key=?", keyRevision)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
@@ -81,7 +84,7 @@ func SetSettings(ctx context.Context, settings map[string]string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for k, v := range settings {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,4 +39,13 @@ func TestSettings(t *testing.T) {
|
|||||||
assert.EqualValues(t, 3, rev)
|
assert.EqualValues(t, 3, rev)
|
||||||
assert.Len(t, settings, 2)
|
assert.Len(t, settings, 2)
|
||||||
assert.EqualValues(t, "false", settings[keyName])
|
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
|
// Activate/deactivate a user's secondary email address
|
||||||
// First check if there's another user active with the same address
|
// First check if there's another user active with the same address
|
||||||
addr := EmailAddress{UID: userID, LowerEmail: strings.ToLower(email)}
|
addr, exist, err := db.Get[EmailAddress](ctx, builder.Eq{"uid": userID, "lower_email": strings.ToLower(email)})
|
||||||
if has, err := db.GetByBean(ctx, &addr); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !has {
|
} else if !exist {
|
||||||
return fmt.Errorf("no such email: %d (%s)", userID, email)
|
return fmt.Errorf("no such email: %d (%s)", userID, email)
|
||||||
}
|
}
|
||||||
|
|
||||||
if addr.IsActivated == activate {
|
if addr.IsActivated == activate {
|
||||||
// Already in the desired state; no action
|
// Already in the desired state; no action
|
||||||
return nil
|
return nil
|
||||||
@@ -544,25 +545,26 @@ func ActivateUserEmail(ctx context.Context, userID int64, email string, activate
|
|||||||
return ErrEmailAlreadyUsed{Email: email}
|
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)
|
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
|
// Activate/deactivate a user's primary email address and account
|
||||||
if addr.IsPrimary {
|
if addr.IsPrimary {
|
||||||
user := User{ID: userID, Email: email}
|
user, exist, err := db.Get[User](ctx, builder.Eq{"id": userID, "email": email})
|
||||||
if has, err := db.GetByBean(ctx, &user); err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !has {
|
} else if !exist {
|
||||||
return fmt.Errorf("no user with ID: %d and Email: %s", userID, email)
|
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
|
// The user's activation state should be synchronized with the primary email
|
||||||
if user.IsActive != activate {
|
if user.IsActive != activate {
|
||||||
user.IsActive = activate
|
user.IsActive = activate
|
||||||
if user.Rands, err = GetUserSalt(); err != nil {
|
if user.Rands, err = GetUserSalt(); err != nil {
|
||||||
return fmt.Errorf("unable to generate salt: %w", err)
|
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)
|
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
|
// LinkExternalToUser link the external user to the user
|
||||||
func LinkExternalToUser(ctx context.Context, user *User, externalLoginUser *ExternalLoginUser) error {
|
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).
|
has, err := db.Exist[ExternalLoginUser](ctx, builder.Eq{
|
||||||
NoAutoCondition().
|
"external_id": externalLoginUser.ExternalID,
|
||||||
Exist(externalLoginUser)
|
"login_source_id": externalLoginUser.LoginSourceID,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if has {
|
} else if has {
|
||||||
@@ -145,9 +146,10 @@ func GetUserIDByExternalUserID(ctx context.Context, provider, userID string) (in
|
|||||||
|
|
||||||
// UpdateExternalUserByExternalID updates an external user's information
|
// UpdateExternalUserByExternalID updates an external user's information
|
||||||
func UpdateExternalUserByExternalID(ctx context.Context, external *ExternalLoginUser) error {
|
func UpdateExternalUserByExternalID(ctx context.Context, external *ExternalLoginUser) error {
|
||||||
has, err := db.GetEngine(ctx).Where("external_id=? AND login_source_id=?", external.ExternalID, external.LoginSourceID).
|
has, err := db.Exist[ExternalLoginUser](ctx, builder.Eq{
|
||||||
NoAutoCondition().
|
"external_id": external.ExternalID,
|
||||||
Exist(external)
|
"login_source_id": external.LoginSourceID,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
webhook_module "code.gitea.io/gitea/modules/webhook"
|
webhook_module "code.gitea.io/gitea/modules/webhook"
|
||||||
|
|
||||||
gouuid "github.com/google/uuid"
|
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
|
// ReplayHookTask copies a hook task to get re-delivered
|
||||||
func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask, error) {
|
func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask, error) {
|
||||||
task := &HookTask{
|
task, exist, err := db.Get[HookTask](ctx, builder.Eq{"hook_id": hookID, "uuid": uuid})
|
||||||
HookID: hookID,
|
|
||||||
UUID: uuid,
|
|
||||||
}
|
|
||||||
has, err := db.GetByBean(ctx, task)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !exist {
|
||||||
return nil, ErrHookTaskNotExist{
|
return nil, ErrHookTaskNotExist{
|
||||||
HookID: hookID,
|
HookID: hookID,
|
||||||
UUID: uuid,
|
UUID: uuid,
|
||||||
|
|||||||
@@ -392,39 +392,40 @@ func CreateWebhooks(ctx context.Context, ws []*Webhook) error {
|
|||||||
return db.Insert(ctx, ws)
|
return db.Insert(ctx, ws)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getWebhook uses argument bean as query condition,
|
// GetWebhookByID returns webhook of repository by given ID.
|
||||||
// ID must be specified and do not assign unnecessary fields.
|
func GetWebhookByID(ctx context.Context, id int64) (*Webhook, error) {
|
||||||
func getWebhook(ctx context.Context, bean *Webhook) (*Webhook, error) {
|
bean := new(Webhook)
|
||||||
has, err := db.GetEngine(ctx).Get(bean)
|
has, err := db.GetEngine(ctx).ID(id).Get(bean)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else if !has {
|
} else if !has {
|
||||||
return nil, ErrWebhookNotExist{ID: bean.ID}
|
return nil, ErrWebhookNotExist{ID: id}
|
||||||
}
|
}
|
||||||
return bean, nil
|
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.
|
// GetWebhookByRepoID returns webhook of repository by given ID.
|
||||||
func GetWebhookByRepoID(ctx context.Context, repoID, id int64) (*Webhook, error) {
|
func GetWebhookByRepoID(ctx context.Context, repoID, id int64) (*Webhook, error) {
|
||||||
return getWebhook(ctx, &Webhook{
|
webhook := new(Webhook)
|
||||||
ID: id,
|
has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", id, repoID).Get(webhook)
|
||||||
RepoID: repoID,
|
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.
|
// GetWebhookByOwnerID returns webhook of a user or organization by given ID.
|
||||||
func GetWebhookByOwnerID(ctx context.Context, ownerID, id int64) (*Webhook, error) {
|
func GetWebhookByOwnerID(ctx context.Context, ownerID, id int64) (*Webhook, error) {
|
||||||
return getWebhook(ctx, &Webhook{
|
webhook := new(Webhook)
|
||||||
ID: id,
|
has, err := db.GetEngine(ctx).Where("id=? AND owner_id=?", id, ownerID).Get(webhook)
|
||||||
OwnerID: ownerID,
|
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
|
// ListWebhookOptions are options to filter webhooks on ListWebhooksByOpts
|
||||||
@@ -461,20 +462,20 @@ func UpdateWebhookLastStatus(ctx context.Context, w *Webhook) error {
|
|||||||
return err
|
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.
|
// 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)
|
ctx, committer, err := db.TxContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer committer.Close()
|
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
|
return err
|
||||||
} else if count == 0 {
|
} else if count == 0 {
|
||||||
return ErrWebhookNotExist{ID: bean.ID}
|
return ErrWebhookNotExist{ID: id}
|
||||||
} else if _, err = db.DeleteByBean(ctx, &HookTask{HookID: bean.ID}); err != nil {
|
} else if _, err = db.DeleteByBean(ctx, &HookTask{HookID: id}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,16 +484,16 @@ func deleteWebhook(ctx context.Context, bean *Webhook) (err error) {
|
|||||||
|
|
||||||
// DeleteWebhookByRepoID deletes webhook of repository by given ID.
|
// DeleteWebhookByRepoID deletes webhook of repository by given ID.
|
||||||
func DeleteWebhookByRepoID(ctx context.Context, repoID, id int64) error {
|
func DeleteWebhookByRepoID(ctx context.Context, repoID, id int64) error {
|
||||||
return deleteWebhook(ctx, &Webhook{
|
if _, err := GetWebhookByRepoID(ctx, repoID, id); err != nil {
|
||||||
ID: id,
|
return err
|
||||||
RepoID: repoID,
|
}
|
||||||
})
|
return DeleteWebhookByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteWebhookByOwnerID deletes webhook of a user or organization by given ID.
|
// DeleteWebhookByOwnerID deletes webhook of a user or organization by given ID.
|
||||||
func DeleteWebhookByOwnerID(ctx context.Context, ownerID, id int64) error {
|
func DeleteWebhookByOwnerID(ctx context.Context, ownerID, id int64) error {
|
||||||
return deleteWebhook(ctx, &Webhook{
|
if _, err := GetWebhookByOwnerID(ctx, ownerID, id); err != nil {
|
||||||
ID: id,
|
return err
|
||||||
OwnerID: ownerID,
|
}
|
||||||
})
|
return DeleteWebhookByID(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -308,6 +308,12 @@ func RepoRefForAPI(next http.Handler) http.Handler {
|
|||||||
return
|
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 {
|
if ref := ctx.FormTrim("ref"); len(ref) > 0 {
|
||||||
commit, err := ctx.Repo.GitRepo.GetCommit(ref)
|
commit, err := ctx.Repo.GitRepo.GetCommit(ref)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -325,7 +331,6 @@ func RepoRefForAPI(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny)
|
refName := getRefName(ctx.Base, ctx.Repo, RepoRefAny)
|
||||||
|
|
||||||
if ctx.Repo.GitRepo.IsBranchExist(refName) {
|
if ctx.Repo.GitRepo.IsBranchExist(refName) {
|
||||||
@@ -342,7 +347,7 @@ func RepoRefForAPI(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
|
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.CommitID = refName
|
||||||
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
|
ctx.Repo.Commit, err = ctx.Repo.GitRepo.GetCommit(refName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -668,11 +668,9 @@ func RepoAssignment(ctx *Context) context.CancelFunc {
|
|||||||
branchOpts := git_model.FindBranchOptions{
|
branchOpts := git_model.FindBranchOptions{
|
||||||
RepoID: ctx.Repo.Repository.ID,
|
RepoID: ctx.Repo.Repository.ID,
|
||||||
IsDeletedBranch: util.OptionalBoolFalse,
|
IsDeletedBranch: util.OptionalBoolFalse,
|
||||||
ListOptions: db.ListOptions{
|
ListOptions: db.ListOptionsAll,
|
||||||
ListAll: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
branchesTotal, err := git_model.CountBranches(ctx, branchOpts)
|
branchesTotal, err := db.Count[git_model.Branch](ctx, branchOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("CountBranches", err)
|
ctx.ServerError("CountBranches", err)
|
||||||
return cancel
|
return cancel
|
||||||
@@ -827,7 +825,9 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
|
|||||||
}
|
}
|
||||||
// For legacy and API support only full commit sha
|
// For legacy and API support only full commit sha
|
||||||
parts := strings.Split(path, "/")
|
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:], "/")
|
repo.TreePath = strings.Join(parts[1:], "/")
|
||||||
return parts[0]
|
return parts[0]
|
||||||
}
|
}
|
||||||
@@ -871,7 +871,9 @@ func getRefName(ctx *Base, repo *Repository, pathType RepoRefType) string {
|
|||||||
return getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsTagExist)
|
return getRefNameFromPath(ctx, repo, path, repo.GitRepo.IsTagExist)
|
||||||
case RepoRefCommit:
|
case RepoRefCommit:
|
||||||
parts := strings.Split(path, "/")
|
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:], "/")
|
repo.TreePath = strings.Join(parts[1:], "/")
|
||||||
return parts[0]
|
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.
|
// Get default branch.
|
||||||
if len(ctx.Params("*")) == 0 {
|
if len(ctx.Params("*")) == 0 {
|
||||||
refName = ctx.Repo.Repository.DefaultBranch
|
refName = ctx.Repo.Repository.DefaultBranch
|
||||||
@@ -997,7 +1005,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
|
|||||||
return cancel
|
return cancel
|
||||||
}
|
}
|
||||||
ctx.Repo.CommitID = ctx.Repo.Commit.ID.String()
|
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.IsViewCommit = true
|
||||||
ctx.Repo.CommitID = refName
|
ctx.Repo.CommitID = refName
|
||||||
|
|
||||||
@@ -1007,7 +1015,7 @@ func RepoRefByType(refType RepoRefType, ignoreNotExistErr ...bool) func(*Context
|
|||||||
return cancel
|
return cancel
|
||||||
}
|
}
|
||||||
// If short commit ID add canonical link header
|
// 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\"",
|
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))))
|
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
|
// RunChecks runs the doctor checks for the provided list
|
||||||
func RunChecks(ctx context.Context, colorize, autofix bool, checks []*Check) error {
|
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
|
// the checks output logs by a special logger, they do not use the default logger
|
||||||
logger := log.BaseLoggerToGeneralLogger(&doctorCheckLogger{colorize: colorize})
|
logger := log.BaseLoggerToGeneralLogger(&doctorCheckLogger{colorize: colorize})
|
||||||
loggerStep := log.BaseLoggerToGeneralLogger(&doctorCheckStepLogger{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("OK")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
logger.Info("\nAll done.")
|
logger.Info("\nAll done (checks: %d).", len(checks))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register registers a command with the list
|
// Register registers a command with the list
|
||||||
func Register(command *Check) {
|
func Register(command *Check) {
|
||||||
Checks = append(Checks, command)
|
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
|
|
||||||
}
|
}
|
||||||
if Checks[i].Priority == 0 {
|
|
||||||
|
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 {
|
||||||
return false
|
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
|
// countOrphanedRepos count repository where user of owner_id do not exist
|
||||||
func countOrphanedRepos(ctx context.Context) (int64, error) {
|
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
|
// deleteOrphanedRepos delete repository where user of owner_id do not exist
|
||||||
@@ -43,7 +43,7 @@ func deleteOrphanedRepos(ctx context.Context) (int64, error) {
|
|||||||
default:
|
default:
|
||||||
var ids []int64
|
var ids []int64
|
||||||
if err := e.Table("`repository`").
|
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"}).
|
Where(builder.IsNull{"`user`.id"}).
|
||||||
Select("`repository`.id").Limit(batchSize).Find(&ids); err != nil {
|
Select("`repository`.id").Limit(batchSize).Find(&ids); err != nil {
|
||||||
return deleted, err
|
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
|
// ReadBatchLine reads the header line from cat-file --batch
|
||||||
// We expect:
|
// We expect:
|
||||||
// <sha> SP <type> SP <size> LF
|
// <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) {
|
func ReadBatchLine(rd *bufio.Reader) (sha []byte, typ string, size int64, err error) {
|
||||||
typ, err = rd.ReadString('\n')
|
typ, err = rd.ReadString('\n')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -251,20 +251,19 @@ headerLoop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// git tree files are a list:
|
// 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
|
// 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"
|
const hextable = "0123456789abcdef"
|
||||||
|
|
||||||
// To40ByteSHA converts a 20-byte SHA into a 40-byte sha. Input and output can be the
|
// BinToHexHeash converts a binary Hash into a hex encoded one. Input and output can be the
|
||||||
// same 40 byte slice to support in place conversion without allocations.
|
// same byte slice to support in place conversion without allocations.
|
||||||
// This is at least 100x quicker that hex.EncodeToString
|
// This is at least 100x quicker that hex.EncodeToString
|
||||||
// NB This requires that out is a 40-byte slice
|
func BinToHex(objectFormat ObjectFormat, sha, out []byte) []byte {
|
||||||
func To40ByteSHA(sha, out []byte) []byte {
|
for i := objectFormat.FullLength()/2 - 1; i >= 0; i-- {
|
||||||
for i := 19; i >= 0; i-- {
|
|
||||||
v := sha[i]
|
v := sha[i]
|
||||||
vhi, vlo := v>>4, v&0x0f
|
vhi, vlo := v>>4, v&0x0f
|
||||||
shi, slo := hextable[vhi], hextable[vlo]
|
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
|
// It is recommended therefore to pass in an fnameBuf large enough to avoid almost all allocations
|
||||||
//
|
//
|
||||||
// Each line is composed of:
|
// 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
|
// We don't attempt to convert the raw HASH to save a lot of time
|
||||||
func ParseTreeLine(rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
|
func ParseTreeLine(objectFormat ObjectFormat, rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fname, sha []byte, n int, err error) {
|
||||||
var readBytes []byte
|
var readBytes []byte
|
||||||
|
|
||||||
// Read the Mode & fname
|
// Read the Mode & fname
|
||||||
@@ -324,11 +323,12 @@ func ParseTreeLine(rd *bufio.Reader, modeBuf, fnameBuf, shaBuf []byte) (mode, fn
|
|||||||
fnameBuf = fnameBuf[:len(fnameBuf)-1]
|
fnameBuf = fnameBuf[:len(fnameBuf)-1]
|
||||||
fname = fnameBuf
|
fname = fnameBuf
|
||||||
|
|
||||||
// Deal with the 20-byte SHA
|
// Deal with the binary hash
|
||||||
idx = 0
|
idx = 0
|
||||||
for idx < 20 {
|
len := objectFormat.FullLength() / 2
|
||||||
|
for idx < len {
|
||||||
var read int
|
var read int
|
||||||
read, err = rd.Read(shaBuf[idx:20])
|
read, err = rd.Read(shaBuf[idx:len])
|
||||||
n += read
|
n += read
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mode, fname, sha, n, err
|
return mode, fname, sha, n, err
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
@@ -20,6 +19,8 @@ import (
|
|||||||
type BlamePart struct {
|
type BlamePart struct {
|
||||||
Sha string
|
Sha string
|
||||||
Lines []string
|
Lines []string
|
||||||
|
PreviousSha string
|
||||||
|
PreviousPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// BlameReader returns part of file blame one by one
|
// BlameReader returns part of file blame one by one
|
||||||
@@ -30,47 +31,56 @@ type BlameReader struct {
|
|||||||
done chan error
|
done chan error
|
||||||
lastSha *string
|
lastSha *string
|
||||||
ignoreRevsFile *string
|
ignoreRevsFile *string
|
||||||
|
objectFormat ObjectFormat
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *BlameReader) UsesIgnoreRevs() bool {
|
func (r *BlameReader) UsesIgnoreRevs() bool {
|
||||||
return r.ignoreRevsFile != nil
|
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)
|
// NextPart returns next part of blame (sequential code lines with the same commit)
|
||||||
func (r *BlameReader) NextPart() (*BlamePart, error) {
|
func (r *BlameReader) NextPart() (*BlamePart, error) {
|
||||||
var blamePart *BlamePart
|
var blamePart *BlamePart
|
||||||
|
|
||||||
if r.lastSha != nil {
|
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 isPrefix bool
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
for err != io.EOF {
|
for err != io.EOF {
|
||||||
line, isPrefix, err = r.bufferedReader.ReadLine()
|
lineBytes, isPrefix, err = r.bufferedReader.ReadLine()
|
||||||
if err != nil && err != io.EOF {
|
if err != nil && err != io.EOF {
|
||||||
return blamePart, err
|
return blamePart, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(line) == 0 {
|
if len(lineBytes) == 0 {
|
||||||
// isPrefix will be false
|
// isPrefix will be false
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
lines := shaLineRegex.FindSubmatch(line)
|
var objectID string
|
||||||
if lines != nil {
|
objectFormatLength := r.objectFormat.FullLength()
|
||||||
sha1 := string(lines[1])
|
|
||||||
|
|
||||||
|
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 {
|
if blamePart == nil {
|
||||||
blamePart = &BlamePart{sha1, make([]string, 0)}
|
blamePart = &BlamePart{
|
||||||
|
Sha: objectID,
|
||||||
|
Lines: make([]string, 0),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if blamePart.Sha != sha1 {
|
if blamePart.Sha != objectID {
|
||||||
r.lastSha = &sha1
|
r.lastSha = &objectID
|
||||||
// need to munch to end of line...
|
// need to munch to end of line...
|
||||||
for isPrefix {
|
for isPrefix {
|
||||||
_, isPrefix, err = r.bufferedReader.ReadLine()
|
_, isPrefix, err = r.bufferedReader.ReadLine()
|
||||||
@@ -80,10 +90,13 @@ func (r *BlameReader) NextPart() (*BlamePart, error) {
|
|||||||
}
|
}
|
||||||
return blamePart, nil
|
return blamePart, nil
|
||||||
}
|
}
|
||||||
} else if line[0] == '\t' {
|
} else if lineBytes[0] == '\t' {
|
||||||
code := line[1:]
|
blamePart.Lines = append(blamePart.Lines, string(lineBytes[1:]))
|
||||||
|
} else if bytes.HasPrefix(lineBytes, []byte(previousHeader)) {
|
||||||
blamePart.Lines = append(blamePart.Lines, string(code))
|
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...
|
// 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
|
// 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
|
var ignoreRevsFile *string
|
||||||
if CheckGitVersionAtLeast("2.23") == nil && !bypassBlameIgnore {
|
if CheckGitVersionAtLeast("2.23") == nil && !bypassBlameIgnore {
|
||||||
ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit)
|
ignoreRevsFile = tryCreateBlameIgnoreRevsFile(commit)
|
||||||
@@ -162,6 +175,7 @@ func CreateBlameReader(ctx context.Context, repoPath string, commit *Commit, fil
|
|||||||
bufferedReader: bufferedReader,
|
bufferedReader: bufferedReader,
|
||||||
done: done,
|
done: done,
|
||||||
ignoreRevsFile: ignoreRevsFile,
|
ignoreRevsFile: ignoreRevsFile,
|
||||||
|
objectFormat: objectFormat,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,20 +24,22 @@ func TestReadingBlameOutput(t *testing.T) {
|
|||||||
|
|
||||||
parts := []*BlamePart{
|
parts := []*BlamePart{
|
||||||
{
|
{
|
||||||
"72866af952e98d02a73003501836074b286a78f6",
|
Sha: "72866af952e98d02a73003501836074b286a78f6",
|
||||||
[]string{
|
Lines: []string{
|
||||||
"# test_repo",
|
"# test_repo",
|
||||||
"Test repository for testing migration from github to gitea",
|
"Test repository for testing migration from github to gitea",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"f32b0a9dfd09a60f616f29158f772cedd89942d2",
|
Sha: "f32b0a9dfd09a60f616f29158f772cedd89942d2",
|
||||||
[]string{"", "Do not make any changes to this repo it is used for unit testing"},
|
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} {
|
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.NoError(t, err)
|
||||||
assert.NotNil(t, blameReader)
|
assert.NotNil(t, blameReader)
|
||||||
defer blameReader.Close()
|
defer blameReader.Close()
|
||||||
@@ -64,16 +66,18 @@ func TestReadingBlameOutput(t *testing.T) {
|
|||||||
|
|
||||||
full := []*BlamePart{
|
full := []*BlamePart{
|
||||||
{
|
{
|
||||||
"af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
Sha: "af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||||
[]string{"line", "line"},
|
Lines: []string{"line", "line"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"45fb6cbc12f970b04eacd5cd4165edd11c8d7376",
|
Sha: "45fb6cbc12f970b04eacd5cd4165edd11c8d7376",
|
||||||
[]string{"changed line"},
|
Lines: []string{"changed line"},
|
||||||
|
PreviousSha: "af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||||
|
PreviousPath: "blame.txt",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
Sha: "af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||||
[]string{"line", "line", ""},
|
Lines: []string{"line", "line", ""},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,8 +93,8 @@ func TestReadingBlameOutput(t *testing.T) {
|
|||||||
Bypass: false,
|
Bypass: false,
|
||||||
Parts: []*BlamePart{
|
Parts: []*BlamePart{
|
||||||
{
|
{
|
||||||
"af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
Sha: "af7486bd54cfc39eea97207ca666aa69c9d6df93",
|
||||||
[]string{"line", "line", "changed line", "line", "line", ""},
|
Lines: []string{"line", "line", "changed line", "line", "line", ""},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -118,7 +122,7 @@ func TestReadingBlameOutput(t *testing.T) {
|
|||||||
commit, err := repo.GetCommit(c.CommitID)
|
commit, err := repo.GetCommit(c.CommitID)
|
||||||
assert.NoError(t, err)
|
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.NoError(t, err)
|
||||||
assert.NotNil(t, blameReader)
|
assert.NotNil(t, blameReader)
|
||||||
defer blameReader.Close()
|
defer blameReader.Close()
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
// Blob represents a Git object.
|
// Blob represents a Git object.
|
||||||
type Blob struct {
|
type Blob struct {
|
||||||
ID SHA1
|
ID ObjectID
|
||||||
|
|
||||||
gogitEncodedObj plumbing.EncodedObject
|
gogitEncodedObj plumbing.EncodedObject
|
||||||
name string
|
name string
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import (
|
|||||||
|
|
||||||
// Blob represents a Git object.
|
// Blob represents a Git object.
|
||||||
type Blob struct {
|
type Blob struct {
|
||||||
ID SHA1
|
ID ObjectID
|
||||||
|
|
||||||
gotSize bool
|
gotSize bool
|
||||||
size int64
|
size int64
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ import (
|
|||||||
// Commit represents a git commit.
|
// Commit represents a git commit.
|
||||||
type Commit struct {
|
type Commit struct {
|
||||||
Tree
|
Tree
|
||||||
ID SHA1 // The ID of this commit object
|
ID ObjectID // The ID of this commit object
|
||||||
Author *Signature
|
Author *Signature
|
||||||
Committer *Signature
|
Committer *Signature
|
||||||
CommitMessage string
|
CommitMessage string
|
||||||
Signature *CommitGPGSignature
|
Signature *CommitGPGSignature
|
||||||
|
|
||||||
Parents []SHA1 // SHA1 strings
|
Parents []ObjectID // ID strings
|
||||||
submoduleCache *ObjectCache
|
submoduleCache *ObjectCache
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,15 +43,16 @@ func (c *Commit) Message() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Summary returns first line of commit message.
|
// Summary returns first line of commit message.
|
||||||
|
// The string is forced to be valid UTF8
|
||||||
func (c *Commit) Summary() string {
|
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).
|
// ParentID returns oid of n-th parent (0-based index).
|
||||||
// It returns nil if no such parent exists.
|
// 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) {
|
if n >= len(c.Parents) {
|
||||||
return SHA1{}, ErrNotExist{"", ""}
|
return nil, ErrNotExist{"", ""}
|
||||||
}
|
}
|
||||||
return c.Parents[n], nil
|
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
|
// 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()
|
this := c.ID.String()
|
||||||
that := commitHash.String()
|
that := objectID.String()
|
||||||
|
|
||||||
if this == that {
|
if this == that {
|
||||||
return false, nil
|
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
|
// IsForcePush returns true if a push from oldCommitHash to this is a force push
|
||||||
func (c *Commit) IsForcePush(oldCommitID string) (bool, error) {
|
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
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
oldCommit, err := c.repo.GetCommit(oldCommitID)
|
oldCommit, err := c.repo.GetCommit(oldCommitID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|||||||
@@ -59,11 +59,11 @@ func convertPGPSignature(c *object.Commit) *CommitGPGSignature {
|
|||||||
|
|
||||||
func convertCommit(c *object.Commit) *Commit {
|
func convertCommit(c *object.Commit) *Commit {
|
||||||
return &Commit{
|
return &Commit{
|
||||||
ID: c.Hash,
|
ID: ParseGogitHash(c.Hash),
|
||||||
CommitMessage: c.Message,
|
CommitMessage: c.Message,
|
||||||
Committer: &c.Committer,
|
Committer: &c.Committer,
|
||||||
Author: &c.Author,
|
Author: &c.Author,
|
||||||
Signature: convertPGPSignature(c),
|
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()
|
defer commitGraphFile.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := commitNodeIndex.Get(commit.ID)
|
c, err := commitNodeIndex.Get(plumbing.Hash(commit.ID.RawValue()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ func GetLastCommitForPaths(ctx context.Context, commit *Commit, treePath string,
|
|||||||
if typ != "commit" {
|
if typ != "commit" {
|
||||||
return nil, fmt.Errorf("unexpected type: %s for commit id: %s", typ, commitID)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ import (
|
|||||||
// We need this to interpret commits from cat-file or cat-file --batch
|
// 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
|
// 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{
|
commit := &Commit{
|
||||||
ID: sha,
|
ID: objectID,
|
||||||
Author: &Signature{},
|
Author: &Signature{},
|
||||||
Committer: &Signature{},
|
Committer: &Signature{},
|
||||||
}
|
}
|
||||||
@@ -71,10 +71,10 @@ readLoop:
|
|||||||
|
|
||||||
switch string(split[0]) {
|
switch string(split[0]) {
|
||||||
case "tree":
|
case "tree":
|
||||||
commit.Tree = *NewTree(gitRepo, MustIDFromString(string(data)))
|
commit.Tree = *NewTree(gitRepo, objectID.Type().MustIDFromString(string(data)))
|
||||||
_, _ = payloadSB.Write(line)
|
_, _ = payloadSB.Write(line)
|
||||||
case "parent":
|
case "parent":
|
||||||
commit.Parents = append(commit.Parents, MustIDFromString(string(data)))
|
commit.Parents = append(commit.Parents, objectID.Type().MustIDFromString(string(data)))
|
||||||
_, _ = payloadSB.Write(line)
|
_, _ = payloadSB.Write(line)
|
||||||
case "author":
|
case "author":
|
||||||
commit.Author = &Signature{}
|
commit.Author = &Signature{}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ gpgsig -----BEGIN PGP SIGNATURE-----
|
|||||||
|
|
||||||
empty commit`
|
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"))
|
gitRepo, err := openRepositoryWithDefaultContext(filepath.Join(testReposDir, "repo1_bare"))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.NotNil(t, gitRepo)
|
assert.NotNil(t, gitRepo)
|
||||||
@@ -135,8 +135,8 @@ func TestHasPreviousCommit(t *testing.T) {
|
|||||||
commit, err := repo.GetCommit("8006ff9adbf0cb94da7dad9e537e53817f9fa5c0")
|
commit, err := repo.GetCommit("8006ff9adbf0cb94da7dad9e537e53817f9fa5c0")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
parentSHA := MustIDFromString("8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2")
|
parentSHA := repo.objectFormat.MustIDFromString("8d92fc957a4d7cfd98bc375f0b7bb189a0d6c9f2")
|
||||||
notParentSHA := MustIDFromString("2839944139e0de9737a044f78b0e4b40d989a9e3")
|
notParentSHA := repo.objectFormat.MustIDFromString("2839944139e0de9737a044f78b0e4b40d989a9e3")
|
||||||
|
|
||||||
haz, err := commit.HasPreviousCommit(parentSHA)
|
haz, err := commit.HasPreviousCommit(parentSHA)
|
||||||
assert.NoError(t, err)
|
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 is the default context to run git commands in, must be initialized by git.InitXxx
|
||||||
DefaultContext context.Context
|
DefaultContext context.Context
|
||||||
|
|
||||||
// SupportProcReceive version >= 2.29.0
|
SupportProcReceive bool // >= 2.29
|
||||||
SupportProcReceive bool
|
SupportHashSha256 bool // >= 2.42, SHA-256 repositories no longer an ‘experimental curiosity’
|
||||||
|
|
||||||
gitVersion *version.Version
|
gitVersion *version.Version
|
||||||
)
|
)
|
||||||
@@ -189,7 +189,7 @@ func InitFull(ctx context.Context) (err error) {
|
|||||||
globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=")
|
globalCommandArgs = append(globalCommandArgs, "-c", "credential.helper=")
|
||||||
}
|
}
|
||||||
SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
|
SupportProcReceive = CheckGitVersionAtLeast("2.29") == nil
|
||||||
|
SupportHashSha256 = CheckGitVersionAtLeast("2.42") == nil
|
||||||
if setting.LFS.StartServer {
|
if setting.LFS.StartServer {
|
||||||
if CheckGitVersionAtLeast("2.1.2") != nil {
|
if CheckGitVersionAtLeast("2.1.2") != nil {
|
||||||
return errors.New("LFS server support requires Git >= 2.1.2")
|
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
|
// GetCommitByPath gets the last commit for the entry in the provided commit
|
||||||
func (c *LastCommitCache) GetCommitByPath(commitID, entryPath string) (*Commit, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lastCommit, err := c.Get(sha1.String(), entryPath)
|
lastCommit, err := c.Get(sha.String(), entryPath)
|
||||||
if err != nil || lastCommit != nil {
|
if err != nil || lastCommit != nil {
|
||||||
return lastCommit, err
|
return lastCommit, err
|
||||||
}
|
}
|
||||||
|
|
||||||
lastCommit, err = c.repo.getCommitByPathWithID(sha1, entryPath)
|
lastCommit, err = c.repo.getCommitByPathWithID(sha, entryPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ package git
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
cgobject "github.com/go-git/go-git/v5/plumbing/object/commitgraph"
|
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()
|
commitNodeIndex, _ := c.repo.CommitNodeIndex()
|
||||||
|
|
||||||
index, err := commitNodeIndex.Get(c.ID)
|
index, err := commitNodeIndex.Get(plumbing.Hash(c.ID.RawValue()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// Our "line" must look like: <commitid> SP (<parent> SP) * NUL
|
||||||
ret.CommitID = string(g.next[0:40])
|
commitIds := string(g.next)
|
||||||
parents := string(g.next[41:])
|
|
||||||
if g.buffull {
|
if g.buffull {
|
||||||
more, err := g.rd.ReadString('\x00')
|
more, err := g.rd.ReadString('\x00')
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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"
|
// now read the next "line"
|
||||||
g.buffull = false
|
g.buffull = false
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
|
||||||
|
"github.com/go-git/go-git/v5/plumbing"
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
"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()
|
defer commitGraphFile.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
commitNode, err := commitNodeIndex.Get(notes.ID)
|
commitNode, err := commitNodeIndex.Get(plumbing.Hash(notes.ID.RawValue()))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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"
|
"strconv"
|
||||||
"strings"
|
"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/filemode"
|
||||||
|
"github.com/go-git/go-git/v5/plumbing/hash"
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseTreeEntries parses the output of a `git ls-tree -l` command.
|
// 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)
|
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]))
|
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))
|
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 {
|
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 = plumbing.Hash(entry.ID.RawValue())
|
||||||
entry.gogitTreeEntry.Hash = id
|
|
||||||
pos += 41 // skip over sha and trailing space
|
pos += 41 // skip over sha and trailing space
|
||||||
|
|
||||||
end := pos + bytes.IndexByte(data[pos:], '\t')
|
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).
|
// In case entry name is surrounded by double quotes(it happens only in git-shell).
|
||||||
if data[pos] == '"' {
|
if data[pos] == '"' {
|
||||||
|
var err error
|
||||||
entry.gogitTreeEntry.Name, err = strconv.Unquote(string(data[pos:end]))
|
entry.gogitTreeEntry.Name, err = strconv.Unquote(string(data[pos:end]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Invalid ls-tree output: %w", err)
|
return nil, fmt.Errorf("Invalid ls-tree output: %w", err)
|
||||||
|
|||||||
@@ -6,8 +6,10 @@
|
|||||||
package git
|
package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"testing"
|
"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/filemode"
|
||||||
"github.com/go-git/go-git/v5/plumbing/object"
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -26,9 +28,9 @@ func TestParseTreeEntries(t *testing.T) {
|
|||||||
Input: "100644 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c 1022\texample/file2.txt\n",
|
Input: "100644 blob 61ab7345a1a3bbc590068ccae37b8515cfc5843c 1022\texample/file2.txt\n",
|
||||||
Expected: []*TreeEntry{
|
Expected: []*TreeEntry{
|
||||||
{
|
{
|
||||||
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
ID: ObjectFormatFromID(Sha1).MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
||||||
gogitTreeEntry: &object.TreeEntry{
|
gogitTreeEntry: &object.TreeEntry{
|
||||||
Hash: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
Hash: plumbing.Hash(ObjectFormatFromID(Sha1).MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()),
|
||||||
Name: "example/file2.txt",
|
Name: "example/file2.txt",
|
||||||
Mode: filemode.Regular,
|
Mode: filemode.Regular,
|
||||||
},
|
},
|
||||||
@@ -42,9 +44,9 @@ func TestParseTreeEntries(t *testing.T) {
|
|||||||
"040000 tree 1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8 -\texample\n",
|
"040000 tree 1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8 -\texample\n",
|
||||||
Expected: []*TreeEntry{
|
Expected: []*TreeEntry{
|
||||||
{
|
{
|
||||||
ID: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
ID: ObjectFormatFromID(Sha1).MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
||||||
gogitTreeEntry: &object.TreeEntry{
|
gogitTreeEntry: &object.TreeEntry{
|
||||||
Hash: MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c"),
|
Hash: plumbing.Hash(ObjectFormatFromID(Sha1).MustIDFromString("61ab7345a1a3bbc590068ccae37b8515cfc5843c").RawValue()),
|
||||||
Name: "example/\n.txt",
|
Name: "example/\n.txt",
|
||||||
Mode: filemode.Symlink,
|
Mode: filemode.Symlink,
|
||||||
},
|
},
|
||||||
@@ -52,10 +54,10 @@ func TestParseTreeEntries(t *testing.T) {
|
|||||||
sized: true,
|
sized: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
|
ID: ObjectFormatFromID(Sha1).MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
|
||||||
sized: true,
|
sized: true,
|
||||||
gogitTreeEntry: &object.TreeEntry{
|
gogitTreeEntry: &object.TreeEntry{
|
||||||
Hash: MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8"),
|
Hash: plumbing.Hash(ObjectFormatFromID(Sha1).MustIDFromString("1d01fb729fb0db5881daaa6030f9f2d3cd3d5ae8").RawValue()),
|
||||||
Name: "example",
|
Name: "example",
|
||||||
Mode: filemode.Dir,
|
Mode: filemode.Dir,
|
||||||
},
|
},
|
||||||
@@ -65,8 +67,12 @@ func TestParseTreeEntries(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
entries, err := ParseTreeEntries([]byte(testCase.Input))
|
entries, err := ParseTreeEntries(ObjectFormatFromID(Sha1), []byte(testCase.Input))
|
||||||
assert.NoError(t, err)
|
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)
|
assert.EqualValues(t, testCase.Expected, entries)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ParseTreeEntries parses the output of a `git ls-tree -l` command.
|
// ParseTreeEntries parses the output of a `git ls-tree -l` command.
|
||||||
func ParseTreeEntries(data []byte) ([]*TreeEntry, error) {
|
func ParseTreeEntries(objectFormat ObjectFormat, data []byte) ([]*TreeEntry, error) {
|
||||||
return parseTreeEntries(data, nil)
|
return parseTreeEntries(objectFormat, data, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sepSpace = []byte{' '}
|
var sepSpace = []byte{' '}
|
||||||
|
|
||||||
func parseTreeEntries(data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
func parseTreeEntries(objectFormat ObjectFormat, data []byte, ptree *Tree) ([]*TreeEntry, error) {
|
||||||
var err error
|
var err error
|
||||||
entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1)
|
entries := make([]*TreeEntry, 0, bytes.Count(data, []byte{'\n'})+1)
|
||||||
for pos := 0; pos < len(data); {
|
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))
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid ls-tree output (invalid object id): %q, err: %w", line, err)
|
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
|
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)
|
fnameBuf := make([]byte, 4096)
|
||||||
modeBuf := make([]byte, 40)
|
modeBuf := make([]byte, 40)
|
||||||
shaBuf := make([]byte, 40)
|
shaBuf := make([]byte, objectFormat.FullLength())
|
||||||
entries := make([]*TreeEntry, 0, 10)
|
entries := make([]*TreeEntry, 0, 10)
|
||||||
|
|
||||||
loop:
|
loop:
|
||||||
for sz > 0 {
|
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 != nil {
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break loop
|
break loop
|
||||||
@@ -127,7 +127,7 @@ loop:
|
|||||||
return nil, fmt.Errorf("unknown mode: %v", string(mode))
|
return nil, fmt.Errorf("unknown mode: %v", string(mode))
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.ID = MustID(sha)
|
entry.ID = objectFormat.MustID(sha)
|
||||||
entry.name = string(fname)
|
entry.name = string(fname)
|
||||||
entries = append(entries, entry)
|
entries = append(entries, entry)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestParseTreeEntriesLong(t *testing.T) {
|
func TestParseTreeEntriesLong(t *testing.T) {
|
||||||
|
objectFormat := ObjectFormatFromID(Sha1)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
Input string
|
Input string
|
||||||
Expected []*TreeEntry
|
Expected []*TreeEntry
|
||||||
@@ -24,28 +26,28 @@ func TestParseTreeEntriesLong(t *testing.T) {
|
|||||||
`,
|
`,
|
||||||
Expected: []*TreeEntry{
|
Expected: []*TreeEntry{
|
||||||
{
|
{
|
||||||
ID: MustIDFromString("ea0d83c9081af9500ac9f804101b3fd0a5c293af"),
|
ID: objectFormat.MustIDFromString("ea0d83c9081af9500ac9f804101b3fd0a5c293af"),
|
||||||
name: "README.md",
|
name: "README.md",
|
||||||
entryMode: EntryModeBlob,
|
entryMode: EntryModeBlob,
|
||||||
size: 8218,
|
size: 8218,
|
||||||
sized: true,
|
sized: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: MustIDFromString("037f27dc9d353ae4fd50f0474b2194c593914e35"),
|
ID: objectFormat.MustIDFromString("037f27dc9d353ae4fd50f0474b2194c593914e35"),
|
||||||
name: "README_ZH.md",
|
name: "README_ZH.md",
|
||||||
entryMode: EntryModeBlob,
|
entryMode: EntryModeBlob,
|
||||||
size: 4681,
|
size: 4681,
|
||||||
sized: true,
|
sized: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: MustIDFromString("9846a94f7e8350a916632929d0fda38c90dd2ca8"),
|
ID: objectFormat.MustIDFromString("9846a94f7e8350a916632929d0fda38c90dd2ca8"),
|
||||||
name: "SECURITY.md",
|
name: "SECURITY.md",
|
||||||
entryMode: EntryModeBlob,
|
entryMode: EntryModeBlob,
|
||||||
size: 429,
|
size: 429,
|
||||||
sized: true,
|
sized: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: MustIDFromString("84b90550547016f73c5dd3f50dea662389e67b6d"),
|
ID: objectFormat.MustIDFromString("84b90550547016f73c5dd3f50dea662389e67b6d"),
|
||||||
name: "assets",
|
name: "assets",
|
||||||
entryMode: EntryModeTree,
|
entryMode: EntryModeTree,
|
||||||
sized: true,
|
sized: true,
|
||||||
@@ -54,7 +56,7 @@ func TestParseTreeEntriesLong(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
entries, err := ParseTreeEntries([]byte(testCase.Input))
|
entries, err := ParseTreeEntries(objectFormat, []byte(testCase.Input))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, entries, len(testCase.Expected))
|
assert.Len(t, entries, len(testCase.Expected))
|
||||||
for i, entry := range entries {
|
for i, entry := range entries {
|
||||||
@@ -64,6 +66,8 @@ func TestParseTreeEntriesLong(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestParseTreeEntriesShort(t *testing.T) {
|
func TestParseTreeEntriesShort(t *testing.T) {
|
||||||
|
objectFormat := ObjectFormatFromID(Sha1)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
Input string
|
Input string
|
||||||
Expected []*TreeEntry
|
Expected []*TreeEntry
|
||||||
@@ -74,12 +78,12 @@ func TestParseTreeEntriesShort(t *testing.T) {
|
|||||||
`,
|
`,
|
||||||
Expected: []*TreeEntry{
|
Expected: []*TreeEntry{
|
||||||
{
|
{
|
||||||
ID: MustIDFromString("ea0d83c9081af9500ac9f804101b3fd0a5c293af"),
|
ID: objectFormat.MustIDFromString("ea0d83c9081af9500ac9f804101b3fd0a5c293af"),
|
||||||
name: "README.md",
|
name: "README.md",
|
||||||
entryMode: EntryModeBlob,
|
entryMode: EntryModeBlob,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ID: MustIDFromString("84b90550547016f73c5dd3f50dea662389e67b6d"),
|
ID: objectFormat.MustIDFromString("84b90550547016f73c5dd3f50dea662389e67b6d"),
|
||||||
name: "assets",
|
name: "assets",
|
||||||
entryMode: EntryModeTree,
|
entryMode: EntryModeTree,
|
||||||
},
|
},
|
||||||
@@ -87,7 +91,7 @@ func TestParseTreeEntriesShort(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
entries, err := ParseTreeEntries([]byte(testCase.Input))
|
entries, err := ParseTreeEntries(objectFormat, []byte(testCase.Input))
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Len(t, entries, len(testCase.Expected))
|
assert.Len(t, entries, len(testCase.Expected))
|
||||||
for i, entry := range entries {
|
for i, entry := range entries {
|
||||||
@@ -98,7 +102,7 @@ func TestParseTreeEntriesShort(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseTreeEntriesInvalid(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
|
// 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.Error(t, err)
|
||||||
assert.Len(t, entries, 0)
|
assert.Len(t, entries, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
|
||||||
gogit "github.com/go-git/go-git/v5"
|
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"
|
"github.com/go-git/go-git/v5/plumbing/object"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,7 +27,7 @@ type LFSResult struct {
|
|||||||
SHA string
|
SHA string
|
||||||
Summary string
|
Summary string
|
||||||
When time.Time
|
When time.Time
|
||||||
ParentHashes []git.SHA1
|
ParentHashes []git.ObjectID
|
||||||
BranchName string
|
BranchName string
|
||||||
FullCommitName 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) }
|
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
|
// 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{}
|
resultsMap := map[string]*LFSResult{}
|
||||||
results := make([]*LFSResult, 0)
|
results := make([]*LFSResult, 0)
|
||||||
|
|
||||||
@@ -65,13 +66,18 @@ func FindLFSFile(repo *git.Repository, hash git.SHA1) ([]*LFSResult, error) {
|
|||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
break
|
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{
|
result := LFSResult{
|
||||||
Name: name,
|
Name: name,
|
||||||
SHA: gitCommit.Hash.String(),
|
SHA: gitCommit.Hash.String(),
|
||||||
Summary: strings.Split(strings.TrimSpace(gitCommit.Message), "\n")[0],
|
Summary: strings.Split(strings.TrimSpace(gitCommit.Message), "\n")[0],
|
||||||
When: gitCommit.Author.When,
|
When: gitCommit.Author.When,
|
||||||
ParentHashes: gitCommit.ParentHashes,
|
ParentHashes: parents,
|
||||||
}
|
}
|
||||||
resultsMap[gitCommit.Hash.String()+":"+name] = &result
|
resultsMap[gitCommit.Hash.String()+":"+name] = &result
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user